web-dev-qa-db-ja.com

Entity Frameworkを使用してリポジトリのトランザクションを実装するにはどうすればよいですか?

2つの理由から、アプリケーションでリポジトリデザインパターンを利用しようとしています。

  1. ある時点でEntityFrameworkを使用しないことにした場合に備えて、アプリケーションをEntityから切り離すのが好きです。

  2. モデルと相互作用するロジックを再利用できるようにしたい

リポジトリパターンを正常にセットアップして使用しました。ただし、トランザクションという1つの複雑さを処理する必要があります。

トランザクションを使用して、リポジトリに対して複数の呼び出しを行ってから、コミットまたはロールバックできるようにしたい。

これが私のリポジトリインターフェースです

_using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Support.Repositories.Contracts
{
    public interface IRepository<TModel> where TModel : class
    {
        // Get records by it's primary key
        TModel Get(int id);

        // Get all records
        IEnumerable<TModel> GetAll();

        // Get all records matching a lambda expression
        IEnumerable<TModel> Find(Expression<Func<TModel, bool>> predicate);

        // Get the a single matching record or null
        TModel SingleOrDefault(Expression<Func<TModel, bool>> predicate);

        // Add single record
        void Add(TModel entity);

        // Add multiple records
        void AddRange(IEnumerable<TModel> entities);

        // Remove records
        void Remove(TModel entity);

        // remove multiple records
        void RemoveRange(IEnumerable<TModel> entities);
    }
}
_

次に、そのようなEntityFrameworkの実装を作成します

_using Support.Repositories.Contracts;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace Support.Repositories
{
    public class EntityRepository<TEntity> : IRepository<TEntity>
        where TEntity : class
    {
        protected readonly DbContext Context;
        protected readonly DbSet<TEntity> DbSet;

        public EntityRepository(DbContext context)
        {
            Context = context;
            DbSet = context.Set<TEntity>();
        }

        public TEntity Get(int id)
        {
            return DbSet.Find(id);
        }

        public IEnumerable<TEntity> GetAll()
        {
            return DbSet.ToList();
        }

        public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
        {
            return DbSet.Where(predicate);
        }

        public TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate)
        {
            return DbSet.SingleOrDefault(predicate);
        }

        public void Add(TEntity entity)
        {
            DbSet.Add(entity);
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            DbSet.AddRange(entities);
        }

        public void Remove(TEntity entity)
        {
            DbSet.Remove(entity);
        }

        public void RemoveRange(IEnumerable<TEntity> entities)
        {
            DbSet.RemoveRange(entities);
        }

    }
}
_

今、私はIUnitOfWorkを作成して、そのようにリポジトリと対話します

_using System;

namespace App.Repositories.Contracts
{
    public interface IUnitOfWork : IDisposable
    {
        IUserRepository Users { get; }
        IAddressRepository Addresses { get;  }
    }
}
_

次に、次のようにEntityFrameworkにこのインターフェイスを実装しました。

_using App.Contexts;
using App.Repositories.Contracts;
using App.Repositories.Entity;

namespace App.Repositories
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly AppContext _context;
        public IUserRepository  Users { get; private set; }
        public IAddressRepository Addresses { get; private set; }

        public UnitOfWork(AppContext context)
        {
            _context = context;

            Users = new UserRepository(_context);
            Addresses = new AddressRepository(_context);
        }

        public UnitOfWork() : this(new AppContext())
        {
        }

        public int Save()
        {
            return _context.SaveChanges();
        }

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

私はこのようなリポジトリを使用することができます

_using(var repository = new UnitOfWork())
{
     repository.Users.Add(new User(... User One ...))
     repository.Save();

     repository.Addresses(new Address(... Address For User One ...))
     repository.Save();

     repository.Users.Add(new User(... User Two...))
     repository.Save();

     repository.Addresses(new Address(... Address For User Two...))
     repository.Save();
}
_

今、私はデータベーストランザクションを使用できるようにしたいので、すべてが良好な場合にのみ、それ以外の場合はロールバックをコミットします。

私の最初の見解は、BeginTransaction()という新しいメソッドをUnitOfWorkクラスに追加することです。ただし、コードはEntityFrameworkのみに結合します。

今、私はBeginTransaction()Commit()およびRollback()メソッドを提供する新しいインターフェースを作成することを考えています。これにより、任意のORMの実装を作成できます。

つまり.

_namespace Support.Contracts
{
    public IRepositoryDatabase
    {
        SomethingToReturn BeginTransaction();

        void Commit();
        void Rollback();
    }
}
_

問題は、正しく実装できるように、IRepositoryDatabaseをUnitOfWorkにどのように結び付けるかです。そして、BeginTransaction()は何を返す必要がありますか?

6
Junior

私はそれを行う方法を考え出したと思います。 (私はそれを正しい方法でやったことを願っています)

