# 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` ```csharp // 文件: DMS.Infrastructure/Entities/DbVariableMqttAlias.cs using SqlSugar; namespace DMS.Infrastructure.Entities; /// /// 数据库实体:对应数据库中的 VariableMqttAliases 表。 /// [SugarTable("VariableMqttAliases")] public class DbVariableMqttAlias { [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] public int Id { get; set; } /// /// 外键,指向 Variables 表的 Id。 /// public int VariableId { get; set; } /// /// 外键,指向 MqttServers 表的 Id。 /// public int MqttServerId { get; set; } /// /// 针对此特定[变量-服务器]连接的发布别名。 /// public string Alias { get; set; } } ``` ## 3. 事务管理 (`RepositoryManager`) ### 3.1. 设计思路与考量 * **模式**:`RepositoryManager` 是工作单元(Unit of Work, UoW)模式的具体实现。它作为所有仓储的统一入口,并管理数据库事务。 * **共享上下文**:所有通过 `RepositoryManager` 获取的仓储实例都共享同一个 `ISqlSugarClient` 实例,从而确保它们在同一个数据库会话和事务中操作。 * **生命周期**:`RepositoryManager` 通常被注册为 `Scoped` 或 `Transient` 生命周期,确保每个业务操作都有一个独立的工作单元。 ### 3.2. 设计优势 * **原子性**:确保跨多个仓储的操作(如创建设备、变量表和菜单)要么全部成功,要么全部失败,维护数据一致性。 * **简化事务管理**:应用层无需直接与数据库事务API交互,只需调用 `BeginTransaction()`, `CommitAsync()`, `RollbackAsync()`。 * **解耦**:应用层不直接依赖具体的仓储实现,而是依赖于 `IRepositoryManager` 这一抽象。 * **资源优化**:在单个业务操作中,所有仓储共享同一个数据库连接,减少了连接开销。 ### 3.3. 设计劣势/权衡 * **复杂性增加**:相比于直接使用仓储,引入UoW模式增加了额外的抽象层和概念。 * **仓储依赖**:`IRepositoryManager` 接口需要列出所有它管理的仓储,当新增仓储时,需要修改此接口。 * **懒加载**:为了避免在 `RepositoryManager` 构造时就创建所有仓储实例的开销,通常会使用懒加载(`Lazy`),这增加了少量复杂性。 ### 3.4. 示例:`RepositoryManager.cs` ```csharp // 文件: DMS.Infrastructure/Data/RepositoryManager.cs using DMS.Core.Interfaces; using SqlSugar; using System; namespace DMS.Infrastructure.Data; /// /// IRepositoryManager 的 SqlSugar 实现,管理所有仓储实例和数据库事务。 /// public class RepositoryManager : IRepositoryManager { private readonly ISqlSugarClient _db; private readonly Lazy _lazyDevices; private readonly Lazy _lazyVariableTables; private readonly Lazy _lazyVariables; private readonly Lazy _lazyMqttServers; private readonly Lazy _lazyVariableMqttAliases; private readonly Lazy _lazyMenus; /// /// 构造函数,通过依赖注入获取 SqlSugar 客户端实例。 /// public RepositoryManager(ISqlSugarClient db) { _db = db; // 使用 Lazy 实现仓储的懒加载,确保它们在第一次被访问时才创建。 // 所有仓储都共享同一个 _db 实例,以保证事务的一致性。 _lazyDevices = new Lazy(() => new DeviceRepository(_db)); _lazyVariableTables = new Lazy(() => new VariableTableRepository(_db)); _lazyVariables = new Lazy(() => new VariableRepository(_db)); _lazyMqttServers = new Lazy(() => new MqttServerRepository(_db)); _lazyVariableMqttAliases = new Lazy(() => new VariableMqttAliasRepository(_db)); _lazyMenus = new Lazy(() => 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; /// /// 开始一个新的数据库事务。 /// public void BeginTransaction() { _db.BeginTran(); } /// /// 异步提交当前事务中的所有变更。 /// public async Task CommitAsync() { await _db.CommitTranAsync(); } /// /// 异步回滚当前事务中的所有变更。 /// public async Task RollbackAsync() { await _db.RollbackTranAsync(); } /// /// 释放 SqlSugar 客户端资源。通常由 DI 容器管理。 /// public void Dispose() { _db.Dispose(); } } ``` ## 4. 仓储实现 ### 4.1. 设计思路与考量 * **职责**:仓储(Repository)是数据访问层的一部分,负责实现 `DMS.Core` 中定义的仓储接口。它们使用ORM框架(如SqlSugar)与数据库进行实际交互,将数据库实体转换为领域模型,反之亦然。 * **通用性**:可以有一个 `BaseRepository` 来实现通用的CRUD操作,减少重复代码。 * **特定查询**:为每个仓储接口实现其特有的查询方法(如 `GetActiveDevicesWithDetailsAsync`)。 ### 4.2. 设计优势 * **实现细节封装**:将ORM框架和数据库查询的细节封装在仓储内部,上层无需关心。 * **可替换性**:如果需要更换ORM框架或数据库类型,只需修改仓储实现,而无需修改应用层或核心层。 * **性能优化点**:可以在仓储层进行查询优化(如使用 `Include` 或 `Join` 预加载关联数据)。 ### 4.3. 设计劣势/权衡 * **代码量**:即使有 `BaseRepository`,每个仓储仍然需要一些样板代码。 * **复杂查询**:对于非常复杂的、跨多个聚合根的查询,有时仓储模式会显得笨拙,可能需要引入查询对象(Query Object)模式。 ### 4.4. 示例:`BaseRepository.cs` ```csharp // 文件: 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; /// /// 仓储基类,实现了 IBaseRepository 接口的通用 CRUD 操作。 /// /// 领域模型类型。 /// 数据库实体类型。 public abstract class BaseRepository : IBaseRepository 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(entity); await _db.Insertable(dbEntity).ExecuteCommandAsync(); // 映射回ID,如果实体是自增ID _mapper.Map(dbEntity, entity); } public virtual async Task DeleteAsync(int id) { await _db.Deleteable(id).ExecuteCommandAsync(); } public virtual async Task> GetAllAsync() { var dbEntities = await _db.Queryable().ToListAsync(); return _mapper.Map>(dbEntities); } public virtual async Task GetByIdAsync(int id) { var dbEntity = await _db.Queryable().InSingleAsync(id); return _mapper.Map(dbEntity); } public virtual async Task UpdateAsync(TDomain entity) { var dbEntity = _mapper.Map(entity); await _db.Updateable(dbEntity).ExecuteCommandAsync(); } } ``` ### 4.5. 示例:`DeviceRepository.cs` ```csharp // 文件: 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; /// /// 设备仓储的具体实现。 /// public class DeviceRepository : BaseRepository, IDeviceRepository { public DeviceRepository(ISqlSugarClient db, IMapper mapper) : base(db, mapper) { } /// /// 异步获取所有激活的S7设备,并级联加载其下的变量表和变量。 /// public async Task> GetActiveDevicesWithDetailsAsync(ProtocolType protocol) { var dbDevices = await _db.Queryable() .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>(dbDevices); } /// /// 异步根据设备ID获取设备及其所有详细信息(变量表、变量、MQTT别名等)。 /// public async Task GetDeviceWithDetailsAsync(int deviceId) { var dbDevice = await _db.Queryable() .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() .Where(a => a.VariableId == variable.Id) .Mapper(a => a.MqttServer, a => a.MqttServerId) .ToListAsync(); variable.MqttAliases = _mapper.Map>(dbAliases); } } return _mapper.Map(dbDevice); } } ```