web-dev-qa-db-ja.com

Dapperで作業単位パターンを実装する方法は?

現在、作業単位+リポジトリパターンでDapper ORMを使用しようとしています。

挿入と更新にはある程度のトランザクション処理が必要なため、単純なdapperリポジトリではなく、作業ユニットを使用します。ほとんどがEntity Frameworkを使用しているようで、作業ユニット内でリークの問題があるため、有用な例を見つけることができませんでした。

誰かが私を正しい方向に向けてくれますか?

33
Stig

この Gitプロジェクト は非常に役立ちます。私は同じものから始め、必要に応じていくつかの変更を行いました。

public sealed class DalSession : IDisposable
{
    public DalSession()
    {
        _connection = new OleDbConnection(DalCommon.ConnectionString);
        _connection.Open();
        _unitOfWork = new UnitOfWork(_connection);
    }

    IDbConnection _connection = null;
    UnitOfWork _unitOfWork = null;

    public UnitOfWork UnitOfWork
    {
        get { return _unitOfWork; }
    }

    public void Dispose()
    {
        _unitOfWork.Dispose();
        _connection.Dispose();
    }
}

public sealed class UnitOfWork : IUnitOfWork
{
    internal UnitOfWork(IDbConnection connection)
    {
        _id = Guid.NewGuid();
        _connection = connection;
    }

    IDbConnection _connection = null;
    IDbTransaction _transaction = null;
    Guid _id = Guid.Empty;

    IDbConnection IUnitOfWork.Connection
    {
        get { return _connection; }
    }
    IDbTransaction IUnitOfWork.Transaction
    {
        get { return _transaction; }
    }
    Guid IUnitOfWork.Id
    {
        get { return _id; }
    }

    public void Begin()
    {
        _transaction = _connection.BeginTransaction();
    }

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

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

    public void Dispose()
    {
        if(_transaction != null)
            _transaction.Dispose();
        _transaction = null;
    }
}

interface IUnitOfWork : IDisposable
{
    Guid Id { get; }
    IDbConnection Connection { get; }
    IDbTransaction Transaction { get; }
    void Begin();
    void Commit();
    void Rollback();
}

これで、リポジトリは何らかの方法でこのUnitOfWorkを受け入れます。コンストラクターによる依存性注入を選択します。

public sealed class MyRepository
{
    public MyRepository(IUnitOfWork unitOfWork) 
    {
        this.unitOfWork = unitOfWork;
    }

    IUnitOfWork unitOfWork = null;

    //You also need to handle other parameters like 'sql', 'param' ect. This is out of scope of this answer.
    public MyPoco Get()
    {
        return unitOfWork.Connection.Query(sql, param, unitOfWork.Transaction, .......);
    }

    public void Insert(MyPoco poco)
    {
        return unitOfWork.Connection.Execute(sql, param, unitOfWork.Transaction, .........);
    }
}

そして、次のように呼び出します:

トランザクションあり:

using(DalSession dalSession = new DalSession())
{
    UnitOfWork unitOfWork = dalSession.UnitOfWork;
    unitOfWork.Begin();
    try
    {
        //Your database code here
        MyRepository myRepository = new MyRepository(unitOfWork);
        myRepository.Insert(myPoco);
        //You may create other repositories in similar way in same scope of UoW.

        unitOfWork.Commit();
    }
    catch
    {
        unitOfWork.Rollback();
        throw;
    }
}

トランザクションなし:

using(DalSession dalSession = new DalSession())
{
    //Your database code here
    MyRepository myRepository = new MyRepository(dalSession.UnitOfWork);//UoW have no effect here as Begin() is not called.
    myRepository.Insert(myPoco);
}

UnitOfWorkはDBTransactionよりも more であることに注意してください。

上記のコードのリポジトリの詳細については、 こちら をご覧ください。

私はすでにこのコードを投稿しました here 。しかし、この質問はこのコードにとって私にとってより関連性が高いように見えます。そのため、元の回答へのリンクだけではなく、再度投稿しています。

27
Amit Joshi

Edit 2018-08-03:Amitのコメントは本当に考えさせられ、リポジトリが実際にプロパティである必要はないことを認識させましたコンテキスト自体。むしろ、リポジトリはコンテキストに依存する可能性があります。以下のコードサンプルに増分変更を継続するのではなく。 git repo を単に参照します。この概念を含めるためにまとめました。

ここで他の人の肩の上に立っています。

この答えは、「dapper」と「unit of work」に関連するほとんどのGoogle検索でトップです。私は自分のアプローチを提供したかったのですが、今まで何度も大きな効果を上げてきました。

架空の(そして非常に単純化された)例の使用:

_public interface IUnitOfWorkFactory
{
    UnitOfWork Create();
}

public interface IDbContext 
{
    IProductRepository Product { get; set; }

    void Commit();
    void Rollback();
}

public interface IUnitOfWork
{
    IDbTransaction Transaction { get;set; }

    void Commit();
    void Rollback();
}


public interface IProductRepository 
{
    Product Read(int id);
}
_

