web-dev-qa-db-ja.com

リポジトリパターンのトランザクション

リポジトリパターンを使用して、トランザクション方式で複数のエンティティの保存をカプセル化するにはどうすればよいですか?たとえば、注文を追加し、その注文の作成に基づいて顧客のステータスを更新したいが、注文が正常に完了した場合にのみ更新するとどうなりますか?この例では、注文は顧客内のコレクションではないことに注意してください。彼らは彼ら自身の実体です。

これは単なる不自然な例であるため、注文が顧客オブジェクト内にあるべきかどうか、あるいは同じ制限されたコンテキスト内にあるべきかどうかはあまり気にしません。基盤となるテクノロジー(nHibernate、EF、ADO.Net、Linqなど)がどのように使用されるかはあまり気にしません。この明らかに考案されたオールオアナッシング操作の例で、呼び出しコードがどのように見えるかを確認したいだけです。

42
Charles Graham

今朝コンピューターを起動すると、自分が取り組んでいるプロジェクトの正確な問題に直面しました。私は次のデザインにつながるいくつかのアイデアを持っていました-そしてコメントは素晴らしい以上のものになるでしょう。残念ながら、Joshが提案した設計は不可能です。これは、リモートSQLサーバーを操作する必要があり、依存するDistribute TransactionCoordinatorサービスを有効にできないためです。

私のソリューションは、既存のコードに対するいくつかの簡単な変更に基づいています。

まず、すべてのリポジトリに単純なマーカーインターフェイスを実装させます。

_/// <summary>
/// A base interface for all repositories to implement.
/// </summary>
public interface IRepository
{ }
_

次に、トランザクションが有効なすべてのリポジトリに次のインターフェイスを実装させます。

_/// <summary>
/// Provides methods to enable transaction support.
/// </summary>
public interface IHasTransactions : IRepository
{
    /// <summary>
    /// Initiates a transaction scope.
    /// </summary>
    void BeginTransaction();

    /// <summary>
    /// Executes the transaction.
    /// </summary>
    void CommitTransaction();
}
_

アイデアは、すべてのリポジトリでこのインターフェイスを実装し、実際のプロバイダーに応じてトランザクションを直接導入するコードを追加することです(偽のリポジトリの場合、コミット時に実行されるデリゲートのリストを作成しました)。 LINQ to SQLの場合、次のような実装を簡単に行うことができます。

_#region IHasTransactions Members

public void BeginTransaction()
{
    _db.Transaction = _db.Connection.BeginTransaction();
}

public void CommitTransaction()
{
    _db.Transaction.Commit();
}

#endregion
_

もちろん、これにはスレッドごとに新しいリポジトリクラスを作成する必要がありますが、これは私のプロジェクトにとっては妥当です。

リポジトリがIHasTransactionsを実装している場合、リポジトリを使用する各メソッドはBeginTransaction()EndTransaction()を呼び出す必要があります。この呼び出しをさらに簡単にするために、次の拡張機能を考え出しました。

_/// <summary>
/// Extensions for spawning and subsequently executing a transaction.
/// </summary>
public static class TransactionExtensions
{
    /// <summary>
    /// Begins a transaction if the repository implements <see cref="IHasTransactions"/>.
    /// </summary>
    /// <param name="repository"></param>
    public static void BeginTransaction(this IRepository repository)
    {
        var transactionSupport = repository as IHasTransactions;
        if (transactionSupport != null)
        {
            transactionSupport.BeginTransaction();
        }
    }

    public static void CommitTransaction(this IRepository repository)
    {
        var transactionSupport = repository as IHasTransactions;
        if (transactionSupport != null)
        {
            transactionSupport.CommitTransaction();
        }
    }
}
_

コメントは大歓迎です!

14
Troels Thomsen

ある種のトランザクションスコープ/コンテキストシステムの使用を検討します。したがって、大まかに.NetとC#に基づいた次のコードがある可能性があります。

public class OrderService
{

public void CreateNewOrder(Order order, Customer customer)
{
  //Set up our transactional boundary.
  using (TransactionScope ts=new TransactionScope())
  {
    IOrderRepository orderRepos=GetOrderRespository();
    orderRepos.SaveNew(order);
    customer.Status=CustomerStatus.OrderPlaced;

    ICustomerRepository customerRepository=GetCustomerRepository();
    customerRepository.Save(customer)
    ts.Commit();   
   }
}
}

TransactionScopeはネストできるため、アプリケーションがTransactionScopeを作成する複数のサービスにまたがるアクションがあったとします。現在の.netでは、TransactionScopeを使用すると、DTCにエスカレートするリスクがありますが、これは将来解決される予定です。

基本的にDB接続を管理し、ローカルSQLトランザクションを使用する独自のTransactionScopeクラスを作成しました。

10
JoshBerke

Spring.NET AOP + NHibernateを使用すると、通常どおりリポジトリクラスを記述し、カスタムXMLファイルでトランザクションを構成できます。

public class CustomerService : ICustomerService
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IOrderRepository _orderRepository;

    public CustomerService(
        ICustomerRepository customerRepository, 
        IOrderRepository orderRepository) 
    {
        _customerRepository = customerRepository;
        _orderRepository = orderRepository;
    }

    public int CreateOrder(Order o, Customer c) 
    {
        // Do something with _customerRepository and _orderRepository
    }
}

