web-dev-qa-db-ja.com

作業ユニット+リポジトリパターン:ビジネストランザクション概念の崩壊

Unit of WorkおよびRepository Patternは、最近かなり広く使用されているものです。 As Martin Fowler saysUoWを使用する目的は、ビジネストランザクションを形成することです無知)。多くの実装をレビューしました。特定の詳細(具体的/抽象クラス、インターフェースなど)を無視すると、次の内容とほぼ同じです。

public class RepositoryBase<T>
{
    private UoW _uow;
    public RepositoryBase(UoW uow) // injecting UoW instance via constructor
    {
       _uow = uow;
    }
    public void Add(T entity)
    {
       // Add logic here
    }
    // +other CRUD methods
}

public class UoW
{
    // Holding one repository per domain entity

    public RepositoryBase<Order> OrderRep { get; set; }
    public RepositoryBase<Customer> CustomerRep { get; set; }
    // +other repositories

    public void Commit()
    {
       // Psedudo code: 
       For all the contained repositories do:
           store repository changes.
    }
}

今私の問題:

UoWは、パブリックメソッドCommitを公開して、変更を保存します。また、各リポジトリにはUoWの共有インスタンスがあるため、各RepositoryはUoWのメソッドCommitにアクセスできます。 1つのリポジトリで呼び出すと、他のすべてのリポジトリにも変更が保存されます。したがって、結果としてトランザクションの概念全体が崩壊します。

class Repository<T> : RepositoryBase<T>
{
    private UoW _uow;
    public void SomeMethod()
    {
        // some processing or data manipulations here
        _uow.Commit(); // makes other repositories also save their changes
    }
}

これは許されてはならないと思います。 UoW(ビジネストランザクション)の目的を考慮すると、メソッドCommitは、たとえばビジネストランザクションを開始した人にのみ公開する必要があります。ビジネス層。私が驚いたのは、この問題に対処する記事が見つからなかったことです。すべてのCommitは、注入されるレポによって呼び出すことができます。

PS:CommitRepositoryを呼び出さないように開発者に指示できることは知っていますが、信頼できるアーキテクチャは信頼できる開発者よりも信頼性が高いです!

56
Alireza

あなたの懸念に同意します。作業単位を開く最も外側の機能が、コミットするか中止するかを決定する、周囲の作業単位を持つことを好みます。呼び出される関数は、作業単位スコープを開くことができます。作業単位スコープがある場合は自動的にアンビエントUoWに参加し、存在しない場合は新しいUoWを作成します。

私が使用したUnitOfWorkScopeの実装は、TransactionScopeの仕組みに大きな影響を受けています。アンビエント/スコープアプローチを使用すると、依存性注入の必要性もなくなります。

クエリを実行するメソッドは次のようになります。

_public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}
_

書き込むメソッドは次のようになります。

_using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}
_

uow.SaveChanges()呼び出しは、これがルート(一番近い)スコープである場合にのみ、データベースへの実際の保存を行うことに注意してください。それ以外の場合は、ルートスコープが変更を保存することを許可することを「承認」と解釈します。

UnitOfWorkScopeの実装全体は、次の場所から入手できます。 http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

27
Anders Abel

リポジトリをUoWのメンバーにします。リポジトリにUoWを「表示」させないでください。 UoWにトランザクションを処理させます。

9
Chalky

UnitOfWorkを渡すのではなく、必要なメソッドを持つインターフェースを渡します。必要に応じて、元の具象UnitOfWork実装でそのインターフェイスを実装できます。

public interface IDbContext
{
   void Add<T>(T entity);
}

public interface IUnitOfWork
{
   void Commit();
}

public class UnitOfWork : IDbContext, IUnitOfWork
{
   public void Add<T>(T entity);
   public void Commit();
}

public class RepositoryBase<T>
{
    private IDbContext _c;

    public RepositoryBase(IDbContext c) 
    {
       _c = c;
    }

    public void Add(T entity)
    {
       _c.Add(entity)
    }
}