IDbContextIUnitOfWorkFactoryもIDisposableを実装していないことに注意してください。これは、意図的に leaky abstraction を避けるために行われます。代わりに、クリーンアップと廃棄の面倒を見るためにCommit()/Rollback()に依存しています。

実装を共有する前のいくつかのポイント。

  • IUnitOfWorkFactoryは、UnitOfWorkをインスタンス化し、データベース接続を仲介します。
  • IDbContextはリポジトリバックボーンです。
  • IUnitOfWorkIDbTransactionのカプセル化であり、複数のリポジトリで作業する場合、それらが単一のデータベースコンテキストを共有することを保証します。

IUnitOfWorkFactoryの実装

_public class UnitOfWorkFactory<TConnection> : IUnitOfWorkFactory where TConnection : IDbConnection, new()
{
    private string connectionString;

    public UnitOfWorkFactory(string connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new ArgumentNullException("connectionString cannot be null");
        }

        this.connectionString = connectionString;
    }

    public UnitOfWork Create()
    {
        return new UnitOfWork(CreateOpenConnection());
    }

    private IDbConnection CreateOpenConnection()
    {
        var conn = new TConnection();
        conn.ConnectionString = connectionString;

        try
        {
            if (conn.State != ConnectionState.Open)
            {
                conn.Open();
            }
        }
        catch (Exception exception)
        {
            throw new Exception("An error occured while connecting to the database. See innerException for details.", exception);
        }

        return conn;
    }
}
_

IDbContextの実装

_public class DbContext : IDbContext
{
    private IUnitOfWorkFactory unitOfWorkFactory;

    private UnitOfWork unitOfWork;

    private IProductRepository product;

    public DbContext(IUnitOfWorkFactory unitOfWorkFactory)
    {
        this.unitOfWorkFactory = unitOfWorkFactory;
    }

    public ProductRepository Product =>
        product ?? (product = new ProductRepository(UnitOfWork));

    protected UnitOfWork UnitOfWork =>
        unitOfWork ?? (unitOfWork = unitOfWorkFactory.Create());

    public void Commit()
    {
        try
        {
            UnitOfWork.Commit();
        }
        finally
        {
            Reset();
        }
    }

    public void Rollback()
    {
        try
        {
            UnitOfWork.Rollback();
        }
        finally
        {
            Reset();
        }
    }

    private void Reset()
    {
        unitOfWork = null;
        product = null;
    }
}
_

IUnitOfWorkの実装

_public class UnitOfWork : IUnitOfWork
{
    private IDbTransaction transaction;

    public UnitOfWork(IDbConnection connection)
    {
        transaction = connection.BeginTransaction();
    }

    public IDbTransaction Transaction =>
        transaction;

    public void Commit()
    {
        try
        {
            transaction.Commit();
            transaction.Connection?.Close();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
        finally
        {
            transaction?.Dispose();
            transaction.Connection?.Dispose();
            transaction = null;
        }
    }

    public void Rollback()
    {
        try
        {
            transaction.Rollback();
            transaction.Connection?.Close();
        }
        catch
        {
            throw;
        }
        finally
        {
            transaction?.Dispose();
            transaction.Connection?.Dispose();
            transaction = null;
        }
    }
}
_

IProductRepositoryの実装

_public class ProductRepository : IProductRepository
{
    protected readonly IDbConnection connection;
    protected readonly IDbTransaction transaction;

    public ProductRepository(UnitOfWork unitOfWork)
    {
      connection = unitOfWork.Transaction.Connection;
      transaction = unitOfWork.Transaction;
    }

    public Product Read(int id)
    {
        return connection.QuerySingleOrDefault<Product>("select * from dbo.Product where Id = @id", new { id }, transaction: Transaction);
    }
}
_

データベースにアクセスするには、DbContextをインスタンス化するか、選択したIoCコンテナーを使用して挿入します(私は個人的に 。NET Core が提供するIoCコンテナーを使用します)。

_var unitOfWorkFactory = new UnitOfWorkFactory<SqlConnection>("your connection string");
var db = new DbContext(unitOfWorkFactory);

Product product = null;

try 
{
    product = db.Product.Read(1);
    db.Commit();
}
catch (SqlException ex)
{
    //log exception
    db.Rollback();
}
_

この単純な読み取り専用操作に対するCommit()の明示的な必要性は過剰に思えますが、システムが成長するにつれて利益をもたらします。そして、明らかに、 Sam Saffron に従って、マイナーなパフォーマンスの利点を提供します。また、単純な読み取り操作でdb.Commit()も省略できます。これを行うには、接続を開いたままにし、ガベージコレクターにクリーンアップの責任を負わせます。したがって、これは推奨されません。

通常、DbContextをサービス層のフォールドに配置します。そこで、他のサービスと連携して「ServiceContext」を形成します。次に、実際のMVCレイヤーでこのServiceContextを参照します。別の言及として、可能であれば、スタック全体でasyncを使用することをお勧めします。ここでは簡単にするために省略しています。

13
pim