如何正确实现跨多个存储库的事务?
假设我们有 2 个存储库(临时)
internal sealed class UserRepository : IUserRepository
{
public UserRepository(IDbConnection connection) {}
public Task<int> Create(int addressId, string name) {}
}
internal sealed class AddressRepository : IAddressRepository
{
public AddressRepository(IDbConnection connection, IDbTransaction transaction, ILogger<AddressRepository> logger){}
public Task<int> Create(string country){}
}
我想在单笔交易中同时调用
IUserRepository.Create
和 IAddressRepository.Create
。
到目前为止我尝试过的:
internal sealed class UserRepository : IUserRepository
{
public void OpenConnection(){}
public void CloseConnection(){}
public IDbTransaction BeginTransaction(){}
public async Task<int> Create(int addressId, string name)
{
OpenConnection();
using var transaction = BeginTransaction();
var result = await Create(addressId, name, transaction);
transaction.Commit();
CloseConnection();
return result;
}
public Task<int> Create(int addressId, string name, IDbTransaction transaction)
{
ArgumentNullException.ThrowIfNull(transaction.Connection);
return transaction.Connection.ExecuteScalarAsync<int>(command, params, transaction);
}
}
然而,这对我来说看起来并不干净,每个方法都需要事务重载。
存储库作为静态类 ...坏主意,不可测试,无法注入其他依赖项,例如
ILogger<T>
。
存储库和交易工厂
这需要对存储库进行一些小调整
internal sealed class UserRepository : IUserRepository
{
public UserRepository(IDbConnection connection, IDbTransaction transaction){}
public Task<int> Create(int addressId, string name){}
}
以及管理事务和存储库创建的附加类
internal sealed class RepositoryFactory
{
public RepositoryFactory(Func<IDbConnection, IDbTransaction, IAddressRepository> addressFactory,
Func<IDbConnection, IDbTransaction, IUserRepository> userFactory){}
public IAddressRepository CreateAddressRepository(IDbConnection connection, IDbTransaction transaction){}
public IUserRepository CreateUserRepository(IDbConnection connection, IDbTransaction transaction){}
}
internal sealed class Transaction : ITransaction
{
private readonly IDbConnection _connection;
private readonly RepositoryFactory _repositoryFactory;
private readonly IDbTransaction _transaction;
public Transaction(IDbConnection connection, RepositoryFactory repositoryFactory){}
public IAddressRepository CreateAddressRepository()
{
return _repositoryFactory.CreateAddressRepository(_connection, _transaction);
}
public IUserRepository CreateUserRepository()
{
return _repositoryFactory.CreateUserRepository(_connection, _transaction);
}
public void Commit(){}
public void Rollback(){}
public void Dispose(){}
}
internal sealed class TransactionFactory : ITransactionFactory
{
private readonly Func<ITransaction> _transaction;
public TransactionFactory(Func<ITransaction> transaction){}
public ITransaction Create()
{
return _transaction();
}
}
使用示例
internal sealed class HighLevelService : IHighLevelService
{
private readonly ITransactionFactory _transactionFactory;
public HighLevelService(ITransactionFactory transactionFactory)
{
_transactionFactory = transactionFactory;
}
public async Task CreateUser(string country, string name)
{
using var transaction = _transactionFactory.Create();
var ar = transaction.CreateAddressRepository();
var ur = transaction.CreateUserRepository();
var addressId = await ar.Create(country);
var userId = await ur.Create(addressId, name);
transaction.Commit();
}
}
这很好......几乎。我在每次调用
ITransactionFactory.Create
时都会收到新的交易,并且在幕后的存储库具有相同的连接/交易。我不再需要担心打开和关闭连接了。
但是我认为它仍然有缺陷。
ITransaction
不应该负责创建存储库,这是 RepositoryFactory
的工作。如果我能以某种方式反转依赖性,那就太好了。
internal sealed class RepositoryFactory
{
public RepositoryFactory(Func<IDbConnection, IDbTransaction, IAddressRepository> addressFactory,
Func<IDbConnection, IDbTransaction, IUserRepository> userFactory){}
public IAddressRepository CreateAddressRepository(ITransaction transaction){}
public IUserRepository CreateUserRepository(ITransaction transaction){}
}
但这需要
ITransaction
看起来像这样。
internal interface ITransaction : IDisposable
{
IDbConnection Connection {get;}
IDbTransaction Transaction {get;}
void Commit();
void Rollback();
}
我不喜欢。
有没有办法在不暴露底层连接和事务的情况下修复它。或者也许我可以使用其他模式。
完整的依赖注入
builder.Services.AddSingleton<RepositoryFactory>(sp =>
{
return new RepositoryFactory(
(connection, transaction) =>
new AddressRepository(connection, transaction, sp.GetRequiredService<ILogger<AddressRepository>>()),
(connection, transaction) => new UserRepository(connection, transaction));
});
builder.Services.AddTransient<ITransaction, Transaction>();
builder.Services.AddSingleton<ITransactionFactory>(sp =>
{
var connection = sp.GetRequiredService<IDbConnection>();
var repositoryFactory = sp.GetRequiredService<RepositoryFactory>();
return new TransactionFactory(() => new Transaction(connection, repositoryFactory));
});
builder.Services.AddTransient<IHighLevelService, HighLevelService>();
这看起来非常复杂。为什么?
存储库需要一个开放的数据库连接才能工作。他们没有责任打开它、关闭它或在上面创建交易。
您的流程应如下所示:
如何构建它取决于您和您的程序。您可能正在使用依赖项注入来创建新存储库并注入数据库连接。您可能有一个中间件,可以打开事务并在成功时提交它并在异常时回滚它。或者它只是程序流程的一部分。其他任何事情都太复杂了,在一个连接和事务上拥有两个存储库并不是火箭科学,保持简单。
using var connection = OpenDatabase();
using var transaction = connection.BeginTransaction();
var userRepository = new UserRepository(connection);
var addressRepository = new AddressRepository(connection);
var address = addressRepository.Create(data, data, data);
var user = userRepository.Create(data, data, data, address);
transaction.Commit();
connection.Close();
使用依赖注入,代码可能如下所示:
call(IDbConnection connection, IUserRepository userRepository, IAddressRepository addressRepository)
{
using var transaction = connection.BeginTransaction();
var address = addressRepository.Create(data, data, data);
var user = userRepository.Create(data, data, data, address);
transaction.Commit();
}