如何用Dapper实现工作单元模式?

Sti*_*tig 35 unit-of-work repository-pattern dapper

目前,我正在尝试使用Dapper ORM与工作单元+存储库模式.

我想使用工作单元而不是简单的dapper存储库,因为我的插入和更新需要一定程度的事务处理.我一直无法找到任何有用的例子,因为大多数似乎使用实体框架并且在工作单元内存在泄漏问题.

有人可以指点我正确的方向吗?

Ami*_*shi 29

这个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();
}
Run Code Online (Sandbox Code Playgroud)

现在,您的存储库应该以某种方式接受此UnitOfWork.我选择了使用Constructor的依赖注入.

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, .........);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你这样称呼它:

随着交易:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

没有交易:

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);
}
Run Code Online (Sandbox Code Playgroud)

请注意,是的UnitOfWork 比DBTransaction.

有关上述代码中存储库的更多详细信息,请参见此处.

我已经在这里发布了这段代码.但是对于这个代码,这个问题对我来说更相关; 所以我再次发帖而不是只链接到原始答案.

  • @kkuilla:我对与数据访问层相关的类执行集成测试。这些测试适用于实际连接;我不需要嘲笑它。执行数据库操作是 DAL 的主要目的。通过模拟依赖关系进行单元测试并不能达到主要目的。看看[这个](/sf/answers/1053453901/)帖子。 (3认同)

rob*_*pim 17

编辑2018-08-03: Amit的评论让我思考,并让我意识到存储库实际上并不需要成为上下文本身的属性.但是,存储库可能依赖于上下文.而不是继续对下面的代码示例进行增量更改.我将简单地引用一个git repo,我把它放在一起来包含这个概念.

站在别人的肩膀上.

考虑到这个答案在大多数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);
}
Run Code Online (Sandbox Code Playgroud)

注意两者都没有IDbContextIUnitOfWorkFactory实现IDisposable.这是故意做的,以避免漏洞抽象.相反,依赖于Commit()/ Rollback()处理清理和处置.

共享实现之前的几点.

  • IUnitOfWorkFactory负责实例化UnitOfWork和代理数据库连接.
  • IDbContext 是存储库主干.
  • IUnitOfWork是一个封装IDbTransaction,并确保在使用多个存储库时,它们共享一个数据库上下文.

实施 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

实施 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

实施 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

实施 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

要访问数据库,只需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();
}
Run Code Online (Sandbox Code Playgroud)

Commit()对这种简单的只读操作的明确需求似乎过多,但随着系统的增长会带来好处.显然,根据Sam Saffron的说法,提供了一个小的性能优势.你"可以"也省略了db.Commit()简单的读取操作,通过这样做虽然你将连接悬空打开并将清理事务的责任放在垃圾收集器上.所以不建议这样做.

我通常将其DbContext带入服务层,在其中与其他服务协同工作以形成"ServiceContext".然后,我在实际的MVC层中引用此ServiceContext.作为另一点提及,async如果可以的话,建议在整个堆栈中使用.为简单起见,这里省略.

  • 我需要在`DbContext` 类中实例化我的所有存储库吗?如果是这样,那么它就违反了 SRP。每次引入新存储库时,我都必须更改此类。 (2认同)

Neu*_*ino 10

为此不需要手动解决方案。使用框架中已有的类可以非常简单地实现您想要的。

/// <summary>
/// Register a single instance using whatever DI system you like.
/// </summary>
class ConnectionFactory
{
    private string _connectionString;

    public ConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    public IDbConnection CreateConnection()
    {
        return new SqlConnection(_connectionString);
    }
}


/// <summary>
/// Generally, in a properly normalized database, your repos wouldn't map to a single table,
/// but be an aggregate of data from several tables.
/// </summary>
class ProductRepo
{
    private ConnectionFactory _connectionFactory;

    public ProductRepo(ConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public Product Get(int id)
    {
        // Allow connection pooling to worry about connection lifetime, that's its job.
        using (var con = _connectionFactory.CreateConnection())
        {
            return con.Get<Product>(id);
        }
    }

    // ...
}

class OrderRepo
{
    // As above.
    // ...
}

class ProductController : ControllerBase
{
    private ProductRepo _productRepo;
    private OrderRepo _orderRepo;

