Files
DMS/软件设计文档/04-基础设施层-仓储与事务.md
2025-07-20 23:28:45 +08:00

13 KiB
Raw Blame History

04. 基础设施层 - 仓储与事务

本文档详细设计了 DMS.Infrastructure 层中负责数据持久化和事务管理的部分。

1. 目录结构

DMS.Infrastructure/
├── Data/
│   └── RepositoryManager.cs
├── Entities/
│   ├── DbDevice.cs
│   ├── DbVariable.cs
│   ├── DbVariableMqttAlias.cs
│   └── ... (所有数据库表对应的实体)
├── Repositories/
│   ├── BaseRepository.cs
│   ├── DeviceRepository.cs
│   └── ... (所有仓储接口的实现)
└── ...

2. 数据库实体

2.1. 设计思路与考量

  • 职责数据库实体Entities是与数据库表结构直接对应的C#类。它们主要用于ORM对象关系映射框架如SqlSugar进行数据映射。
  • 与领域模型的区别:数据库实体可能包含一些与持久化相关的特性(如 [SugarColumn]),或者为了数据库优化而进行的反范式设计。它们与 DMS.Core 中的领域模型通过AutoMapper进行映射转换。

2.2. 设计优势

  • ORM友好:直接映射数据库表结构,简化了数据持久化操作。
  • 隔离持久化细节:将数据库特有的细节(如列名、主键、外键定义)封装在这一层,不暴露给上层。
  • 性能优化潜力:可以根据数据库的特点进行优化(如索引、视图),而不会影响领域模型。

2.3. 设计劣势/权衡

  • 映射开销:需要在数据库实体和领域模型之间进行映射,增加了代码量和运行时开销。
  • 可能与领域模型重复对于简单的CRUD操作数据库实体和领域模型可能看起来非常相似容易混淆。

2.4. 示例:DbVariableMqttAlias.cs

// 文件: DMS.Infrastructure/Entities/DbVariableMqttAlias.cs
using SqlSugar;

namespace DMS.Infrastructure.Entities;

/// <summary>
/// 数据库实体:对应数据库中的 VariableMqttAliases 表。
/// </summary>
[SugarTable("VariableMqttAliases")]
public class DbVariableMqttAlias
{
    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
    public int Id { get; set; }

    /// <summary>
    /// 外键,指向 Variables 表的 Id。
    /// </summary>
    public int VariableId { get; set; }

    /// <summary>
    /// 外键,指向 MqttServers 表的 Id。
    /// </summary>
    public int MqttServerId { get; set; }

    /// <summary>
    /// 针对此特定[变量-服务器]连接的发布别名。
    /// </summary>
    public string Alias { get; set; }
}

3. 事务管理 (RepositoryManager)

3.1. 设计思路与考量

  • 模式RepositoryManager 是工作单元Unit of Work, UoW模式的具体实现。它作为所有仓储的统一入口并管理数据库事务。
  • 共享上下文:所有通过 RepositoryManager 获取的仓储实例都共享同一个 ISqlSugarClient 实例,从而确保它们在同一个数据库会话和事务中操作。
  • 生命周期RepositoryManager 通常被注册为 ScopedTransient 生命周期,确保每个业务操作都有一个独立的工作单元。

3.2. 设计优势

  • 原子性:确保跨多个仓储的操作(如创建设备、变量表和菜单)要么全部成功,要么全部失败,维护数据一致性。
  • 简化事务管理应用层无需直接与数据库事务API交互只需调用 BeginTransaction(), CommitAsync(), RollbackAsync()
  • 解耦:应用层不直接依赖具体的仓储实现,而是依赖于 IRepositoryManager 这一抽象。
  • 资源优化:在单个业务操作中,所有仓储共享同一个数据库连接,减少了连接开销。

3.3. 设计劣势/权衡

  • 复杂性增加相比于直接使用仓储引入UoW模式增加了额外的抽象层和概念。
  • 仓储依赖IRepositoryManager 接口需要列出所有它管理的仓储,当新增仓储时,需要修改此接口。
  • 懒加载:为了避免在 RepositoryManager 构造时就创建所有仓储实例的开销,通常会使用懒加载(Lazy<T>),这增加了少量复杂性。

3.4. 示例:RepositoryManager.cs

// 文件: DMS.Infrastructure/Data/RepositoryManager.cs
using DMS.Core.Interfaces;
using SqlSugar;
using System;

namespace DMS.Infrastructure.Data;

