XiHan.Framework.Data
基于 SqlSugar 的数据访问基础设施:仓储模式、工作单元事务接入、多租户数据隔离、审计/软删除全局过滤、启动自动建库建表与种子数据
- NuGet:
XiHan.Framework.Data - 模块类:
XiHanDataModule - 所在层:基础设施层
- 关键依赖:SqlSugarCore(
5.1.4.217,ORM,直接对接数据库)
概述
这个包是框架的数据访问基础设施,用 SqlSugar ORM 落地领域层定义的仓储契约。它统一注册 SqlSugar 客户端、提供多套开箱即用的仓储实现(只读 / 读写 / 软删除 / 审计 / 聚合根),并把主键生成、审计字段注入、多租户数据隔离、软删除过滤、事务接入全部下沉到 AOP 与全局过滤器里。
设计上遵循"仓储只管纯持久化、横切关注点交给基础设施":租户连接选择由 ISqlSugarClientResolver 承担;租户行级过滤与软删过滤由 SqlSugar 全局 QueryFilter AOP 注入;创建/修改/删除审计字段与雪花主键由 DataExecuting AOP 自动填充;事务边界由 XiHan.Framework.Uow 接管。你在业务里只需注入仓储接口读写实体,其它都在幕后完成。
仓储接口契约定义在 XiHan.Framework.Domain(IRepositoryBase<TEntity,TKey> 等),本包提供 SqlSugar 实现并以开放泛型 TryAddScoped 注册——业务层可以注册自己的实现覆盖默认行为。
何时使用
- 需要用仓储模式访问数据库,而不想手写 SqlSugar 客户端注册、连接管理与事务
- 需要多租户数据隔离:行级租户过滤(
TenantId=0视为全局模板)、跨库/跨连接路由 - 需要软删除、审计字段(创建/修改/删除时间与操作人、租户 Id、TraceId)自动注入
- 需要应用启动时自动建库、
CodeFirst建表、写入种子数据 - 需要对多个数据库(
ConfigId多连接、主从读写分离)统一管理
安装与启用
dotnet add package XiHan.Framework.Data[DependsOn(typeof(XiHanDataModule))]
public class MyModule : XiHanModule { }XiHanDataModule 依赖 XiHanDomainModule、XiHanUowModule、XiHanDistributedIdsModule、XiHanMultiTenancyModule、XiHanSecurityModule、XiHanAuditingModule。它在 ConfigureServices 里调用 AddXiHanDataSqlSugar(config) 注册全部服务;在 OnApplicationInitializationAsync 里创建作用域、取 IDbInitializer 执行 InitializeAsync() 完成数据库初始化(初始化失败会抛出中断启动)。
启用后你得到(均为 TryAdd 语义,可被业务覆盖):
SqlSugarScope(单例,线程安全)与按当前租户解析的ISqlSugarClient(Scoped)- 仓储开放泛型:
IReadOnlyRepositoryBase<,>、IRepositoryBase<,>、ISoftDeleteRepositoryBase<,>、IAuditedRepository<,>、IAggregateRootRepository<,> ISqlSugarClientResolver/ISqlSugarTenantConnectionResolver/ISqlSugarConnectionConfiguratorSqlSugarDataExecutingHandler(主键/审计/租户/TraceId 注入)IDatabaseMetadataProvider(库表元数据)IEntityAuditContextProvider(默认从当前用户/请求填充)、IEntityDiffLogWriter(默认Null零开销)——这两个契约的默认实现随依赖的 XiHan.Framework.Auditing 模块一并注册,本包直接复用IDbInitializer(建库/建表/种子)SqlSugarSlaveHealthCheckService(后台从库健康探针,始终注册;由EnableSlaveHealthCheck开关决定是否实际探测,未开启时空转零成本)
工作原理
一次仓储读写背后的横切链路:
业务调用仓储方法
└─ 仓储通过 ISqlSugarClientResolver.GetClientForEntity(实体类型) 取客户端
├─ 实体标了 [ModuleDataSource("Erp")] → 取当前布局下派生的模块库(模块分库,未配置则 fail-closed)
├─ 否则若存在租户连接提供器且处于租户上下文 → 解析租户独立连接(库隔离,fail-closed)
├─ 否则 → ISqlSugarTenantConnectionResolver 解析 ConfigId(行/字段隔离)
└─ 若当前存在事务型工作单元 → 把该连接的事务登记进 UoW(GetOrAddTransactionApi)
└─ SqlSugar 执行 SQL
├─ QueryFilter 全局过滤器(AOP):自动 AND 软删过滤 + 租户过滤
├─ DataExecuting AOP:Insert 注入雪花主键+创建审计+TraceId;Update/Delete 注入修改/删除审计
└─ (可选)OnDiffLogEvent AOP:生成 before/after 快照 → IEntityDiffLogWriter 落库关键约定:
- 连接选择每次访问实时解析:
DbClient每次访问都重新解析——实体声明了模块数据源固定落该模块库,未声明则随租户上下文切换即时生效。 - 静态连接与运行时动态连接一致:
SqlSugarConnectionConfigurator复用同一套过滤器/AOP 装配逻辑,保证运行时新注册的租户连接与启动期静态连接行为完全一致。 - 事务不在仓储内开启:仓储只做 before 预读保证实体在当前租户范围内,事务边界统一由工作单元接管(见 XiHan.Framework.Uow)。
- 写操作前的租户安全校验:
UpdateAsync/DeleteAsync/批量操作会先按当前过滤器读取实体,读不到即视为"不存在或不在当前租户范围内"并抛InvalidOperationException,防止越租户改删。 - SQL 执行自动上报链路追踪:每条 SQL 执行完成(或异常)后,
SetSugarAop按已知耗时回溯生成一个db.query(ActivityKind.Client)的 OpenTelemetry Activity,挂在当前请求 Activity 之下,携带db.system/db.statement标签,异常时记录状态与堆栈;基于XiHanActivitySources.DataSource,无监听者时直接跳过、零开销。
核心能力
- 多套仓储实现:只读、读写、软删除、审计、聚合根,全部开放泛型注册,注入接口即用。
- 多租户数据隔离:全局
QueryFilter自动过滤租户数据(当前无租户上下文时不过滤,有上下文时"本租户数据 OR 全局模板TenantId=0");连接层支持按ConfigId/前缀/名称/自定义委托解析,或由业务提供ISqlSugarTenantConnectionProvider做库级隔离。 - 模块分库:实体标
[ModuleDataSource("Erp")]即固定落在当前布局的 Erp 模块库上,租户上下文照常统一;建表初始化同步收窄到该库(详见下文)。 - 软删除过滤:实现
ISoftDelete的实体自动过滤IsDeleted;ISoftDeleteRepositoryBase<,>额外提供SoftDelete/Restore/GetDeleted/GetAllWithDeleted等。 - 审计字段自动注入:通过 SqlSugar
DataExecutingAOP 注入雪花主键、创建/修改/删除时间与操作人、TenantId、TraceId,业务与仓储都无需手填。 - 实体差异日志:可选启用(
EnableDiffLog),基于 SqlSugar 原生OnDiffLogEventAOP 生成 before/after 快照,交IEntityDiffLogWriter落库。 - 分页 / 规约 / 自动查询:内置多种分页重载(
pageIndex/pageSize、PageRequestDtoBase、规约ISpecification<TEntity>、GetPagedAutoAsync按 DTO 自动构建条件)。 - 数据库初始化:可选建库(自动处理 MySQL utf8mb4 归一化)、
CodeFirst建表(含分表SplitTables识别)、按Order顺序执行IDataSeeder;建哪些表、跑哪些种子可用特性/配置圈定或整体接管。 - 雪花 ID:接入
XiHan.Framework.DistributedIds,通过StaticConfig.CustomSnowFlakeFunc作为 SqlSugar 全局主键生成器。 - SQL 日志与慢查询:可开启 SQL/异常/慢 SQL 日志,慢 SQL 阈值可配。
- 主从读写分离:SqlSugar 原生主从能力完整放出,appsettings 声明从库即可分担读;差异化权重与更多原生定制走代码钩子;可选从库健康探针自动摘除/回填权重(详见下文)。
- SQL 执行链路追踪:每条 SQL 执行完成/异常自动上报
db.queryOpenTelemetry Client Span,挂在当前请求 Activity 下;无监听者时零开销,不需要额外配置。
主要 API / 类型
仓储契约与实现
| 契约(Domain 定义) | 实现(本包) | 说明 |
|---|---|---|
IReadOnlyRepositoryBase<TEntity,TKey> | SqlSugarReadOnlyRepository<,> | 只读:查询/统计/存在性/分页 |
IRepositoryBase<TEntity,TKey> | SqlSugarRepositoryBase<,> | 读写:在只读之上增删改 |
ISoftDeleteRepositoryBase<TEntity,TKey> | SqlSugarSoftDeleteRepository<,> | 软删除 / 恢复 / 查询已删记录 |
IAuditedRepository<TEntity,TKey> | SqlSugarAuditedRepository<,> | 审计仓储 |
IAggregateRootRepository<TEntity,TKey> | SqlSugarAggregateRepository<,> | 聚合根仓储,写操作登记领域事件到工作单元 |
SqlSugarRepositoryBase<TEntity,TKey> 约束 where TEntity : class, IEntityBase<TKey>, new()、where TKey : IEquatable<TKey>。
只读仓储关键方法(SqlSugarReadOnlyRepository<,>):
| 方法 | 说明 |
|---|---|
Task<TEntity?> GetByIdAsync(TKey id, ...) | 按主键取单条 |
Task<IReadOnlyList<TEntity>> GetByIdsAsync(IEnumerable<TKey> ids, ...) | 按主键集合取 |
Task<TEntity?> GetFirstAsync(Expression<Func<TEntity,bool>> predicate, ...) | 条件取首个;另有规约重载 |
Task<IReadOnlyList<TEntity>> GetListAsync(Expression<Func<TEntity,bool>> predicate, ...) | 条件列表;另有排序 / 规约 / PageRequestDtoBase 重载 |
Task<IReadOnlyList<TEntity>> GetAllAsync(...) | 取全部 |
Task<long> CountAsync(...) | 统计(无参 / 条件 / 规约) |
Task<bool> AnyAsync(Expression<Func<TEntity,bool>> predicate, ...) | 存在性判断(条件 / 规约) |
Task<PageResultDtoBase<TEntity>> GetPagedAsync(int pageIndex, int pageSize, ...) | 分页;多种重载(条件、条件+排序、规约、PageRequestDtoBase) |
Task<PageResultDtoBase<TEntity>> GetPagedAutoAsync(object queryDto, ...) | 按查询 DTO 自动构建条件分页 |
读写仓储关键方法(SqlSugarRepositoryBase<,>):
| 方法 | 说明 |
|---|---|
Task<TEntity> AddAsync(TEntity entity, ...) | 新增,返回持久化后实体 |
Task<TKey> AddReturnIdAsync(TEntity entity, ...) | 新增并返回主键 |
Task<IReadOnlyList<TEntity>> AddRangeAsync(IEnumerable<TEntity> entities, ...) | 批量新增 |
Task<TEntity> UpdateAsync(TEntity entity, ...) | 按主键更新(先租户可见性预读) |
Task<bool> UpdateAsync(Expression<Func<TEntity,TEntity>> columns, Expression<Func<TEntity,bool>> where, ...) | 按条件更新指定列 |
Task<IReadOnlyList<TEntity>> UpdateRangeAsync(IEnumerable<TEntity> entities, ...) | 批量更新(校验全部可见) |
Task<bool> AddOrUpdateAsync(TEntity entity, ...) | 按 IsTransient() 决定新增/更新;另有批量重载 |
Task<bool> DeleteAsync(TEntity entity, ...) | 按实体删除;另有 DeleteByIdAsync、按条件、批量重载 |
软删除过滤开启时,对实现
ISoftDelete的实体,Delete*走的是全局过滤器语义;SqlSugarSoftDeleteRepository的SoftDeleteAsync是把IsDeleted=true(并置DeletedTime)走更新落库,RestoreAsync反之。
客户端与租户解析
| 类型 | 说明 |
|---|---|
ISqlSugarClientResolver | GetCurrentClient() 按当前租户解析并把连接登记进事务型 UoW;GetClientForEntity(entityType) / GetClientForEntity<TEntity>() 按实体模块数据源解析;GetClient(configId)、GetAllConfigIds()、GetAllClients()(初始化/种子遍历各库,含派生出的模块库)、GetCurrentLayoutConfigIds()(当前这一套布局有哪些库)、AsTenant() |
ModuleDataSourceAttribute | 实体上声明所属模块数据源:[ModuleDataSource("Erp")],标在基类上对派生实体生效;SqlSugar 原生 [Tenant("Erp")] 同样被识别 |
IEntityModuleDataSourceResolver | 实体模块数据源解析器:ResolveModuleDataSource(entityType) 返回实体声明的模块名,未声明返回 null;默认实现读特性并缓存,可 Replace |
IModuleDataSourceConnectionResolver | 模块数据源连接解析器:ResolveClient(moduleDataSource, parentConfigId) 按「模块名 + 当前布局」定连接,两条维度在此交汇 |
ISqlSugarTenantConnectionResolver | ResolveCurrentConfigId() / ResolveConfigId(long? tenantId, string? name) / GetConfigIds()——解析顺序:自定义委托 → tenantId 直配 → 前缀+tenantId → 租户名 → 默认 |
ISqlSugarConnectionConfigurator | Configure(provider) 装配过滤器+AOP;EnsureTenantConnection(tenant, descriptor) 运行时幂等注册租户独立连接(缺连接串 fail-closed) |
ISqlSugarTenantConnectionProvider | 业务实现的租户库隔离扩展点:Resolve(tenantId, name) 返回连接描述符;返回 null 退化为 ConfigId 解析,抛异常则 fail-closed |
SqlSugarTransactionApi | 工作单元事务适配器,ITransactionApi+ISupportsRollback,包装 Ado.BeginTran/CommitTran/RollbackTran |
初始化与种子
| 类型 | 说明 |
|---|---|
IDbInitializer | InitializeAsync()(遍历全部库的完整流程)/ InitializeCurrentLayoutAsync()(只初始化当前租户这一套布局,租户开通时用)/ CreateDatabaseAsync() / CreateTablesAsync() / SeedDataAsync() |
IDataSeeder | 种子契约:int Order(越小越先)/ string Name / Task SeedAsync() |
DataSeederBase | 种子基类:提供 DbClient、DbClientFor<T>()(按实体模块数据源解析)、HasDataAsync<T>(predicate)、BulkInsertAsync<T>(list) 等辅助 |
IDbEntityTypeProvider | 建表实体提供器:GetEntityTypes(context) 决定当前库建哪些表,默认实现按特性+选项筛选,可 Replace |
IDataSeederSelector | 种子选取器:Select(seeders, context) 决定当前库跑哪些种子,默认实现按特性+选项筛选,可 Replace |
TableInitializationAttribute | 实体上声明建表方式:Enabled / Group / Target / ConnectionConfigIds |
DataSeedingAttribute | 种子上声明播种方式:Enabled / Group / Target / ConnectionConfigIds |
DbInitializationContext | 当前库上下文:ConnectionConfigId / TenantId / IsTenantDatabase / Target |
IDatabaseMetadataProvider | 库表结构元数据读取 |
审计
| 类型 | 说明 |
|---|---|
SqlSugarDataExecutingHandler | DataExecuting AOP 处理器:Insert 注入雪花主键+创建审计+TraceId;Update 注入修改审计(含软删);Delete 注入删除审计 |
IEntityAuditContextProvider | 审计上下文来源,默认 DefaultEntityAuditContextProvider(从 ICurrentUser/HTTP 填充),可注册覆盖 |
IEntityDiffLogWriter | 差异日志落库器,默认 NullEntityDiffLogWriter(零开销);启用 EnableDiffLog 时须替换为真实实现 |
SqlSugar 实体基类
本包提供带 SugarColumn 映射(snake_case 列名)的实体基类,供业务实体继承:
SugarEntity<TKey>/SugarEntityWithIdentity— 基础实体SugarCreationEntity/SugarModificationEntity/SugarDeletionEntity/SugarFullAuditedEntity<TKey>— 审计实体(Created_Time/Modified_Time/Is_Deleted/Deleted_Time等映射 +Row_Version并发标识)SugarMultiTenantEntity<TKey>及其审计变体 — 多租户实体(含Tenant_Id列,0=平台/全局模板)SugarAggregateRoot/SugarMultiTenantAggregateRoot— 聚合根基类
配置
配置节:XiHan:Data:SqlSugarCore(XiHanSqlSugarCoreOptions.SectionName)。
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
ConnectionConfigs | List<SqlSugarConnectionConfigOptions> | [] | 连接配置集合 |
DefaultConfigId | string | "Default" | 默认连接标识 |
TenantConfigIdPrefix | string | "Tenant_" | 租户连接标识前缀(解析 前缀+tenantId) |
ThrowIfTenantConnectionNotFound | bool | false | 租户有值但找不到连接时是否抛异常 |
EnableTenantModuleDatabaseConvention | bool | true | 库隔离租户按默认布局镜像出自带的整套模块库(库名从租户主库名派生) |
ResolveConnectionConfigId | Func<long?,string?,string?>? | null | 自定义租户连接解析委托 |
EnableTenantFilter | bool | true | 启用租户行级过滤器 |
EnableSoftDeleteFilter | bool | true | 启用软删除过滤器 |
EnableAutoUpdateQueryFilter | bool | true | 更新时自动附加查询过滤条件 |
EnableAutoDeleteQueryFilter | bool | true | 删除时自动附加查询过滤条件 |
GlobalFilters | Dictionary<Type,LambdaExpression> | {} | 额外全局过滤器(仅代码注册,推荐 AddGlobalFilter<TEntity>) |
ConfigureDbAction | Action<ISqlSugarClient>? | null | 客户端自定义配置钩子 |
EnableSqlLog | bool | false | SQL 执行日志 |
EnableSqlErrorLog | bool | true | SQL 异常日志 |
EnableSlowSqlLog | bool | true | 慢 SQL 日志 |
SlowSqlThresholdMilliseconds | int | 10000 | 慢 SQL 阈值(毫秒),纯观测用途、不影响语句执行 |
CommandTimeoutSeconds | int | 300 | ADO 命令执行超时(秒);0/负值不覆盖,沿用 SqlSugar 默认 300 秒 |
EnableDiffLog | bool | false | 实体差异日志(需实现 IEntityDiffLogWriter/IEntityAuditContextProvider) |
EnableDbInitialization | bool | false | 启动自动建库总开关 |
EnableTableInitialization | bool | false | CodeFirst 建表 |
EnableDataSeeding | bool | false | 执行种子数据 |
TableInitialization | TableInitializationOptions | 全量 | 建表选取规则(哪些实体真正建表),见选择初始化范围 |
DataSeeding | DataSeedingOptions | 全量 | 种子选取规则(哪些种子真正执行),见选择初始化范围 |
SqlSugarConnectionConfigOptions 字段:ConfigId(默认 "Default")、ConnectionString、DbType(SqlSugar 枚举)、IsAutoCloseConnection(默认 true)、InitKeyType(默认 InitKeyType.Attribute)、MoreSettings、DbLinkName、LanguageType、IndexSuffix、SlaveConnectionConfigs(主从读写分离,详见下文)。
DbType 取 SqlSugar 的 DbType 枚举值,支持 PostgreSQL / MySql / MariaDB 等(DbInitializer 对 MySql/MySqlConnector 会自动把库字符集归一化为 utf8mb4)。
真实示例(取自 XiHan.BasicApp.WebHost/appsettings.Development.json):
{
"XiHan": {
"Data": {
"SqlSugarCore": {
"ConnectionConfigs": [
{
"ConfigId": "1",
"ConnectionString": "Server=127.0.0.1;Database=XiHanBasicApp;Username=postgres;Password=postgres;TrustServerCertificate=true;",
"DbType": "PostgreSQL",
"IsAutoCloseConnection": true,
"SlaveConnectionConfigs": []
}
],
"EnableSqlLog": false,
"EnableSqlErrorLog": true,
"EnableSlowSqlLog": true,
"SlowSqlThresholdMilliseconds": 10000,
"EnableDbInitialization": true,
"EnableTableInitialization": true,
"EnableDataSeeding": true
}
}
}
}主从读写分离
框架把 SqlSugar 的原生主从能力完整放出来,遵循「想改就改、不改吃默认」:SqlSugar 自动把 SELECT 按权重路由到从库、INSERT/UPDATE/DELETE 与事务走主库,无需业务感知。配置有两个通道。
通道 A:appsettings 声明从库(够用即可)
在连接里填 SlaveConnectionConfigs 即可让读走从库:
{
"ConfigId": "1",
"ConnectionString": "主库连接串",
"DbType": "PostgreSQL",
"SlaveConnectionConfigs": [
{ "ConnectionString": "从库1连接串" },
{ "ConnectionString": "从库2连接串" }
]
}HitRate 无法经 appsettings 设置(重要)
SqlSugar 的 SlaveConnectionConfig.HitRate(读权重)是字段而非属性,.NET 配置绑定器只绑属性、不绑字段,所以 appsettings 里写的 HitRate 绑不上、恒为 0,会导致从库权重为 0 而永远收不到读流量。
框架已对此兜底:构建时把 HitRate <= 0 的从库归一化为 DefaultSlaveHitRate(默认 10),保证 appsettings 声明的从库能真正分担读。要给从库设置差异化权重,请用下面的通道 B。
通道 B:代码钩子完整定制原生配置
XiHanSqlSugarCoreOptions.ConfigureConnectionConfigs 在 SqlSugarScope 构建前,把已填好框架默认值的原生 List<ConnectionConfig> 交给你,任何原生能力都能改——差异化 HitRate、ConfigureExternalServices(如可空列处理)、自定义主从探活等:
services.Configure<XiHanSqlSugarCoreOptions>(options =>
{
options.ConfigureConnectionConfigs = configs =>
{
foreach (var config in configs.Where(c => Equals(c.ConfigId, "1")))
{
// HitRate 只能在代码里设(字段赋值不受配置绑定限制)
config.SlaveConnectionConfigs =
[
new SlaveConnectionConfig { HitRate = 30, ConnectionString = "从库1连接串" },
new SlaveConnectionConfig { HitRate = 10, ConnectionString = "从库2连接串" }
];
}
};
});该钩子对运行时动态注册的租户连接同样生效(以单元素列表触发,请按 ConfigId 分支处理并保证幂等)。库隔离租户也可在 SqlSugarTenantConnection.SlaveConnectionConfigs 直接带从库。
追加
DataExecuting逻辑请用AppendDataExecuting,切勿在ConfigureDbAction里直接给Aop.DataExecuting赋值——该事件是单次赋值,直接赋值会整体冲掉框架的雪花主键/审计/租户注入。核心注入焊死、只许追加。
可选:从库健康探针(默认关闭)
打开 EnableSlaveHealthCheck 后,框架后台周期性探测从库连通性:不可用的从库自动摘除读权重(HitRate=0),恢复后按 SlaveFailureCooldownSeconds 冷却窗口回填原始权重。默认关闭、零成本;仅覆盖 appsettings 静态连接,运行时租户连接不在探测范围。
| 选项 | 默认 | 说明 |
|---|---|---|
DefaultSlaveHitRate | 10 | 通道 A 从库权重归一化默认值 |
EnableSlaveHealthCheck | false | 是否启用从库健康探针 |
SlaveHealthCheckIntervalSeconds | 30 | 探测周期(秒) |
SlaveFailureCooldownSeconds | 120 | 故障冷却窗口(秒),避免抖动 |
使用示例
1. 应用服务里注入仓储读写
仓储接口在 XiHan.Framework.Domain.Repositories 中定义,本包提供实现,直接注入即可:
public class ProductService
{
private readonly IRepositoryBase<Product, long> _repository;
public ProductService(IRepositoryBase<Product, long> repository)
{
_repository = repository;
}
public async Task<Product> CreateAsync(Product product)
{
// 雪花主键、租户 Id、创建审计字段由 DataExecuting AOP 自动注入
return await _repository.AddAsync(product);
}
public async Task<Product?> GetAsync(long id)
{
// 自动附加当前租户过滤 + 软删除过滤
return await _repository.GetByIdAsync(id);
}
public async Task<PageResultDtoBase<Product>> QueryAsync(int pageIndex, int pageSize, string keyword)
{
return await _repository.GetPagedAsync(
pageIndex, pageSize,
p => p.Name.Contains(keyword),
p => p.CreatedTime, isAscending: false);
}
}2. 软删除仓储
public class ArticleService(ISoftDeleteRepositoryBase<Article, long> repo)
{
public Task RemoveAsync(long id) => repo.SoftDeleteAsync(id); // 置 IsDeleted=true
public Task RestoreAsync(long id) => repo.RestoreAsync(id); // 恢复
public Task<IReadOnlyList<Article>> RecycleBinAsync() // 已删列表(跳过软删过滤)
=> repo.GetDeletedAsync();
}3. 注册种子数据
public class RoleSeeder : DataSeederBase
{
public RoleSeeder(ISqlSugarClientResolver r, ILogger<RoleSeeder> l, IServiceProvider sp)
: base(r, l, sp) { }
public override int Order => 100;
public override string Name => "系统角色";
protected override async Task SeedInternalAsync()
{
if (await HasDataAsync<Role>(x => x.Code == "admin")) return;
await BulkInsertAsync(new List<Role> { new() { Code = "admin", Name = "管理员" } });
}
}
// 模块 ConfigureServices 中注册(须配合 EnableDataSeeding=true)
services.AddDataSeeder<RoleSeeder>();
// 或批量:services.AddDataSeeders(typeof(RoleSeeder), typeof(MenuSeeder));模块分库:模块数据源
同一套应用里挂多个业务模块、各模块一个库时(主应用一个库,别人基于框架搭的 Erp、Mes 各自一个库),在实体上标一次模块数据源即可:
// 主应用实体:不标特性,跟着租户上下文解析连接(主库)
[SugarTable("sys_user")]
public class SysUser : SugarMultiTenantEntity<long> { }
// Erp 模块实体:固定落在 Erp 模块库
[SugarTable("erp_order")]
[ModuleDataSource("Erp")]
public class ErpOrder : SugarMultiTenantEntity<long> { }模块库挂在主连接下面声明——条目只写模块名和连接串,其余字段留空即继承主连接:
{
"XiHan": { "Data": { "SqlSugarCore": {
"DefaultConfigId": "Default",
"ConnectionConfigs": [
{
"ConfigId": "Default",
"DbType": "MySql",
"ConnectionString": "...",
"ModuleDataSourceConfigs": [
{ "ModuleDataSource": "Erp", "ConnectionString": "..." },
{ "ModuleDataSource": "Mes", "DbType": "PostgreSQL", "ConnectionString": "..." }
]
}
]
} } }
}之后 IRepositoryBase<ErpOrder, long> 的读写全部走 Erp 模块库,业务代码不用切连接、不用自己拿 ISqlSugarClient:
public class ErpOrderAppService(IRepositoryBase<ErpOrder, long> orderRepository, IRepositoryBase<SysUser, long> userRepository)
{
// orderRepository 走 Erp 模块库,userRepository 走主库,两者在同一次请求里各取各的连接
}规则:
- 模块名不是连接标识:模块库的
ConfigId由父连接派生({父连接}_{模块名}),上例即Default_Erp。因此模块名不占用顶层ConfigId命名空间,跟租户连接标识撞不上。 - 两条维度正交:租户维度先定「用哪一套布局」(主库加它下面的模块库),模块维度再在布局内选库。库隔离租户的模块表落
Tenant_{租户Id}_Erp,字段隔离租户落共享的Default_Erp,实体一个字都不用改。 - 库隔离租户自带整套模块库:租户连接描述符没声明模块库时,框架按默认布局逐条镜像给它——默认布局给某模块分了库,该租户也分,库名由租户主库名派生成
{租户库名}_{模块名}(SQLite 是库文件名);默认布局那条连接串留空(该模块不分库)时租户同样不分,模块表落它自己的主库。于是「租户声明了库隔离」就意味着它的数据都在自己的库里,不会有一部分悄悄落回公共模块库。开关是EnableTenantModuleDatabaseConvention(默认true),关掉即退回旧行为(模块表回落共享模块库,按TenantId行过滤区分)。派生不出库名(如 Oracle 这类连接串里没有库名字段)时抛异常,且抛在主库注册之前——整套布局要么一起落地要么都不落地。 - 连接串留空表示不分库:模块条目写了名字、连接串留空,即该模块直接用父连接的主库;条目整条缺失才是未配置。
- fail-closed:实体声明的模块数据源在当前布局与默认布局里都没有配置时直接抛异常,绝不回退主库造成跨库串写。
- 建表口径一致:声明了模块数据源的实体只在自己的模块库建表(每套布局各建一份);未声明的实体不进模块库,需要模块库里也建框架公共表时把该
ConfigId列入TableInitialization.SharedConnectionConfigIds。 - 模块库跟着启动一起建:启动时的建库建表遍历的是全量连接标识(含派生出的模块库),模块库不存在会被自动创建;库隔离租户开通时走
IDbInitializer.InitializeCurrentLayoutAsync(),把该租户的主库与它自带的模块库一起建出来。 - 种子按连接圈定:模块自带的种子标
[DataSeeding(ConnectionConfigIds = ["Default_Erp"])],避免在遍历每个库时重复执行。 - 兼容 SqlSugar 原生特性:实体已标了 SqlSugar 的
[Tenant("Erp")]时同样被识别,此时声明的是连接标识本身,按相等匹配。 - 跨库写不是一个事务:同一工作单元跨多个库写入时,每个
ConfigId各开一个本地事务,框架不提供跨库分布式事务;需要强一致时把跨库步骤拆成可补偿的流程。
要把某个租户的某个模块库指到别处(另一台机器、另一种库),在它的租户连接描述符里显式写出来即可——显式的优先,上面的约定只补它没提到的模块:
return descriptor with
{
ModuleDataSourceConfigs = [new SqlSugarModuleDataSourceConfigOptions
{
ModuleDataSource = "Erp",
ConnectionString = "..."
}]
};需要从别处(模块清单、配置表)决定实体归属时,替换解析器即可,仓储路由与建表初始化会同步跟着变:
services.Replace(ServiceDescriptor.Singleton<IEntityModuleDataSourceResolver, ManifestEntityModuleDataSourceResolver>());选择初始化范围
EnableTableInitialization / EnableDataSeeding 是总开关,选哪些表建、哪些种子跑由两组选取规则决定。默认全量(扫描到的实体都建、注册的种子都跑),与总开关打开时的历史行为一致。
特性:在实体/种子上声明
// 这张表由 DBA 维护,框架不要碰
[SugarTable("sys_partitioned_log")]
[TableInitialization(false)]
public class SysPartitionedLog : SugarEntity<long> { }
// 平台库独有,租户独立库不建
[SugarTable("sys_tenant")]
[TableInitialization(Target = DbInitializationTarget.Platform)]
public class SysTenant : SugarEntity<long> { }
// 只在归档库建表
[SugarTable("sys_archive")]
[TableInitialization(ConnectionConfigIds = ["Archive"])]
public class SysArchive : SugarEntity<long> { }
// 整组种子归一个分组,配置里按组开关
[DataSeeding(Group = "Demo")]
public abstract class DemoSeederBase : DataSeederBase { }| 成员 | 说明 |
|---|---|
Enabled | false 表示不参与,该表/该种子交由开发者自己维护 |
Group | 分组名,配合选项里的 IncludedGroups/ExcludedGroups 按组开关 |
Target | Platform / Tenant / All(默认)。租户库指 ConfigId 以 TenantConfigIdPrefix 开头的运行时连接 |
ConnectionConfigIds | 仅在这些连接上建表/播种,为空表示不限连接 |
特性标在基类上对派生类型同样生效(Inherited = true)。
选项:在配置里圈定范围
{
"XiHan": { "Data": { "SqlSugarCore": {
"EnableTableInitialization": true,
"TableInitialization": {
"Mode": "All",
"ExcludedTables": [ "sys_diff_log", "XiHan.BasicApp.Cms.*" ]
},
"EnableDataSeeding": true,
"DataSeeding": {
"ExcludedGroups": [ "Demo" ]
}
} } }
}Mode:All(默认,扫到的都参与,标了Enabled = false的除外)或OptIn(只有显式标了特性的才参与)。IncludedGroups/ExcludedGroups:按特性上的Group圈定。IncludedTables/ExcludedTables(种子对应IncludedSeeders/ExcludedSeeders):支持*?通配;表按实体类名、实体全名、表名任一匹配,种子按Name、类名、类全名任一匹配。SharedConnectionConfigIds(仅建表):模块库中额外允许建「未声明模块数据源实体」的连接名单,支持通配。Filter:Func<Type,bool>?/Func<IDataSeeder,bool>?代码钩子,只能在Configure<XiHanSqlSugarCoreOptions>里设置。
判定顺序:模块数据源 → 特性 → 模式 → 分组 → 名称 → 自定义委托,任一环节否决即不参与。
接管:自己实现选取逻辑
想完全自己决定(例如从清单文件、数据库配置表读建表范围)时替换默认实现:
services.Replace(ServiceDescriptor.Singleton<IDbEntityTypeProvider, ManifestEntityTypeProvider>());
services.Replace(ServiceDescriptor.Singleton<IDataSeederSelector, MyDataSeederSelector>());两个接口都拿到 DbInitializationContext(当前连接标识、租户标识、是否租户独立库),据此对不同库返回不同范围。默认实现的 ShouldInitialize / ShouldSeed 是 protected virtual,只想改一条规则时继承默认实现覆写即可。
扩展点 / 自定义
所有核心服务用 TryAdd* 注册,业务层可注册自定义实现覆盖:
- 审计上下文:注册自定义
IEntityAuditContextProvider替换默认从ICurrentUser/HTTP 的填充逻辑。 - 差异日志落库:启用
EnableDiffLog后注册真实IEntityDiffLogWriter(默认Null不产生记录)。 - 租户库隔离:注册
ISqlSugarTenantConnectionProvider,Resolve返回租户独立连接描述符即启用库级隔离;返回null退化为ConfigId(行/字段)隔离。 - 自定义连接解析:设置
Options.ResolveConnectionConfigId委托,或用TenantConfigIdPrefix/DefaultConfigId约定映射。 - 额外全局过滤器:调用
Options.AddGlobalFilter<TEntity>(expr)注册表达式树(须可翻译为 SQL 的成员访问/标量比较,勿调用外部方法;构建期 fail-fast 校验,非法表达式启动即报错)。 - 初始化范围:用
[TableInitialization]/[DataSeeding]特性与TableInitialization/DataSeeding选项圈定,或Replace掉IDbEntityTypeProvider/IDataSeederSelector自己实现,见选择初始化范围。 - 模块数据源:实体标
[ModuleDataSource("Erp")]声明所属模块库,或Replace掉IEntityModuleDataSourceResolver从别处决定归属,见模块分库(模块数据源)。 - 客户端级配置:
Options.ConfigureDbAction钩子拿到ISqlSugarClient做自定义装配。
注意事项与最佳实践
- 默认不建库:
EnableDbInitialization/EnableTableInitialization/EnableDataSeeding默认全false;本机/首次部署需在配置里显式打开。表已存在会跳过(不做迁移,遵循"重建库、无向后兼容"约定)。 - 总开关之外还有范围:打开建表/种子开关后默认全量参与;要挑表挑种子(含平台库与租户独立库分开建)见选择初始化范围。
- 事务靠工作单元:仓储内不开事务;需要多写原子提交时给应用服务方法打
[UnitOfWork(isTransactional: true)],ISqlSugarClientResolver会自动把连接登记进 UoW 事务。见 XiHan.Framework.Uow。 - 跨库不是一个事务:同一工作单元跨多个
ConfigId(模块分库、租户独立库)写入时,每个连接各开本地事务,框架不提供跨库分布式事务;有强一致要求就把跨库步骤拆成可补偿流程。 - 越租户写会被拒:
Update/Delete前的可见性预读若读不到实体(不在当前租户/已软删),抛InvalidOperationException;这是安全边界,不是 bug。 - 跨租户/含软删查询:仓储内部提供
CreateNoTenantQueryable()(清租户过滤)/CreateWithDeletedQueryable()(清软删过滤),仅用于平台运维/审计恢复且须自行做权限校验。 TenantId=0是全局模板:多租户实体的TenantId非空,0表示平台/全局记录,对所有租户可见(配合UNIQUE(TenantId, Code)复合唯一索引对全局记录生效)。- 审计字段勿手填:
TenantId、创建/修改/删除时间与操作人由DataExecutingAOP 注入,业务侧手填会被覆盖或引发不一致。 - 雪花主键:主键由
IDistributedIdGenerator<long>通过StaticConfig.CustomSnowFlakeFunc全局生成;实体基类的BasicId映射为非自增主键。 - 慢 SQL 阈值与命令超时相互独立:
SlowSqlThresholdMilliseconds只用于慢 SQL 日志判定;命令执行超时由CommandTimeoutSeconds单独配置(默认 300s),且应明显大于慢 SQL 阈值,否则慢 SQL 会先被驱动杀掉、慢日志记不到。
依赖模块
- XiHan.Framework.Domain(仓储接口 / 实体与聚合根抽象来源)
- XiHan.Framework.Uow(事务边界接管,
SqlSugarTransactionApi接入) - XiHan.Framework.MultiTenancy(
ICurrentTenant/ICurrentTenantAccessor租户上下文) - XiHan.Framework.Security(
ICurrentUser审计上下文) - XiHan.Framework.DistributedIds(雪花 ID 生成器)
- XiHan.Framework.Auditing(
IEntityAuditContextProvider/IEntityDiffLogWriter默认实现来源) - XiHan.Framework.Core(模块化基础设施,含链路追踪
XiHanActivitySources) - 第三方核心:SqlSugarCore
5.1.4.217
