web-dev-qa-db-ja.com

Entity Framework 6と作業単位...どこで、いつ?それはado.netのトランザクションのようなものですか?

新しいMVCプロジェクトを作成し、データレイヤーのリポジトリのアイデアが好きなので、それらを実装しました。すべてのビジネスロジックと検証を処理するサービスレイヤーも作成しました。このレイヤーは適切なリポジトリを使用します。このようなもの(私はSimple Injectorを使用して注入しています)

ダルレイヤー

public class MyRepository {

    private DbContext _context;
    public MyRepository(DbContext context) {
        _context = context;
    }    

    public MyEntity Get(int id)
    {
        return _context.Set<MyEntity>().Find(id);
    }

    public TEntity Add(MyEntity t)
    {
        _context.Set<MyEntity>().Add(t);
        _context.SaveChanges();
        return t;
    }

    public TEntity Update(MyEntity updated, int key)
    {
        if (updated == null)
            return null;

        MyEntity existing = _context.Set<MyEntity>().Find(key);
        if (existing != null)
        {
            _context.Entry(existing).CurrentValues.SetValues(updated);
            _context.SaveChanges();
        }
        return existing;
    }

    public void Delete(MyEntity t)
    {
        _context.Set<MyEntity>().Remove(t);
        _context.SaveChanges();
    }
}

サービス層

public class MyService {
    private MyRepository _repository;

    public MyService(MyRepository repository) {
        _repository = repository;    
    }

    public MyEntity Get(int id)
    {
        return _repository.Get(id);
    }

    public MyEntity Add(MyEntity t)
    {
        _repository.Add(t);

        return t;
    }

    public MyEntity Update(MyEntity updated)
    {
        return _repository.Update(updated, updated.Id);
    }

    public void Delete(MyEntity t)
    {
        _repository.Delete(t);
    }
}

これは非常に簡単なので、次のコードを使用してオブジェクトを更新できます。

MyEntity entity = MyService.Get(123);
MyEntity.Name = "HELLO WORLD";
entity = MyService.Update(entity);

または、これはオブジェクトを作成します

MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
// entity.Id is now populated

今、私は別の作成IDに基づいてアイテムを更新する必要があると言います、私は何よりも上記のコードを使用できますが、エラーが発生した場合はどうなりますか?何らかのトランザクション/ロールバックが必要です。これは、作業単位パターンが解決すると想定しているものですか?

だから私はUnitOfWorkオブジェクトにDbContextが必要だと思うので、そのようなオブジェクトを作成しますか?

public class UnitOfWork : IDisposable {

    private DbContext _context;

    public UnitOfWork(DbContext context) {
        _context = context;
    }

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

    public Dispose() {
        _context.Dispose();
    }

}

繰り返しますが、これは非常に簡単です。 UnitOfWorkはコンテキストも保持し(私はとにかくすべてのリポジトリで同じコンテキストを使用します)、SaveChanges()メソッドを呼び出します。次に、リポジトリからSaveChanges()メソッド呼び出しを削除します。追加するには、次のようにします:

UnitOfWork uow = new UnitOfWork(new DbContext()); // i would inject this somehow

MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);

uow.Commit();

しかし、オブジェクトを作成し、そのIDに基づいて他のオブジェクトを更新する必要がある場合、uowでCommitを呼び出すまでIDが作成されないため、これは機能しません。例

UnitOfWork uow = new UnitOfWork(new DbContext()); // i would inject this somehow

MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
// entity.Id is NOT populated

MyEntity otherEntity = MyService.Get(123);
otherEntity.OtherProperty = entity.Id;
MyService.Update(otherEntity);

uow.Commit();  // otherEntity.OtherProperty is not linked.....?

だから私はこのUnitOfWorkクラスが正しくないと感じています...多分私は何かを理解するのが懐かしいです。

エンティティを追加してそのIDを取得し、別のエンティティで使用できるようにする必要がありますが、エラーが発生した場合は、ado.netトランザクションのように「ロールバック」したいです。

この機能は、Entity Frameworkとリポジトリを使用して可能ですか?

32
Gillardo