/// <summary>
/// IRepositoryManager 的 SqlSugar 实现,管理所有仓储实例和数据库事务。
/// </summary>
public class RepositoryManager : IRepositoryManager
{
    private readonly ISqlSugarClient _db;
    private readonly Lazy<IDeviceRepository> _lazyDevices;
    private readonly Lazy<IVariableTableRepository> _lazyVariableTables;
    private readonly Lazy<IVariableRepository> _lazyVariables;
    private readonly Lazy<IMqttServerRepository> _lazyMqttServers;
    private readonly Lazy<IVariableMqttAliasRepository> _lazyVariableMqttAliases;
    private readonly Lazy<IMenuRepository> _lazyMenus;

    /// <summary>
    /// 构造函数,通过依赖注入获取 SqlSugar 客户端实例。
    /// </summary>
    public RepositoryManager(ISqlSugarClient db)
    {
        _db = db;
        // 使用 Lazy<T> 实现仓储的懒加载,确保它们在第一次被访问时才创建。
        // 所有仓储都共享同一个 _db 实例,以保证事务的一致性。
        _lazyDevices = new Lazy<IDeviceRepository>(() => new DeviceRepository(_db));
        _lazyVariableTables = new Lazy<IVariableTableRepository>(() => new VariableTableRepository(_db));
        _lazyVariables = new Lazy<IVariableRepository>(() => new VariableRepository(_db));
        _lazyMqttServers = new Lazy<IMqttServerRepository>(() => new MqttServerRepository(_db));
        _lazyVariableMqttAliases = new Lazy<IVariableMqttAliasRepository>(() => new VariableMqttAliasRepository(_db));
        _lazyMenus = new Lazy<IMenuRepository>(() => new MenuRepository(_db));
    }

    public IDeviceRepository Devices => _lazyDevices.Value;
    public IVariableTableRepository VariableTables => _lazyVariableTables.Value;
    public IVariableRepository Variables => _lazyVariables.Value;
    public IMqttServerRepository MqttServers => _lazyMqttServers.Value;
    public IVariableMqttAliasRepository VariableMqttAliases => _lazyVariableMqttAliases.Value;
    public IMenuRepository Menus => _lazyMenus.Value;

    /// <summary>
    /// 开始一个新的数据库事务。
    /// </summary>
    public void BeginTransaction()
    {
        _db.BeginTran();
    }

    /// <summary>
    /// 异步提交当前事务中的所有变更。
    /// </summary>
    public async Task CommitAsync()
    {
        await _db.CommitTranAsync();
    }

    /// <summary>
    /// 异步回滚当前事务中的所有变更。
    /// </summary>
    public async Task RollbackAsync()
    {
        await _db.RollbackTranAsync();
    }

    /// <summary>
    /// 释放 SqlSugar 客户端资源。通常由 DI 容器管理。
    /// </summary>
    public void Dispose()
    {
        _db.Dispose();
    }
}

4. 仓储实现

4.1. 设计思路与考量

  • 职责仓储Repository是数据访问层的一部分负责实现 DMS.Core 中定义的仓储接口。它们使用ORM框架如SqlSugar与数据库进行实际交互将数据库实体转换为领域模型反之亦然。
  • 通用性:可以有一个 BaseRepository 来实现通用的CRUD操作减少重复代码。
  • 特定查询:为每个仓储接口实现其特有的查询方法(如 GetActiveDevicesWithDetailsAsync)。

4.2. 设计优势

  • 实现细节封装将ORM框架和数据库查询的细节封装在仓储内部上层无需关心。
  • 可替换性如果需要更换ORM框架或数据库类型只需修改仓储实现而无需修改应用层或核心层。
  • 性能优化点:可以在仓储层进行查询优化(如使用 IncludeJoin 预加载关联数据)。

4.3. 设计劣势/权衡

  • 代码量:即使有 BaseRepository,每个仓储仍然需要一些样板代码。
  • 复杂查询对于非常复杂的、跨多个聚合根的查询有时仓储模式会显得笨拙可能需要引入查询对象Query Object模式。

4.4. 示例:BaseRepository.cs

// 文件: DMS.Infrastructure/Repositories/BaseRepository.cs
using AutoMapper;
using DMS.Core.Interfaces;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DMS.Infrastructure.Repositories;