    public ProductController(ProductRepo productRepo, OrderRepo orderRepo)
    {
        _productRepo = productRepo;
        _orderRepo = orderRepo;
    }

    [HttpGet]
    public Task<IAsyncResult> Get(int id)
    {
        // This establishes your transaction.
        // Default isolation level is 'serializable' which is generally desirable and is configurable.
        // Enable async flow option in case subordinate async code results in a thread continuation switch.
        // If you don't need this transaction here, don't use it, or put it where it is needed.
        using (var trn = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
        {
            Product product = _productRepo.Get(id);

            // Use additional repositories and do something that actually requires an explicit transaction.
            // A single SQL statement does not require a transaction on SQL Server due to default autocommit mode.
            // ...

            return Ok(product);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 问题是关于 UnitOfWork 模式,而不是如何使用 TransactionScope (4认同)
  • 我的观点是,除非您希望向 UnitOfWork 抽象添加其他功能,例如更改跟踪(这里的其他答案都没有这样做),而只是使用提供简单事务的 UnitOfWork 抽象,那么这可以很简单地实现无需手动处理自定义 UnitOfWork 抽象,因为框架提供的 TransactionScope 类型已经提供了这一点。 (3认同)
  • 我还想补充一点,如果您_确实_需要更改跟踪,那么除非您已经知道您将需要大量优化性能,否则您最好硬着头皮并使用实体框架或其他一些提供它的库框,否则您将最终维护、测试和调试大量(可能不必要的)定制框架代码。 (2认同)

Nat*_*per 9

好的,自从 OP 询问以来已经过去了五年,但是当我使用 Dapper 进行开发时不断遇到这个问题(或其他任何东西,这并不是非常特定于 Dapper)。这是我的两分钱。

先说说其他的回答:

pimbrouwers 的回答 IDbContext以与实体框架非常相似的方式管理工作单元。这是完全合理且易于理解的。但主要的缺点是您最终将 a 传递IDbContext给您的所有业务代码。这有点像上帝的对象。就像在 EF 中一样。我更喜欢注入单个存储库并明确我将要做什么数据库内容,而不是让我的域模型中的所有内容始终只有一个.。但是,如果您不同意我的“上帝反对”反对意见,那么 pim 的回答听起来很适合您。

Amit Joshi 的回答MyRepository工作单元作为构造函数参数。这意味着您不能再注入存储库。这可以通过注入存储库工厂来解决,但这肯定是它自己的麻烦。

顺便说一句:在其中一些答案中,“事务”和“工作单元”这个词可以互换使用。在实践中,它们具有 1:1 的关系,但它们不是一回事。“事务”是数据库实现,“工作单元”更像是一个更高层次的概念性事物。如果我们有比一个数据库更多的持久性,就会有所不同,UOW 将包含不止一个事务。因此,为避免混淆,在我们的 UOW 界面中使用“事务”可能不是一个好词。

所以这是我的方式

我将从用法开始

// Business code. I'm going to write a method, but a class with dependencies is more realistic
static async Task MyBusinessCode(IUnitOfWorkContext context, EntityRepoitory repo)
{
    var expectedEntity = new Entity {Id = null, Value = 10};

    using (var uow = context.Create())
    {
        expectedEntity.Id = await repo.CreateAsync(expectedEntity.Value);
        await uow.CommitAsync();
    }

    using (context.Create())
    {
         var entity = await repo.GetOrDefaultAsync(expectedEntity.Id.Value);
         entity.Should().NotBeNull();
         entity.Value.Should().Be(expectedEntity.Value);
    }
}
Run Code Online (Sandbox Code Playgroud)

工作单元只是包装一个事务并且是短暂的:

public class UnitOfWork : IDisposable
{

    private readonly SQLiteTransaction _transaction;
    public SQLiteConnection Connection { get; }

    public bool IsDisposed { get; private set; } = false;

    public UnitOfWork(SQLiteConnection connection)
    {
        Connection = connection;
        _transaction = Connection.BeginTransaction();
    }

    public async Task RollBackAsync()
    {
        await _transaction.RollbackAsync();
    }

    public async Task CommitAsync()
    {
        await _transaction.CommitAsync();
    }

    public void Dispose()
    {
        _transaction?.Dispose();

        IsDisposed = true;
    }
}
Run Code Online (Sandbox Code Playgroud)

上下文更有趣。这是 repos 和作品单元在幕后交流的方式。

有一个接口供业务代码管理工作单元,还有一个接口供 repo 遵守该工作单元。

public class UnitOfWorkContext : IUnitOfWorkContext, IConnectionContext
{
    private readonly SQLiteConnection _connection;
    private UnitOfWork _unitOfWork;

    private bool IsUnitOfWorkOpen => !(_unitOfWork == null || _unitOfWork.IsDisposed);

    public UnitOfWorkContext(SQLiteConnection connection)
    {
        _connection = connection;
    }

    public SQLiteConnection GetConnection()
    {
        if (!IsUnitOfWorkOpen)
        {
            throw new InvalidOperationException(
                "There is not current unit of work from which to get a connection. Call BeginTransaction first");
        }

        return _unitOfWork.Connection;
    }

    public UnitOfWork Create()
    {
        if (IsUnitOfWorkOpen)
        {
            throw new InvalidOperationException(
                "Cannot begin a transaction before the unit of work from the last one is disposed");
        }

        _unitOfWork = new UnitOfWork(_connection);
        return _unitOfWork;
    }
}

public interface IConnectionContext
{
    SQLiteConnection GetConnection();
}

public interface IUnitOfWorkContext
{
    UnitOfWork Create();
}
Run Code Online (Sandbox Code Playgroud)

下面是 repo 是如何做到的:

public class EntityRepository
{
    private readonly IConnectionContext _context;

    public EntityRepository(IConnectionContext context)
    {
        _context = context;
    }

    public async Task<int> CreateAsync(int value)
    {
        return await _context.GetConnection().QuerySingleAsync<int>(
            @"
insert into Entity (Value) values (@value);
select last_insert_rowid();
", new { value });
    }

    public async Task<Entity> GetOrDefaultAsync(int id)
    {
        return await _context.GetConnection().QuerySingleOrDefaultAsync<Entity>(
            @"
select * from Entity where Id = @id
", new { id });
    }
}
Run Code Online (Sandbox Code Playgroud)

最后是 DI。进行设置。这是一个单线程控制台应用程序示例。我想让它成为单例或每个请求是明智的。无论如何,都可以更改 UnitOfWorkContext 的实现以匹配您的线程选择(例如,通过使用带有线程静态 UOW 的 UnitOfWorkContext)。

public static void Main(string[] args)
{
    using (var connection = new SQLiteConnection("Data Source=:memory:"))
    {
        connection.Open();
        Setup(connection);
        var context = new UnitOfWorkContextContext(connection);
        var repo = new EntityRepository(context);

        MyBusinessCode(repo, context).ConfigureAwait(false).GetAwaiter().GetResult();
    }
}
Run Code Online (Sandbox Code Playgroud)

Github 上的完整版本:https : //github.com/NathanLBCooper/unit-of-work-example

分析

我们已经消除了 God 对象,并且不需要为我们所有的存储库创建工厂。代价是我们在我们的回购和工作单元之间有更多微妙的非明显联系。没有样板,但我们确实需要小心我们给上下文对象的生命周期,特别是在多线程时。

我认为这是值得的权衡,但这就是我。

聚苯乙烯

我要补充一件事。也许您已经查找了这个答案,因为您已经开始使用 dapper。现在你所有的存储库方法都是独立的原子操作,你觉得还没有必要将它们组合成事务。那么暂时你不需要做任何这些。关闭此浏览器窗口,以最简单明了的方式编写您的存储库,然后开心。

  • “我要添加一件事......” - 很好的建议。很多人在没有真正理解他们需要/正在做什么的情况下过度设计。@内森 (3认同)