数据库操作
数据库操作
本项目数据库采用 ORM SqlSugar,数据仓储模式,在 SimpleClient<T> 之上做了轻量封装,核心是三件事:库路由、事务、AOP 审计。
- 仓储:
ZR.Repository/BaseRepository.cs - 配置与 AOP:
ZR.ServiceCore/SqlSugar/SqlsugarSetup.cs - 迁移:
ZR.ServiceCore/SqlSugar/DbMigrationService.cs(详见数据库迁移)
一、数据仓储(BaseRepository)
BaseRepository<T> 继承 SimpleClient<T>,构造函数按实体类型自动选择数据库连接:
| 场景 | 路由目标 |
|---|---|
实体实现 IMainDbEntity | 系统库(App.SystemDbConfigId,固定 "0") |
实体标 [Tenant("MainDb")] | 业务主库(App.BizMainDbConfigId,缺省 "0") |
| 其他(SaaS 已启用) | 当前租户库(App.GetCurrentTenantId()) |
非 SaaS + [Tenant("MallDb")] | 商城库(App.MallDbConfigId,缺省 "0") |
非 SaaS + [Tenant("1")] 等具体值 | 该值对应的 ConfigId(也支持配置 key 引用) |
| 非 SaaS + 无标记 | 系统库 "0" |
查看 BaseRepository 关键代码
public class BaseRepository<T> : SimpleClient<T> where T : class, new()
{
/// <summary>[Tenant] 特性中标记业务主库的 key,解析为 App.BizMainDbConfigId</summary>
public const string BizMainDbKey = "MainDb";
public ITenant itenant = null;//多租户事务
public BaseRepository(ISqlSugarClient context = null) : base(context)
{
if (App.IsTenantEnabled())
{
if (typeof(IMainDbEntity).IsAssignableFrom(typeof(T)))
{
Context = DbScoped.SugarScope.GetConnectionScope(App.SystemDbConfigId); // 系统库
}
else if (IsBizMainDbKey(typeof(T).GetCustomAttribute<TenantAttribute>()?.configId))
{
Context = DbScoped.SugarScope.GetConnectionScope(App.BizMainDbConfigId); // 业务主库
}
else
{
Context = DbScoped.SugarScope.GetConnectionScope(App.GetCurrentTenantId()); // 租户库
}
}
else
{
// [Tenant("MallDb")] → App.MallDbConfigId(未配置回退 "0")
// [Tenant("1")] → App.Configuration["1"] ?? "1"
var tenantAttr = typeof(T).GetCustomAttribute<TenantAttribute>()?.configId;
Context = tenantAttr != null
? DbScoped.SugarScope.GetConnectionScope(ResolveDbConfigId(tenantAttr))
: context ?? DbScoped.SugarScope.GetConnectionScope(App.SystemDbConfigId);
}
itenant = DbScoped.SugarScope;//设置租户接口
}
/// <summary>解析租户库:优先传入的租户ID,其次当前请求租户;SaaS 下找不到配置直接抛错</summary>
protected ISqlSugarClient ResolveTenantDb(string tenantId = null) { /* ... */ }
/// <summary>解析系统库连接(固定 "0")</summary>
protected ISqlSugarClient ResolveMainDb()
{
return DbScoped.SugarScope.GetConnectionScope(App.SystemDbConfigId);
}
// ... 增删改查、分页(GetPages / ToPage / ToPageAsync)等方法
}
要点:
- SaaS 下
ResolveTenantDb不会再静默回退主库:租户标识为空或dbConfigs中找不到对应ConfigId时直接抛CustomException,避免"查错库还查成功了"。 - 分页统一走
PagedInfo<T>/PagerInfo,QueryableExtension提供ToPage/ToPageAsync(支持投影到 DTO)。 - 数据权限与租户过滤在 Setup 阶段挂载:
DataScopeExtensions.RegisterScopeFilter(SysUser/SysDept/SysRole/SysLogininfor/UserOnlineLog),SaaS 下主库连接再叠加TenantFilter.Apply。详见数据权限、SaaS 多租户。
后台任务取库:BackgroundDbHelper
后台线程(fire-and-forget、Quartz Job、Webhook 重试等)禁止复用请求级 Context:请求结束后连接已释放、租户上下文丢失,与主事务共用连接还会报 "connection is already in use"。
// 请求线程内:捕获租户
string tenantId = BackgroundDbHelper.CaptureTenantId();
// 后台线程内:建独立连接(CopyNew,多租户时路由到该租户库)
var db = BackgroundDbHelper.CreateBackgroundDb(tenantId);
二、使用事务
单库事务
public bool AuthDataScope(SysRoleDto sysRoleDto)
{
var result = UseTran(() =>
{
var oldMenus = SelectUserRoleMenus(sysRoleDto.RoleId);
var newMenus = sysRoleDto.MenuIds;
var arr_c = oldMenus.Intersect(newMenus).ToArray();
var delMenuIds = oldMenus.Where(c => !arr_c.Contains(c)).ToArray();
var addMenuIds = newMenus.Where(c => !arr_c.Contains(c)).ToArray();
RoleMenuService.DeleteRoleMenuByRoleIdMenuIds(sysRoleDto.RoleId, delMenuIds);
sysRoleDto.MenuIds = addMenuIds.ToList();
sysRoleDto.DelMenuIds = delMenuIds.ToList();
InsertRoleMenu(sysRoleDto);
});
if (!result.IsSuccess)
{
throw new Exception("提交数据异常," + result.ErrorMessage, result.ErrorException);
}
return true;
}
UseTran 内部是 Context.Ado.UseTran(...),异常会先 RollbackTran 再抛出,因此不要在 UseTran 里吞异常。
多库 / 多租户事务
跨库操作要用 AsTenant() 的事务,单连接事务管不到另一个库:
var result = UseTran(DbScoped.SugarScope, () =>
{
itenant.GetConnectionScope("0").Insertable(new SysUser { /* ... */ }).ExecuteCommand();
itenant.GetConnectionScope("tenant_a").Insertable(new SysLog { /* ... */ }).ExecuteCommand();
});
UseTran2
等价于单库事务但只返回 bool(内部 result.IsSuccess),适合不关心错误详情的场景。
Action 级自动事务(TransactionFilter)
框架提供了 TransactionFilter(ZR.ServiceCore/Filters/TransactionFilter.cs):Action 执行前 BeginTranAsync,成功提交、异常回滚。
⚠️ 它当前没有全局注册,
Program.cs只注册了GlobalActionMonitor。若要使用,需在目标 Controller / Action 上显式标注[TransactionFilter],并注意:
- 事务包住整个 Action,长耗时或含外部调用的接口会长时间持锁;
- 与其他事务叠加时(Service 内又调
UseTran)容易产生嵌套事务语义问题。
大多数场景建议在 Service 层显式 UseTran,边界更清楚。
三、切库
方式 1:按 ConfigId 取连接
var db1 = DbScoped.SugarScope.GetConnection(configId); // 非线程安全
var db1 = DbScoped.SugarScope.GetConnectionScope(configId); // 线程安全(推荐)
方式 2:按实体特性取连接
var db2 = DbScoped.SugarScope.GetConnectionWithAttr<T>(); // 非线程安全
var db2 = DbScoped.SugarScope.GetConnectionScopeWithAttr<T>(); // 线程安全(推荐)
方式 3:Repository / Service 内切库
// 使用 ConfigId 为 test 的数据库
var db = itenant.GetConnectionScope("test"); // 线程安全
// var db = itenant.GetConnection("test"); // 非线程安全
var result = db.Ado.UseStoredProcedure().SqlQuery<SysUser>("proc_xxx");
var list = db.Queryable<SysUser>().ToList();
[Tenant] 特性写法
| 写法 | 解析结果 |
|---|---|
[Tenant("0")] | 系统库,SaaS 下也固定 "0" |
[Tenant("MainDb")] | 业务主库 App.BizMainDbConfigId(缺省回退 "0") |
[Tenant("MallDb")] | 商城库 App.MallDbConfigId(缺省回退 "0";SaaS 下走租户库) |
[Tenant("tenant_a")] | 直接作为 ConfigId |
[Tenant("SomeKey")] | 先查 App.Configuration["SomeKey"],找不到则用原值 |
更多:SqlSugar 多租户文档、本项目的《SaaS 多租户》。
四、数据库初始化与迁移
初始化与迁移已从 Web 启动主链路解耦,配置项 InitDb / InitSeed 已移除。
1. 一次性初始化:--initdb
init-run.bat
# 或
dotnet run --project ZR.Admin.WebApi -- --initdb
执行内容(仅 Development 生效,完成后直接退出):
① CodeFirst 建表 → 系统表(约 35 张)
② 种子数据导入 → wwwroot/data.xlsx(菜单、角色、用户、字典等)
③ 预置套餐创建 → 免费版/专业版
④ 默认租户创建 → 主租户建档
⑤ 独立模块初始化 → 按 ModuleInit 开关执行(Saas / Member / Mall / Workflow / Pro)
详见快速开始。
2. 启动期幂等补齐(自动,不影响启动)
SqlsugarSetup.AddDb 在每次启动时执行三步(失败不阻断启动):
DbMigrationService.ApplyLegacyTableRenames(); // 1) 历史表名重命名(仅主库,幂等)
DbMigrationService.MigrateTenantColumns(); // 2) 存量表补 TenantId 列
DbMigrationService.EnsureTenantBusinessTables(); // 3) 租户库补表/补列(跳过主库)
全量"迁移 + 种子"刻意不再放启动流程,避免每次启动被拖慢。主库的表/列变更走
--initdb或后台「同步结构」;租户库在启动时自动补齐,新建租户同样自动建表。
3. 结构迁移能力
DbMigrationService 提供 Migrate(全量建表+补列)、Diff(只算差异)、EnsureEntitySchema(单实体幂等)、ApplyLegacyTableRenames(历史表名自动改名)、MigrateTenantColumns 等,详见数据库迁移。
迁移只增不减
不会删列、不会改列类型;NOT NULL 且无默认值的新列在存量数据上会补列失败(记入报告,需人工处理)。
五、数据库缓存
SqlSugar 的 DataInfoCacheService 有两种实现,由 RedisServer.dbCache 切换:
| 配置 | 实现类 | 说明 |
|---|---|---|
RedisServer.dbCache=false(默认) | SqlSugarCache | 进程内内存缓存 |
RedisServer.dbCache=true | SqlSugarRedisCache | Redis 缓存,多节点共享 |
实现类在启动装配时确定,改动
dbCache需重启;dbCache=true时还要保证RedisServer:open=1且连接可用。
MoreSettings.IsAutoRemoveDataCache = true:更新数据时自动清理相关查询缓存,因此常规增删改无需手动 RemoveDataCache()。多实例部署且使用了二级缓存时,务必开启 Redis,否则各节点缓存互不可见。
六、AOP 操作
配置在 ZR.ServiceCore/SqlSugar/SqlsugarSetup.cs(SetSugarAop),每个连接独立挂载。
1. 差异化日志(审计)
记录"增 / 删 / 改"前后的数据,落表 sys_sql_diff_log(旧表名 sqlDiffLog 由迁移自动改名),后台可在「SQL 差异日志」页查看。
db.GetConnectionScope(configId).Aop.OnDiffLogEvent = it =>
{
var editBeforeData = it.BeforeData; // 插入时 Before 为 null
var editAfterData = it.AfterData;
var sql = it.Sql;
var data = it.BusinessData; // 业务侧传入的标题/对象
var diffType = it.DiffType; // Insert / Update / Delete
foreach (var item in editBeforeData)
{
var log = new SqlDiffLog
{
BeforeData = db.Utilities.SerializeObject(item.Columns.ToDictionary(it => it.ColumnName, it => it.Value)),
AfterData = /* 取 editAfterData 中同表数据序列化 */,
BusinessData = data?.ToString(),
DiffType = diffType.ToString(),
Sql = sql,
TableName = item.TableName,
UserName = App.UserName,
AddTime = DateTime.Now,
ConfigId = configId
};
db.GetConnectionScopeWithAttr<SqlDiffLog>().Insertable(log).ExecuteReturnSnowflakeId();
}
};
启用方式:
// 修改
Context.Updateable(sysUser).EnableDiffLogEvent("更新数据").ExecuteCommand();
// 删除(BaseRepository.Delete 已封装)
var rows = Delete(id, "删除文章");
2. SQL 执行日志
OnLogExecuting 按 ShowDbLog 开关打印最终 SQL(DELETE / TRUNCATE 走 Warn 级别,其余 Info):
db.GetConnectionScope(configId).Aop.OnLogExecuting = (sql, pars) =>
{
if (!showDbLog) return;
string log = $"【config={configId}】{UtilMethods.GetSqlString(config.DbType, sql, pars)}\n";
// ...
};
3. SQL 错误日志
OnError 记录连接 ConfigId、完整 SQL 与参数、异常堆栈,排查线上问题时先看这里。
4. SQL 超时告警
OnLogExecuted 监控实际执行耗时,超过 sqlExecutionTime 秒时写 Warn 日志并推送企业微信告警(含调用方文件名、行号、方法名):
db.GetConnectionScope(configId).Aop.OnLogExecuted = (sql, pars) =>
{
var seconds = AppSettings.Get<int>("sqlExecutionTime");
if (db.Ado.SqlExecutionTime.TotalSeconds > seconds)
{
WxNoticeHelper.SendMsg("Sql请求时间过长", $"用时{db.Ado.SqlExecutionTime.TotalSeconds}秒【{sql}】...");
}
};
5. 租户字段漏填告警
DataExecuting 会检查主库共享表(TenantFilter.FilteredEntityTypes)的 INSERT 是否设置 TenantId,未设置只打 Warn、不自动补齐(自动填会掩盖调用方漏赋值)。
七、数据库配置
appsettings.json 关键配置
{
// 系统库 ConfigId 固定 "0"(常量,不可配置;配置项 SystemDb 已废弃)
// 业务主库 / 商城库默认也落 "0",仅在独立建库时才需要配置:
// "BizMainDb": "1", // 标记 [Tenant("MainDb")] 的实体走这个 ConfigId
// "MallDb": "1", // 非 SaaS 模式下商城实体走这个 ConfigId
"ShowDbLog": false, // 是否打印 SQL 日志
"sqlExecutionTime": 5, // SQL 执行超时告警阈值(秒)
"dbConfigs": [
{
"Conn": "...",
"DbType": 1, // MySql=0, SqlServer=1, Sqlite=2, Oracle=3, PgSql=4
"ConfigId": "0", // 系统库;租户库需与 sys_tenant.TenantId 一致
"IsAutoCloseConnection": true
}
],
// 代码生成器独立连接
"CodeGenDbConfig": {
"Conn": "Data Source=...Initial Catalog={dbName};...", // {dbName} 为占位符
"DbType": 1,
"DbName": "ZrAdmin" // 默认库名
},
// Redis 数据库缓存
"RedisServer": {
"open": 0, // 是否启用 Redis
"dbCache": false // true 时 SqlSugar 二级缓存走 Redis
},
// 结构迁移
"DbMigration": {
"ReportOnly": false, // true=只出差异报告,不执行 DDL
"AdditionalTypes": [] // 业务模块实体由各自 Initializer 注册,一般无需声明
}
}
单库部署不需要任何
BizMainDb/MallDb配置;配了SystemDb会被忽略并输出启动告警(WarnLegacyDbConfig)。
DbType 枚举值
| 值 | 数据库类型 |
|---|---|
| 0 | MySql |
| 1 | SqlServer |
| 2 | Sqlite |
| 3 | Oracle |
| 4 | PostgreSQL |
跨库注意
- 不能跨库 JOIN:SqlSugar 的
Queryable只作用于当前连接;跨库数据要么分两次查再内存关联,要么把表放到同一个库。 - 跨库事务用
AsTenant().UseTran(...),单连接BeginTran管不到另一个库。 - 定时任务的 SQL 执行有白名单校验(
SqlTaskGuard:拒绝多语句、DDL、系统过程、无WHERE的全表写),见《定时任务》。
