目前,我正在尝试将Dapper ORM与工作单元+存储库模式一起使用。
我想使用Unit of Work而不是简单的dapper Repository,因为我的插入和更新需要一定程度的事务处理。我一直无法找到任何有用的例子,因为大多数似乎都使用实体框架并且在工作单元内存在泄漏问题。
有人可以指出我正确的方向吗?
答案 0 :(得分:25)
这Git project非常有帮助。我从同一个开始,根据我的需要做了一些改变。
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是more而不是DBTransaction。
可以在here找到有关上述代码中存储库的更多详细信息。
我已发布此代码here。但是对于这个代码,这个问题对我来说更相关;所以我再次发帖,而不只是链接到原始答案。
答案 1 :(得分:11)
编辑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);
}
请注意
IDbContext
或IUnitOfWorkFactory
如何实现IDisposable。这样做是为了避免leaky abstraction。相反,依靠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;
}
}
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
。为简单起见,此处省略。
答案 2 :(得分:5)
好的,距离OP提出已经过去了五年,但是当我与Dapper一起开发时(或者实际上,这并不是Dapper特有的),我不断遇到这个问题。这是我的两分钱。
首先让我们谈谈其他答案:
pimbrouwers' answer IDbContext
以与实体框架相似的方式管理工作单元。这是完全明智且易于理解的。但是主要的缺点是最终将IDbContext
传递给所有业务代码。这有点像上帝的对象。就像在EF中一样。我更喜欢注入单个存储库,并明确说明我将要执行的数据库工作,而不是将域模型中的所有内容始终仅相距.
。但是,如果您不同意我的“上帝对象”反对意见,那么pim的答案听起来像是最适合您的选择。
Amit Joshi's answer具有MyRepository
,将工作单位作为构造函数参数。这意味着您不能再注入存储库。可以通过注入存储库工厂来解决此问题,但这当然是其自身的麻烦级别。
一个简短的说法:在某些答案中,“交易”和“工作单元”一词可以互换使用。实际上,它们之间是1:1的关系,但是它们不是同一回事。 “事务”是db的实现,“工作单元”更多地是更高层次的概念性事物。如果我们拥有更多的持久性,那就是只有一个数据库,那将会有所不同,并且UOW将包含多个事务。因此,为避免混淆,在我们的UOW界面中使用“ Transaction”可能不是一个好词。
这就是我的方式:
我将从用法开始
// 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);
}
}
工作单元只是包装了一个事务,并且寿命很短:
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;
}
}
上下文更有趣。这是回购和工作单元在幕后进行交流的方式。
对于业务代码,有一个界面可以管理一个工作单元,而对仓库来说,有一个界面可以遵守该工作单元。
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();
}
以下是仓库的工作方式:
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 });
}
}
最后是DI。进行安装。这是一个单线程控制台应用程序示例。我认为将其设置为单例或按请求是明智的。无论如何,都可以更改UnitOfWorkContext的实现以匹配您的线程选择(例如,通过将UnitOfWorkContext与线程静态UOW一起使用)。
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();
}
}
Github上的完整版本:https://github.com/NathanLBCooper/unit-of-work-example
分析:
我们已经消除了神物,不需要为我们所有的存储库创建工厂。代价是,我们的存储库和工作单元之间有一些微妙的,非显而易见的联系。没有样板,但是我们需要注意赋予上下文对象的生命周期,特别是在多线程时。
我认为这是值得的权衡,但这就是我。
PS
我要添加一件事。也许您已经查找了此答案,因为您已经开始使用dapper。现在,您所有的存储库方法都是单独的原子操作,您觉得还不需要将它们组合到事务中。然后暂时不需要执行任何操作。关闭此浏览器窗口,以最简单和明显的方式编写存储库,并感到高兴。
答案 3 :(得分:3)
为此不需要手动解决方案。使用框架中已经存在的类,可以非常轻松地实现所需的目标。
fun bind(post: Board) {
boardImage = itemView.findViewById(R.id.boardImage)
rootView = itemView.findViewById(R.id.rootView)
requestManager.load(post.imageUrl).into(boardImage)}
答案 4 :(得分:0)
我在您的github存储库中注意到您删除了UnitOfWorkFactory,而是在访问Connection时实例化了它
这种方法的问题是我无法解决问题。
想象一下以下情况,如果我将DBContext注册为作用域,而将存储库注册为瞬态
1. UserService CreateUserProfile
a. UserRepositoryGetByEmail("some@email.com")
b. UserRepository.Add(user)
c. AddressRepository.Add(new address)
2. UserService Commit?
在这种情况下,上面的所有(1.)都是一个事务,然后我要提交(2。)
在一个具有多个服务的大型业务层中,使用同一个作用域的dbcontext实例,我可以看到事务重叠
现在我可以将dbcontext设置为Transient,但是每次注入的UnitOfWork都会不同,并且将不起作用。