[〜#〜] edit [〜#〜]

これを投稿した後、私は再考しました。 UnitOfWork実装でAddメソッドを公開することは、2つのパターンの組み合わせであることを意味します。

私は自分のコードでEntity Frameworkを使用しており、そこで使用される DbContext は「作業単位とリポジトリパターンの組み合わせ」として説明されています。

この2つを分割する方が良いと思います。つまり、DbContextの周りに2つのラッパーが必要です。1つは作業単位ビット用、もう1つはリポジトリビット用です。そして、RepositoryBaseでリポジトリのラッピングを行います。

主な違いは、UnitOfWorkをリポジトリに渡さず、DbContextを渡すことです。つまり、BaseRepositorySaveChanges上のDbContextにアクセスできます。また、カスタムリポジトリはBaseRepositoryを継承することを目的としているため、DbContextへのアクセスも取得します。したがって、開発者couldがそのDbContextを使用するカスタムリポジトリにコードを追加する可能性があります。だから、私の「ラッパー」は少し漏れやすいと思う...

リポジトリコンストラクターに渡してそれを閉じることができるDbContextの別のラッパーを作成する価値はありますか?確かではない...

DbContextを渡す例:

リポジトリと作業単位の実装

Entity Frameworkのリポジトリと作業単位

ジョンパパの元のソースコード

4
Colin

これが尋ねられてからしばらく経っており、人々は老齢で亡くなったり、経営陣に転勤したりしたかもしれないが、ここに行くことに気づく。

データベース、トランザクションコントローラー、および2フェーズコミットプロトコルからインスピレーションを得て、パターンに対する次の変更が機能するはずです。

  1. FowlerのP of EAA bookに記述されている作業単位インターフェースを実装しますが、リポジトリを各UoWメソッドに注入します。
  2. 作業ユニットを各リポジトリ操作に注入します。
  3. 各リポジトリ操作は適切なUoW操作を呼び出し、それ自体を注入します。
  4. リポジトリにCanCommit()、Commit()、Rollback()の2フェーズコミットメソッドを実装します。
  5. 必要に応じて、UoWでコミットすると、各リポジトリでコミットを実行したり、データストア自体にコミットしたりできます。必要に応じて、2フェーズコミットを実装することもできます。

これにより、リポジトリとUoWの実装方法に応じて、さまざまな構成をサポートできます。例えばトランザクションのない単純なデータストア、単一のRDBM、複数の異種データストアなど。データストアとその相互作用は、状況に応じて、リポジトリまたはUoWのいずれかで行うことができます。

interface IEntity
{
    int Id {get;set;}
}

interface IUnitOfWork()
{
    void RegisterNew(IRepsitory repository, IEntity entity);
    void RegisterDirty(IRepository respository, IEntity entity);
    //etc.
    bool Commit();
    bool Rollback();
}

interface IRepository<T>() : where T : IEntity;
{
    void Add(IEntity entity, IUnitOfWork uow);
    //etc.
    bool CanCommit(IUnitOfWork uow);
    void Commit(IUnitOfWork uow);
    void Rollback(IUnitOfWork uow);
}

ユーザーコードは、DBの実装に関係なく常に同じで、次のようになります。

// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();

元の投稿に戻ります。 UoWを各リポジトリ操作にインジェクトするメソッドであるため、UoWを各リポジトリに格納する必要はありません。つまり、リポジトリのCommit()をスタブアウトでき、UoWのCommitで実際のDBコミットを実行できます。

3
Patrick Farry

.NETでは、データアクセスコンポーネントは通常、周囲のトランザクションに自動的に参加します。したがって、-トランザクション内で変更を保存する変更を永続化するためにトランザクションをコミットするから分離されます。

別の言い方をすると、トランザクションスコープを作成すると、開発者が必要なだけ保存できるようになります。トランザクションがコミットされるまで、データベースの観察可能な状態は更新されません(観察可能なものはトランザクション分離レベルによって異なります)。

これは、C#でトランザクションスコープを作成する方法を示しています。

using (TransactionScope scope = new TransactionScope())
{
    // Your logic here. Save inside the transaction as much as you want.

    scope.Complete(); // <-- This will complete the transaction and make the changes permanent.
}
2
lightbricko

私も最近この設計パターンを調査しており、作業単位と汎用リポジトリパターンを利用して、リポジトリ実装の作業単位「変更の保存」を抽出できました。私のコードは次のとおりです。

public class GenericRepository<T> where T : class
{
  private MyDatabase _Context;
  private DbSet<T> dbset;

  public GenericRepository(MyDatabase context)
  {
    _Context = context;
    dbSet = context.Set<T>();
  }

  public T Get(int id)
  {
    return dbSet.Find(id);
  }

  public IEnumerable<T> GetAll()
  {
    return dbSet<T>.ToList();
  }

  public IEnumerable<T> Where(Expression<Func<T>, bool>> predicate)
  {
    return dbSet.Where(predicate);
  }
  ...
  ...
}

基本的に、データコンテキストを渡し、基本的なGet、GetAll、Add、AddRange、Remove、RemoveRange、WhereのエンティティフレームワークのdbSetメソッドを利用するだけです。

次に、これらのメソッドを公開するための汎用インターフェイスを作成します。

public interface <IGenericRepository<T> where T : class
{
  T Get(int id);
  IEnumerable<T> GetAll();
  IEnumerabel<T> Where(Expression<Func<T, bool>> predicate);
  ...
  ...
}

ここで、エンティティフレームワークで各エンティティのインターフェイスを作成し、IGenericRepositoryから継承して、インターフェイスが継承されたリポジトリ内にメソッドシグネチャを実装することを期待します。

例:

public interface ITable1 : IGenericRepository<table1>
{
}

すべてのエンティティでこの同じパターンに従います。これらのインターフェイスには、エンティティに固有の関数シグネチャも追加します。これにより、リポジトリでGenericRepositoryメソッドとインターフェイスで定義されたカスタムメソッドを実装する必要が生じます。

リポジトリについては、このように実装します。

public class Table1Repository : GenericRepository<table1>, ITable1
{
  private MyDatabase _context;

  public Table1Repository(MyDatabase context) : base(context)
  {
    _context = context;
  }
} 

上記のリポジトリの例では、table1リポジトリを作成し、「table1」タイプのGenericRepositoryを継承しています。次にITable1インターフェイスを継承しています。これにより、汎用のdbSetメソッドが自動的に実装されるため、カスタムリポジトリメソッドがある場合にのみ集中できます。 dbContextをコンストラクターに渡すと、dbContextも基本の汎用リポジトリーにも渡す必要があります。

ここから、作業ユニットのリポジトリとインターフェイスを作成します。

public interface IUnitOfWork
{
  ITable1 table1 {get;}
  ...
  ...
  list all other repository interfaces here.

  void SaveChanges();
} 

public class UnitOfWork : IUnitOfWork
{
  private readonly MyDatabase _context;
  public ITable1 Table1 {get; private set;}

  public UnitOfWork(MyDatabase context)
  {
    _context = context; 

    // Initialize all of your repositories here
    Table1 = new Table1Repository(_context);
    ...
    ...
  }

  public void SaveChanges()
  {
    _context.SaveChanges();
  }
}

システム内の他のすべてのコントローラーが継承するカスタムコントローラーでトランザクションスコープを処理します。このコントローラーは、デフォルトのMVCコントローラーを継承します。

public class DefaultController : Controller
{
  protected IUnitOfWork UoW;

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    UoW = new UnitOfWork(new MyDatabase());
  }

  protected override void OnActionExecuted(ActionExecutedContext filterContext) 
  {
    UoW.SaveChanges();
  }
}

