322 lines
13 KiB
Markdown
322 lines
13 KiB
Markdown
|
|
# 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;
|
|||
|
|
|
|||
|
|
/// <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` 通常被注册为 `Scoped` 或 `Transient` 生命周期,确保每个业务操作都有一个独立的工作单元。
|
|||
|
|
|
|||
|
|
### 3.2. 设计优势
|
|||
|
|
|
|||
|
|
* **原子性**:确保跨多个仓储的操作(如创建设备、变量表和菜单)要么全部成功,要么全部失败,维护数据一致性。
|
|||
|
|
* **简化事务管理**:应用层无需直接与数据库事务API交互,只需调用 `BeginTransaction()`, `CommitAsync()`, `RollbackAsync()`。
|
|||
|
|
* **解耦**:应用层不直接依赖具体的仓储实现,而是依赖于 `IRepositoryManager` 这一抽象。
|
|||
|
|
* **资源优化**:在单个业务操作中,所有仓储共享同一个数据库连接,减少了连接开销。
|
|||
|
|
|
|||
|
|
### 3.3. 设计劣势/权衡
|
|||
|
|
|
|||
|
|
* **复杂性增加**:相比于直接使用仓储,引入UoW模式增加了额外的抽象层和概念。
|
|||
|
|
* **仓储依赖**:`IRepositoryManager` 接口需要列出所有它管理的仓储,当新增仓储时,需要修改此接口。
|
|||
|
|
* **懒加载**:为了避免在 `RepositoryManager` 构造时就创建所有仓储实例的开销,通常会使用懒加载(`Lazy<T>`),这增加了少量复杂性。
|
|||
|
|
|
|||
|
|
### 3.4. 示例:`RepositoryManager.cs`
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 文件: 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框架或数据库类型,只需修改仓储实现,而无需修改应用层或核心层。
|
|||
|
|
* **性能优化点**:可以在仓储层进行查询优化(如使用 `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;
|
|||
|
|
|
|||
|
|
/// <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`
|
|||
|
|
|
|||
|
|
```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;
|
|||
|
|
|
|||
|
|
/// <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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|