まず、この問題を解決するためのユニークな正しい方法がないことを言わなければなりません。私はここで私がおそらく何をするかを提示しています。


まず、DbContext自体が作業単位パターンを実装しますです。 SaveChangesの呼び出しdoes DBトランザクションを作成して、DBに対して実行されたすべてのクエリがロールバックされるようにします。

現在、現在の設計には大きな問題があります。リポジトリはSaveChangesDbContextを呼び出します。これは、リポジトリーが担当するXXXエンティティーの変更だけでなく、XXXRepositoryをコミットする責任があることを意味します作業単位で行ったすべての変更

もう1つは、DbContextもリポジトリそのものであるということです。したがって、別のリポジトリ内でDbContextの使用を抽象化すると、既存の抽象化に別の抽象化が作成されるだけで、コードが多すぎます。

さらに、YYYリポジトリのXXXエンティティとXXXリポジトリのYYYエンティティにアクセスする必要がある場合があるため、循環依存関係を避けるために、すべてのDbSetメソッドを複製するだけの無用な_MyRepository : IRepository<TEntity>_になってしまいます。

リポジトリレイヤー全体を削除します。サービスレイヤー内でDbContextを直接使用します。もちろん、サービス層で複製したくないすべての複雑なクエリをファクタリングできます。何かのようなもの:

_public MyService()
{
    ...
    public MyEntity Create(some parameters)
    {
        var entity = new MyEntity(some parameters);
        this.context.MyEntities.Add(entity);

        // Actually commits the whole thing in a transaction
        this.context.SaveChanges();

        return entity;
    }

    ...

    // Example of a complex query you want to use multiple times in MyService
    private IQueryable<MyEntity> GetXXXX_business_name_here(parameters)
    {
        return this.context.MyEntities
            .Where(z => ...)
            .....
            ;
    }
}
_

このパターンでは、_DbContext.SaveChanges_がトランザクションであるため、サービスクラスのすべてのパブリックコールはトランザクション内で実行されます。

ここで、最初のエンティティの挿入後に必要なIDを使用した例の場合、1つの解決策はIDではなくエンティティ自体を使用することです。そのため、Entity Frameworkと作業ユニットパターンの独自の実装に対処させます。

代わりに:

_var entity = new MyEntity();
entity = mydbcontext.Add(entity);
// what should I put here?
var otherEntity = mydbcontext.MyEntities.Single(z => z.ID == 123);
otherEntity.OtherPropertyId = entity.Id;

uow.Commit();
_

あなたが持っている:

_var entity = new MyEntity();
entity = mydbcontext.Add(entity);

var otherEntity = mydbcontext.MyEntities.Single(z => z.ID == 123);
otherEntity.OtherProperty = entity;     // Assuming you have a navigation property

uow.Commit();
_

ナビゲーションプロパティがない場合、または対処するより複雑なユースケースがある場合、解決策は、パブリックサービスメソッド内で適切なゴールドトランザクションを使用することです。

_public MyService()
{
    ...
    public MyEntity Create(some parameters)
    {
        // Encapuslates multiple SaveChanges calls in a single transaction
        // You could use a ITransaction if you don't want to reference System.Transactions directly, but don't think it's really useful
        using (var transaction = new TransactionScope())
        {
            var firstEntity = new MyEntity { some parameters };
            this.context.MyEntities.Add(firstEntity);

            // Pushes to DB, this'll create an ID
            this.context.SaveChanges();

            // Other commands here
            ...

            var newEntity = new MyOtherEntity { xxxxx };
            newEntity.MyProperty = firstEntity.ID;
            this.context.MyOtherEntities.Add(newEntity);

            // Pushes to DB **again**
            this.context.SaveChanges();

            // Commits the whole thing here
            transaction.Commit();

            return firstEntity;
        }
    }
}
_

必要に応じて、トランザクションスコープ内で複数のサービスメソッドを呼び出すこともできます。

_public class MyController()
{
    ...

    public ActionResult Foo()
    {
        ...
        using (var transaction = new TransactionScope())
        {
            this.myUserService.CreateUser(...);
            this.myCustomerService.CreateOrder(...);

            transaction.Commit();
        }
    }
}
_
43
ken2k