XMLファイルで、トランザクション内で実行するメソッドを選択します。

  <object id="TxProxyConfigurationTemplate" 
          abstract="true"
          type="Spring.Transaction.Interceptor.TransactionProxyFactoryObject, Spring.Data">

    <property name="PlatformTransactionManager" ref="HibernateTransactionManager"/>

    <property name="TransactionAttributes">
      <name-values>
        <add key="Create*" value="PROPAGATION_REQUIRED"/>
      </name-values>
    </property>
  </object>

  <object id="customerService" parent="TxProxyConfigurationTemplate">
    <property name="Target">
      <object type="MyNamespace.CustomerService, HibernateTest">
          <constructor-arg name="customerRepository" ref="customerRepository" />
          <constructor-arg name="orderRepository" ref="orderRepository" />
      </object>
    </property>

  </object>

そして、コードで次のようなCustomerServiceクラスのインスタンスを取得します。

ICustomerService customerService = (ICustomerService)ContextRegistry
    .GetContent()
    .GetObject("customerService");

Spring.NETは、CreateOrderメソッドを呼び出すときにトランザクションを適用するCustomerServiceクラスのプロキシを返します。このように、サービスクラス内にトランザクション固有のコードはありません。 AOPがそれを処理します。詳細については、 Spring.NET のドキュメントを参照してください。

5
Darin Dimitrov

リポジトリパターンを使用して、トランザクション方式で複数のエンティティの保存をカプセル化するにはどうすればよいですか?たとえば、注文を追加し、その注文の作成に基づいて顧客のステータスを更新したいが、注文が正常に完了した場合にのみ更新するとどうなりますか?この例では、注文は顧客内のコレクションではないことに注意してください。彼らは彼ら自身の実体です。

それはリポジトリの責任ではなく、通常はより高いレベルで行われるものです。特定のテクノロジーに興味がないとおっしゃっていましたが、ソリューションを限定する価値があると思います。たとえば、WebアプリでNHibernateを使用する場合は、おそらく session-per request の使用を検討します。

したがって、トランザクションをより高いレベルで管理できる場合、私の2つのオプションは次のようになります。

  1. 事前チェック-たとえば、動作を調整するサービスでは、注文/顧客に尋ねて続行するかどうかを決定します。その後、どちらも更新しようとしないでください。
  2. ロールバック-顧客/注文の更新を続行し、データベーストランザクションのロールバックの途中で問題が発生した場合。

2番目のオプションを選択した場合、問題はメモリ内オブジェクトに何が起こるかということです。顧客は一貫性のない状態のままになる可能性があります。それが重要であり、オブジェクトがそのリクエストに対してのみロードされたためにロードされないシナリオで作業している場合は、他の方法よりもはるかに簡単であるため、可能であれば事前チェックを検討します(ロールバック-メモリの変更またはオブジェクトの再読み込み)。

4
Colin Jack

作業単位パターンの実装を検討したいとします。 NHibernateの実装があります。 1つはRhinoCommonsプロジェクトにあり、Machine.UoWもあります。

3
Garry Shutler

トランザクションで実行するメソッドの最後にトランザクションパラメータを追加し、デフォルト値をnullにすることができます。したがって、既存のトランザクションでメソッドを実行したくない場合は、endパラメーターを省略するか、明示的にnullを渡します。

これらのメソッド内で、nullのパラメーターをチェックして、新しいトランザクションを作成するか、渡されたトランザクションを使用するかを決定できます。たとえば、このロジックを基本クラスにプッシュできます。

これにより、コンテキストベースのソリューションを使用する場合よりもメソッドが純粋に保たれますが、後者はおそらくジェネリックライブラリに適しています。ただし、スタンドアロンアプリでは、トランザクション内でどのメソッドをチェーン化する必要があるかがわかっており、すべてではありません。

void Update(int itemId, string text, IDbTransaction trans = null) =>
   RunInTransaction(ref trans, () =>
   {
      trans.Connection.Update("...");
   });

void RunInTransaction(ref IDbTransaction transaction, Action f)
{
    if (transaction == null)
    {
        using (var conn = DatabaseConnectionFactory.Create())
        {
            conn.Open();

            using (transaction = conn.BeginTransaction())
            {
                f();

                transaction.Commit();
            }
        }
    }
    else
    {
        f();
    }
}

Update(1, "Hello World!");
Update(1, "Hello World!", transaction);

次に、サービスレイヤーのトランザクションランナーを作成できます...

public class TransactionRunner : ITransactionRunner
{
    readonly IDatabaseConnectionFactory databaseConnectionFactory;

    public TransactionRunner(IDatabaseConnectionFactory databaseConnectionFactory) =>
        this.databaseConnectionFactory = databaseConnectionFactory;

    public void RunInTransaction(Action<IDbTransaction> f)
    {
        using (var conn = databaseConnectionFactory.Create())
        {
            conn.Open();

            using (var transaction = conn.BeginTransaction())
            {
                f(transaction);

                transaction.Commit();
            }
        }
    }

    public async Task RunInTransactionAsync(Func<IDbTransaction, Task> f)
    {
        using (var conn = databaseConnectionFactory.Create())
        {
            conn.Open();

            using (var transaction = conn.BeginTransaction())
            {
                await f(transaction);

                transaction.Commit();
            }
        }
    }
}

そして、サービスメソッドは次のようになります...

void MyServiceMethod(int itemId, string text1, string text2) =>
   transactionRunner.RunInTransaction(trans =>
   {
      repos.UpdateSomething(itemId, text1, trans);
      repos.UpdateSomethingElse(itemId, text2, trans);
   });

ユニットテストのためにモックするのは簡単です...

public class MockTransactionRunner : ITransactionRunner
{
    public void RunInTransaction(Action<IDbTransaction> f) => f(null);
    public Task RunInTransactionAsync(Func<IDbTransaction, Task> f) => f(null);
}
1
Ian Warburton