202 lines
6.5 KiB
Markdown
202 lines
6.5 KiB
Markdown
|
|
# 软件开发文档 - 05. 事务与工作单元设计
|
|||
|
|
|
|||
|
|
本文档详细阐述了为确保数据一致性而引入的**工作单元模式 (Unit of Work)** 的设计与实现。我们将此模式的接口命名为 `IRepositoryManager`,以更直观地反映其职责。
|
|||
|
|
|
|||
|
|
## 1. 设计目标
|
|||
|
|
|
|||
|
|
在复杂的业务操作中(例如,创建一个设备,同时需要创建其关联的变量表和菜单项),必须保证所有数据库操作要么全部成功,要么全部失败。这要求一个能够跨越多个仓储(Repository)的事务管理机制。
|
|||
|
|
|
|||
|
|
`IRepositoryManager` 的核心职责就是:
|
|||
|
|
|
|||
|
|
* **组合操作**:将一系列独立的数据库操作组合成一个单一的、原子的工作单元。
|
|||
|
|
* **事务控制**:提供统一的 `Commit` (提交) 和 `Rollback` (回滚) 方法。
|
|||
|
|
* **共享上下文**:确保所有通过它访问的仓储共享同一个数据库连接和事务,这是实现原子性的前提。
|
|||
|
|
|
|||
|
|
## 2. `DMS.Core` - 接口定义
|
|||
|
|
|
|||
|
|
我们在核心层定义 `IRepositoryManager` 接口,它作为所有仓储的容器和事务的控制器。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.Core/Interfaces/IRepositoryManager.cs
|
|||
|
|
namespace DMS.Core.Interfaces;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 定义了一个仓储管理器,它使用工作单元模式来组合多个仓储操作,以确保事务的原子性。
|
|||
|
|
/// 实现了IDisposable,以确保数据库连接等资源能被正确释放。
|
|||
|
|
/// </summary>
|
|||
|
|
public interface IRepositoryManager : IDisposable
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取设备仓储的实例。
|
|||
|
|
/// 所有通过此管理器获取的仓储都共享同一个数据库上下文和事务。
|
|||
|
|
/// </summary>
|
|||
|
|
IDeviceRepository Devices { get; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取变量表仓储的实例。
|
|||
|
|
/// </summary>
|
|||
|
|
IVariableTableRepository VariableTables { get; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取菜单仓储的实例。
|
|||
|
|
/// </summary>
|
|||
|
|
IMenuRepository Menus { get; }
|
|||
|
|
|
|||
|
|
// ... 此处可以添加项目中所有其他的仓储
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 开始一个新的数据库事务。
|
|||
|
|
/// </summary>
|
|||
|
|
void BeginTransaction();
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 异步提交当前事务中的所有变更。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>一个表示异步操作的任务。</returns>
|
|||
|
|
Task CommitAsync();
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 异步回滚当前事务中的所有变更。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>一个表示异步操作的任务。</returns>
|
|||
|
|
Task RollbackAsync();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. `DMS.Infrastructure` - 具体实现
|
|||
|
|
|
|||
|
|
在基础设施层,我们提供 `IRepositoryManager` 的 `SqlSugar` 实现。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.Infrastructure/Data/RepositoryManager.cs
|
|||
|
|
using DMS.Core.Interfaces;
|
|||
|
|
using SqlSugar;
|
|||
|
|
|
|||
|
|
namespace DMS.Infrastructure.Data;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 使用 SqlSugar 实现的仓储管理器。
|
|||
|
|
/// </summary>
|
|||
|
|
public class RepositoryManager : IRepositoryManager
|
|||
|
|
{
|
|||
|
|
private readonly ISqlSugarClient _db;
|
|||
|
|
private readonly Lazy<IDeviceRepository> _lazyDevices;
|
|||
|
|
private readonly Lazy<IVariableTableRepository> _lazyVariableTables;
|
|||
|
|
private readonly Lazy<IMenuRepository> _lazyMenus;
|
|||
|
|
|
|||
|
|
public RepositoryManager(ISqlSugarClient db)
|
|||
|
|
{
|
|||
|
|
_db = db;
|
|||
|
|
// 使用 Lazy<T> 实现懒加载,只有在第一次访问时才创建仓储实例
|
|||
|
|
// 关键点:将当前的数据库客户端 (_db) 传递给仓储,确保它们共享事务
|
|||
|
|
_lazyDevices = new Lazy<IDeviceRepository>(() => new DeviceRepository(_db));
|
|||
|
|
_lazyVariableTables = new Lazy<IVariableTableRepository>(() => new VariableTableRepository(_db));
|
|||
|
|
_lazyMenus = new Lazy<IMenuRepository>(() => new MenuRepository(_db));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IDeviceRepository Devices => _lazyDevices.Value;
|
|||
|
|
public IVariableTableRepository VariableTables => _lazyVariableTables.Value;
|
|||
|
|
public IMenuRepository Menus => _lazyMenus.Value;
|
|||
|
|
|
|||
|
|
public void BeginTransaction()
|
|||
|
|
{
|
|||
|
|
_db.BeginTran();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task CommitAsync()
|
|||
|
|
{
|
|||
|
|
await _db.CommitTranAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task RollbackAsync()
|
|||
|
|
{
|
|||
|
|
await _db.RollbackTranAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
// 数据库客户端的生命周期由DI容器管理,此处无需手动释放
|
|||
|
|
// 但如果事务未提交/回滚,SqlSugar的Dispose会处理它
|
|||
|
|
_db.Dispose();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. `DMS.Application` - 应用服务重构
|
|||
|
|
|
|||
|
|
应用服务现在注入 `IRepositoryManager` 来执行事务性操作。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.Application/Services/DeviceAppService.cs
|
|||
|
|
using DMS.Core.Interfaces;
|
|||
|
|
|
|||
|
|
public class DeviceAppService : IDeviceAppService
|
|||
|
|
{
|
|||
|
|
private readonly IRepositoryManager _repoManager;
|
|||
|
|
private readonly IMapper _mapper;
|
|||
|
|
|
|||
|
|
public DeviceAppService(IRepositoryManager repoManager, IMapper mapper)
|
|||
|
|
{
|
|||
|
|
_repoManager = repoManager;
|
|||
|
|
_mapper = mapper;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task<int> CreateDeviceWithDetailsAsync(CreateDeviceWithDetailsDto dto)
|
|||
|
|
{
|
|||
|
|
// 不再需要 using 语句,因为 DI 容器会管理 RepositoryManager 的生命周期
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
_repoManager.BeginTransaction();
|
|||
|
|
|
|||
|
|
var device = _mapper.Map<Device>(dto.Device);
|
|||
|
|
await _repoManager.Devices.AddAsync(device);
|
|||
|
|
|
|||
|
|
var variableTable = _mapper.Map<VariableTable>(dto.VariableTable);
|
|||
|
|
variableTable.DeviceId = device.Id;
|
|||
|
|
await _repoManager.VariableTables.AddAsync(variableTable);
|
|||
|
|
|
|||
|
|
var menu = _mapper.Map<Menu>(dto.Menu);
|
|||
|
|
menu.TargetId = device.Id;
|
|||
|
|
await _repoManager.Menus.AddAsync(menu);
|
|||
|
|
|
|||
|
|
await _repoManager.CommitAsync();
|
|||
|
|
|
|||
|
|
return device.Id;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
await _repoManager.RollbackAsync();
|
|||
|
|
// 可以在此记录日志
|
|||
|
|
throw new ApplicationException("创建设备时发生错误,操作已回滚。", ex);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 依赖注入配置 (`App.xaml.cs`)
|
|||
|
|
|
|||
|
|
在WPF项目中注册 `IRepositoryManager`。
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: DMS.WPF/App.xaml.cs
|
|||
|
|
private void ConfigureServices(IServiceCollection services)
|
|||
|
|
{
|
|||
|
|
// ...
|
|||
|
|
|
|||
|
|
// 注册SqlSugar客户端 (Singleton)
|
|||
|
|
services.AddSingleton<ISqlSugarClient>(provider => {
|
|||
|
|
// ...数据库连接配置...
|
|||
|
|
return new SqlSugarClient(new ConnectionConfig() { ... });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 注册仓储管理器 (Scoped 或 Transient)
|
|||
|
|
// 对于WPF,每个业务流程使用一个新的实例是安全的,因此用Transient
|
|||
|
|
services.AddTransient<IRepositoryManager, RepositoryManager>();
|
|||
|
|
|
|||
|
|
// 注册应用服务
|
|||
|
|
services.AddTransient<IDeviceAppService, DeviceAppService>();
|
|||
|
|
|
|||
|
|
// 注意:不再需要单独注册每个仓储
|
|||
|
|
}
|
|||
|
|
```
|