これが私がしたことです、これが同じことをしようとしている誰かに役立つことを願っています。

私はそのような新しいインターフェースを作成しました

_using System;

    namespace Support.Repositories.Contracts
    {
        public interface IDatabaseTransaction : IDisposable
        {
            void Commit();

            void Rollback();
        }
    }
_

次に、そのようなEntityFrameworkにIDatabaseTransactionを実装しました

_using Support.Repositories.Contracts;
using System.Data.Entity;

namespace Support.Entity.Repositories
{
    public class EntityDatabaseTransaction : IDatabaseTransaction
    {
        private DbContextTransaction _transaction;

        public EntityDatabaseTransaction(DbContext context)
        {
            _transaction = context.Database.BeginTransaction();
        }

        public void Commit()
        {
            _transaction.Commit();
        }

        public void Rollback()
        {
            _transaction.Rollback();
        }

        public void Dispose()
        {
            _transaction.Dispose();
        }
    }
}
_

次に、BeginTransaction()という新しいメソッドをIUnitOfWorkコントラクトに追加しました。

_using System;

namespace App.Repositories.Contracts
{
    public interface IUnitOfWork : IDisposable
    {
        IDatabaseTransaction BeginTransaction();
        IUserRepository Users { get; }
        IAddressRepository Addresses { get;  }
    }
}
_

最後に、エンティティのUnitOfwork実装を以下に示します。

_using App.Contexts;
using App.Repositories.Contracts;
using App.Repositories.Entity;
using Support.Repositories;


namespace App.Repositories
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly AppContext _context;
        public IUserRepository  Users { get; private set; }
        public IAddressRepository Addresses { get; private set; }

        public UnitOfWork(AppContext context)
        {
            _context = context;

            Users = new UserRepository(_context);
            Addresses = new AddressRepository(_context);
        }

        public UnitOfWork() : this(new AppContext())
        {
        }

        public int Save()
        {
            return _context.SaveChanges();
        }

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

        public IDatabaseTransaction BeginTransaction()
        {
            return new EntityDatabaseTransaction(_context);
        }
    }
}
_

そして、これが私のコントローラーからUnitOfWork実装を消費する方法です

_using(var unitOfWork = new UnitOfWork())
using(var transaction = new unitOfWork.BeginTransaction())
{
     try
     {
         repository.Users.Add(new User(... User One ...))
         repository.Save();

         repository.Addresses(new Address(... Address For User One ...))
         repository.Save();

         repository.Users.Add(new User(... User Two...))
         repository.Save();

         repository.Addresses(new Address(... Address For User Two...))
         repository.Save();
         transaction.Commit();
     } 
     catch(Exception)
     {
          transaction.Rollback();
     }

}
_
13
Junior

Rufo卿のコメントは正しいですが、EFに依存しないソリューションが必要だとおっしゃっていました。通常、ORMから抽象化するのはやり過ぎですが、それでもトランザクションを自分で処理することにしている場合は、TransactionScopeを使用できます(これは明らかに、_context.Database_にBeginTransactionを含める前に、トランザクションを制御する方法でした。

詳細については、次の記事を参照してください。 https://msdn.Microsoft.com/en-us/data/dn456843.aspx

関連するビットは、すべての呼び出しをTransactionScopeで囲むことができることです(これは、他のORMでも実際に箱から出して機能します)。

_using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Transactions; 

namespace TransactionsExamples 
{ 
    class TransactionsExample 
    { 
        static void UsingTransactionScope() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required)) 
            { 
                using (var conn = new SqlConnection("...")) 
                { 
                    conn.Open(); 

                    var sqlCommand = new SqlCommand(); 
                    sqlCommand.Connection = conn; 
                    sqlCommand.CommandText = 
                        @"UPDATE Blogs SET Rating = 5" + 
                            " WHERE Name LIKE '%Entity Framework%'"; 
                    sqlCommand.ExecuteNonQuery(); 

                    using (var context = 
                        new BloggingContext(conn, contextOwnsConnection: false)) 
                    { 
                        var query = context.Posts.Where(p => p.Blog.Rating > 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
                        context.SaveChanges(); 
                    } 
                } 

                scope.Complete(); 
            } 
        } 
    } 
}
_

ただし、次の注意事項に注意する必要があります。

TransactionScopeアプローチにはまだいくつかの制限があります。

  • 非同期メソッドを使用するには、.NET4.5.1以降が必要です
  • 接続が1つしかないことが確実でない限り、クラウドシナリオでは使用できません(クラウドシナリオは分散トランザクションをサポートしていません)
  • 前のセクションのDatabase.UseTransaction()アプローチと組み合わせることはできません
  • DDLを発行し(データベースイニシャライザーが原因など)、MSDTCサービスを介した分散トランザクションを有効にしていない場合は、例外がスローされます。
0
Fredy Treboux