/// <summary>
/// 仓储基类,实现了 IBaseRepository 接口的通用 CRUD 操作。
/// </summary>
/// <typeparam name="TDomain">领域模型类型。</typeparam>
/// <typeparam name="TDbEntity">数据库实体类型。</typeparam>
public abstract class BaseRepository<TDomain, TDbEntity> : IBaseRepository<TDomain>
    where TDomain : class
    where TDbEntity : class, new()
{
    protected readonly ISqlSugarClient _db;
    protected readonly IMapper _mapper;

    public BaseRepository(ISqlSugarClient db, IMapper mapper)
    {
        _db = db;
        _mapper = mapper;
    }

    public virtual async Task AddAsync(TDomain entity)
    {
        var dbEntity = _mapper.Map<TDbEntity>(entity);
        await _db.Insertable(dbEntity).ExecuteCommandAsync();
        // 映射回ID如果实体是自增ID
        _mapper.Map(dbEntity, entity);
    }

    public virtual async Task DeleteAsync(int id)
    {
        await _db.Deleteable<TDbEntity>(id).ExecuteCommandAsync();
    }

    public virtual async Task<List<TDomain>> GetAllAsync()
    {
        var dbEntities = await _db.Queryable<TDbEntity>().ToListAsync();
        return _mapper.Map<List<TDomain>>(dbEntities);
    }

    public virtual async Task<TDomain> GetByIdAsync(int id)
    {
        var dbEntity = await _db.Queryable<TDbEntity>().InSingleAsync(id);
        return _mapper.Map<TDomain>(dbEntity);
    }

    public virtual async Task UpdateAsync(TDomain entity)
    {
        var dbEntity = _mapper.Map<TDbEntity>(entity);
        await _db.Updateable(dbEntity).ExecuteCommandAsync();
    }
}

4.5. 示例:DeviceRepository.cs

// 文件: DMS.Infrastructure/Repositories/DeviceRepository.cs
using AutoMapper;
using DMS.Core.Interfaces;
using DMS.Core.Models;
using DMS.Infrastructure.Entities;
using SqlSugar;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DMS.Infrastructure.Repositories;

/// <summary>
/// 设备仓储的具体实现。
/// </summary>
public class DeviceRepository : BaseRepository<Device, DbDevice>, IDeviceRepository
{
    public DeviceRepository(ISqlSugarClient db, IMapper mapper) : base(db, mapper) { }

    /// <summary>
    /// 异步获取所有激活的S7设备并级联加载其下的变量表和变量。
    /// </summary>
    public async Task<List<Device>> GetActiveDevicesWithDetailsAsync(ProtocolType protocol)
    {
        var dbDevices = await _db.Queryable<DbDevice>()
                                 .Where(d => d.IsActive && d.Protocol == (int)protocol)
                                 .Mapper(d => d.VariableTables, d => d.Id, d => d.VariableTables.Select(vt => vt.DeviceId).ToList())
                                 .Mapper(d => d.VariableTables.Select(vt => vt.Variables), vt => vt.Id, vt => vt.Variables.Select(v => v.VariableTableId).ToList())
                                 .ToListAsync();
        return _mapper.Map<List<Device>>(dbDevices);
    }

    /// <summary>
    /// 异步根据设备ID获取设备及其所有详细信息变量表、变量、MQTT别名等
    /// </summary>
    public async Task<Device> GetDeviceWithDetailsAsync(int deviceId)
    {
        var dbDevice = await _db.Queryable<DbDevice>()
                                .Where(d => d.Id == deviceId)
                                .Mapper(d => d.VariableTables, d => d.Id, d => d.VariableTables.Select(vt => vt.DeviceId).ToList())
                                .Mapper(d => d.VariableTables.Select(vt => vt.Variables), vt => vt.Id, vt => vt.Variables.Select(v => v.VariableTableId).ToList())
                                .FirstAsync();

        if (dbDevice == null) return null;

        // 手动加载 VariableMqttAlias因为 SqlSugar 的 Mapper 可能无法直接处理多层嵌套的关联实体
        foreach (var variableTable in dbDevice.VariableTables)
        {
            foreach (var variable in variableTable.Variables)
            {
                var dbAliases = await _db.Queryable<DbVariableMqttAlias>()
                                         .Where(a => a.VariableId == variable.Id)
                                         .Mapper(a => a.MqttServer, a => a.MqttServerId)
                                         .ToListAsync();
                variable.MqttAliases = _mapper.Map<List<VariableMqttAlias>>(dbAliases);
            }
        }

        return _mapper.Map<Device>(dbDevice);
    }
}