Files
DMS/软件设计文档/05-事务与工作单元设计.md

6.5 KiB
Raw Blame History

软件开发文档 - 05. 事务与工作单元设计

本文档详细阐述了为确保数据一致性而引入的工作单元模式 (Unit of Work) 的设计与实现。我们将此模式的接口命名为 IRepositoryManager,以更直观地反映其职责。

1. 设计目标

在复杂的业务操作中例如创建一个设备同时需要创建其关联的变量表和菜单项必须保证所有数据库操作要么全部成功要么全部失败。这要求一个能够跨越多个仓储Repository的事务管理机制。

IRepositoryManager 的核心职责就是:

  • 组合操作:将一系列独立的数据库操作组合成一个单一的、原子的工作单元。
  • 事务控制:提供统一的 Commit (提交) 和 Rollback (回滚) 方法。
  • 共享上下文:确保所有通过它访问的仓储共享同一个数据库连接和事务,这是实现原子性的前提。

2. DMS.Core - 接口定义

我们在核心层定义 IRepositoryManager 接口,它作为所有仓储的容器和事务的控制器。

// 文件: 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 - 具体实现

在基础设施层,我们提供 IRepositoryManagerSqlSugar 实现。

// 文件: 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 来执行事务性操作。

// 文件: 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

// 文件: 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>();

    // 注意:不再需要单独注册每个仓储
}