この方法でコードを実装します。アクションの開始時にサーバーに要求が行われるたびに、新しいUnitOfWorkが作成され、すべてのリポジトリが自動的に作成され、コントローラーまたはクラスのUoW変数にアクセスできるようになります。これにより、SaveChanges()がリポジトリから削除され、UnitOfWorkリポジトリ内に配置されます。そして最後に、このパターンは、依存性注入によってシステム全体で単一のdbContextのみを利用できます。

単一のコンテキストでの親/子の更新が心配な場合は、更新、挿入、および削除機能にストアドプロシージャを利用し、アクセス方法にエンティティフレームワークを利用できます。

2
logan gilley

はい、この質問は私にとって懸念事項であり、ここでそれをどのように扱うかを示します。

まず第一に、私の理解では、ドメインモデルは作業単位について知らないはずです。ドメインモデルは、トランザクションストレージの存在を暗示しないインターフェイス(または抽象クラス)で構成されます。実際、anyストレージの存在についてはまったく知りません。したがって、ドメインModelという用語です。

作業単位はDomain Model Implementationレイヤーに存在します。これが私の用語だと思います。つまり、データアクセスレイヤーを組み込むことでドメインモデルインターフェイスを実装するレイヤーを意味します。通常、DALとしてORMを使用しているため、組み込みのUoWが付属しています(保留中の変更をコミットするEntity Framework SaveChangesまたはSubmitChangesメソッド)。ただし、その1つはDALに属し、発明者の魔法は必要ありません。

一方、「DALへの変更のコミット」の部分を抽象化する必要があるため、ドメインモデル実装レイヤーに必要なUoWを参照しています。そのためには、Anders Abelのソリューション(再帰的スクロップ)を使用します。これは、解決する必要がある2つのことを解決するためですワンショット

  • 集約がスコープのイニシエーターである場合、1つのトランザクションとして集約の保存をサポートする必要があります。
  • 集約がスコープのイニシエーターではなく、その一部である場合、parentトランザクションの一部として集約の保存をサポートする必要があります。
0
Tengiz