FreeSql 数据访问(Aegis.Core.FreeSql)
解决什么问题
Aegis.Core.FreeSql 负责数据库连接初始化、IFreeSql 实例注册和仓储自动扫描。它封装了 FreeSql ORM 的启动配置,让项目不需要手动构建 FreeSqlBuilder。
版本一致性
Aegis 框架当前基于 FreeSql 3.5.303。业务项目中如果需要直接引用 FreeSql 的扩展包(如数据库提供程序、FreeSql.DbContext 等),版本号必须与框架保持一致。版本不一致会导致运行时类型冲突或方法找不到。
需要保持一致的包:
| 包名 | 版本 |
|---|---|
FreeSql | 3.5.303 |
FreeSql.Repository | 3.5.303 |
FreeSql.Provider.PostgreSQL | 3.5.303 |
FreeSql.Provider.SqlServer | 3.5.303 |
FreeSql.DbContext | 3.5.303 |
只引用 Aegis.Core.FreeSql 而不单独安装 FreeSql 相关包的项目不需要关心这个问题,传递依赖会自动带进来。
框架升级时的注意事项
Aegis 框架升级时可能同步升级 FreeSql 版本。升级后需要注意:
- 业务项目中单独引用的 FreeSql 扩展包版本要同步更新,与框架保持一致
- FreeSql 大版本升级(如 3.x → 4.x)可能存在破坏性变更,升级前查阅 FreeSql 官方更新日志
- 框架的
Changelog.md中会标注 FreeSql 版本是否发生了变更
如何引入
NuGet 包:Aegis.Core.FreeSql
不通过 Component.deps.json 注册,在 Startup.cs 中手动调用:
services.AddDbSource<AegisDb>(options =>
{
options.ConnectionString = ConfigManager.Get("PostgreConnection");
options.DataType = "PostgreSQL";
});
services.AddDbRepositories<AegisDb>();
多数据源
项目需要访问多个数据库时,定义多个 IDbSource 实现类,分别注册:
services.AddDbSource<AegisDb>(options =>
{
options.ConnectionString = ConfigManager.Get("PostgreConnection");
options.DataType = "PostgreSQL";
});
services.AddDbRepositories<AegisDb>();
services.AddDbSource<HisDb>(options =>
{
options.ConnectionString = ConfigManager.Get("HisConnection");
options.DataType = "SqlServer";
});
services.AddDbRepositories<HisDb>();
每个数据源有独立的 IFreeSql<TDbSource> 泛型实例,仓储通过泛型参数自动关联到对应数据源。
配置项
AddDbSource<TDbSource>() 接收一个 DbOptions 配置回调:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DataType | string | — | 数据库类型:PostgreSQL、SqlServer |
ConnectionString | string | — | 数据库连接字符串 |
UseAutoSyncStructure | bool | false | 自动同步实体结构到数据库(仅开发环境) |
UseMonitorCommand | bool | false | 输出执行 SQL 到控制台(本地调试用) |
DataType 支持的值
FreeSql 支持的数据库类型均可用,Aegis 项目中常用的有:
PostgreSQLSqlServer
注册 API 说明
AddDbSource<TDbSource>
注册 IFreeSql<TDbSource> 实例到 DI 容器。完整签名:
services.AddDbSource<TDbSource>(
Action<DbOptions> setupAction,
Action<IFreeSql<TDbSource>> freeSqlSetup = null,
IEnumerable<EventHandler<ConfigEntityPropertyEventArgs>> freeSqlHandlers = null,
EventHandler exceptionHandler = null);
| 参数 | 说明 |
|---|---|
setupAction | 数据库配置(连接字符串、类型等) |
freeSqlSetup | 对 IFreeSql 实例的额外配置(如 AOP 订阅) |
freeSqlHandlers | 实体属性配置处理器(如统一设置 decimal 精度) |
exceptionHandler | 增删改查异常回调 |
PostgreSQL Schema 配置与 search_path 冲突
在 PostgreSQL 下通过 freeSqlSetup 调用 ConfigEntity 给实体表名加上 schema 前缀(如 audit.audit_record)时,不要同时在连接串里设置 SearchPath,否则会出现建表 schema 和写入 schema 错位的 bug。
问题现象
配置 ConfigEntity(e => e.Name("audit.audit_record")) 且连接串带 Search Path=audit 时:
- 启动正常,
SyncStructure用限定名audit.audit_record建表,表正确创建在auditschema - 运行时
INSERT报错42P01: relation "audit_record" does not exist,审计写入持续失败 - 错误信息里是裸名
audit_record而非限定名audit.audit_record
根因
FreeSql 在 search_path 存在时,对带 schema 前缀的表名做了不一致的标准化处理:
- SyncStructure(DDL 路径):保留限定名,生成
CREATE TABLE audit.audit_record,表正确建到auditschema - Insert/Update/Delete(DML 路径):检测到连接串已设
Search Path=audit,认为表名不需要 schema 前缀,将限定名退化为裸名audit_record
裸名 audit_record 理论上应通过 search_path=audit 解析到 audit.audit_record,但由于 Npgsql 连接池的连接复用和 SET search_path 的设置时机问题,复用的连接上 search_path 可能已被重置为默认值,导致 PostgreSQL 在 public schema 下找不到表,抛出 42P01。
正确做法
二选一,不要混用:
方案一(推荐):表名用限定名,连接串不设 SearchPath
services.AddDbSource<AuditDb>(x =>
{
x.ConnectionString = connectionString; // 不追加 Search Path
x.DataType = "PostgreSQL";
}, freeSqlSetup: fsql =>
{
fsql.CodeFirst.ConfigEntity<AuditRecordEntity>(e =>
{
e.Name("audit.audit_record"); // 限定名,直接定位,绕过 search_path
});
});
限定名(schema.table)在 PostgreSQL 里完全绕过 search_path,建表和写入两端一致。EnsureSchemaExists 负责创建 schema,ConfigEntity 负责限定表名,职责清晰。
方案二:表名用裸名,连接串设 SearchPath
services.AddDbSource<BusinessDb>(x =>
{
x.ConnectionString = $"{connectionString};SearchPath=biz";
x.DataType = "PostgreSQL";
});
// 实体特性:[Table(Name = "orders")] 裸名,由 search_path 解析
这种方案下所有表名都是裸名,search_path 统一控制 schema。但需要确保目标 schema 已存在,且不与限定名表名混用于同一数据源。
注意
- FreeSql 表名映射优先级(从小到大):实体类名 <
Aop.ConfigEntity<CodeFirst.ConfigEntity(FluentApi)<[Table]特性 <AsTable。如果实体上有[Table(Name = "xxx")]特性,ConfigEntity的覆写可能被特性压过(取决于 FreeSql 版本,v3.2.660+ 可通过UseMappingPriority调整)。需要动态配置 schema 时,确保实体特性不带Name或优先级配置正确。 - SQL Server 不受
search_path影响,schema 通过dbo.table限定名或数据库默认 schema 解析,不存在此问题。
PostgreSQL 全局统一设置 Schema
当一个数据源下的所有表都需要放在同一个 schema 时,不需要逐个实体配置,可以用 Aop.ConfigEntity 全局拦截器批量处理。这是 FreeSql 官方推荐的全局 schema 设置方式。
全局拦截器(推荐)
在 AddDbSource 的 freeSqlSetup 里订阅 ConfigEntity 事件,给所有没有显式 schema 前缀的表名自动加前缀:
services.AddDbSource<BusinessDb>(x =>
{
x.ConnectionString = connectionString; // 不设 SearchPath,用限定名定位表
x.DataType = "PostgreSQL";
}, freeSqlSetup: fsql =>
{
var defaultSchema = "biz";
fsql.Aop.ConfigEntity += (s, e) =>
{
// 只给没有 schema 前缀的表名加前缀,避免重复处理已配置的限定名
if (!e.ModifyResult.Name.Contains("."))
{
e.ModifyResult.Name = $"{defaultSchema}.{e.ModifyResult.Name}";
}
};
});
工作原理:Aop.ConfigEntity 在每个实体的元数据首次解析时触发一次。e.ModifyResult.Name 此时是当前解析出的表名(来自类名或 [Table] 特性),拦截器可以统一改写它。!Contains(".") 判断保护了已经带 schema 前缀的表名不会被重复加前缀。
多 schema 精细化控制
如果不同实体需要放在不同 schema(如订单表在 orders schema、用户表在 users schema),可以定义自定义特性配合 AOP 解析:
// 1. 定义自定义特性
[AttributeUsage(AttributeTargets.Class)]
public class BizSchemaAttribute : Attribute
{
public string Schema { get; set; }
public string Table { get; set; }
}
// 2. 实体上标注
[BizSchema(Schema = "orders", Table = "order_header")]
public class OrderHeaderEntity { ... }
// 3. AOP 里按特性解析
fsql.Aop.ConfigEntity += (s, e) =>
{
var attr = e.EntityType.GetCustomAttribute<BizSchemaAttribute>();
if (attr != null)
e.ModifyResult.Name = $"{attr.Schema}.{attr.Table}";
};
实体特性直接写 schema 前缀
如果表数量不多且 schema 固定不变,直接在 [Table] 特性里写限定名最简单稳定(Aegis 的 Jobs 模块就是这种方式),完全不受 AOP 优先级规则影响:
[Table(Name = "aegis_jobs.job_item")]
public class JobItemEntity { ... }
[Table(Name = "aegis_jobs.job_log")]
public class JobLogEntity { ... }
优先级陷阱
Aop.ConfigEntity 的优先级低于 [Table] 特性。如果实体上已有 [Table(Name = "orders")] 这种只指定了表名没指定 schema 的特性,全局拦截器的覆写可能被特性压过。解决方案:
- 全局拦截器方案:确保实体特性不写
Name,或写带 schema 的限定名 - 升级到 FreeSql v3.2.660+ 并使用
UseMappingPriority调整优先级,让 Aop 高于 Attribute:
var fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.PostgreSQL, connectionString)
.UseMappingPriority(
MappingPriorityType.FluentApi,
MappingPriorityType.Attribute,
MappingPriorityType.Aop) // Aop 优先级最高
.Build<BusinessDb>();
注意:
UseMappingPriority需要在Build()之前调用,而 Aegis 的AddDbSource内部封装了Build过程。如果需要调整优先级,当前需绕过AddDbSource直接使用FreeSqlBuilder,或扩展AddDbSource支持FreeSqlBuilder配置回调。
AddDbRepositories<TDbSource>
扫描 TDbSource 所在程序集,注册所有继承自 BaseRepository 的仓储类到 DI 容器。
统一 decimal 精度
框架提供了 NewEvent() 辅助方法,统一将 decimal 类型映射为 decimal(18,6):
services.AddDbSource<AegisDb>(options =>
{
options.ConnectionString = connectionString;
options.DataType = "PostgreSQL";
}, null, new[] { ServiceCollectionExtensions.NewEvent() });
实体层约定
IDbSource 定义
每个数据库对应一个实现 IDbSource 的类:
public class AegisDb : IDbSource
{
public AegisDb(IFreeSql<AegisDb> sqlClient)
{
SqlClient = sqlClient;
}
public IFreeSql<AegisDb> SqlClient { get; }
}
这个类不需要写复杂逻辑,它给数据库连接一个明确的泛型标识。
Entity 常用标注
[Table(Name = "UserInfo")]
public class UserInfoEntity
{
[Column(IsPrimary = true)]
public long UserSeq { get; set; }
public string UserCode { get; set; }
public string UserName { get; set; }
// 创建时间:插入时自动赋值,更新时不覆盖
[Column(ServerTime = DateTimeKind.Local, CanUpdate = false)]
public DateTime CreateTime { get; set; }
// 更新时间:更新时自动刷新
[Column(ServerTime = DateTimeKind.Local)]
public DateTime UpdateTime { get; set; }
// 默认值:插入时使用 SQL 表达式赋值
[Column(InsertValueSql = "'0'")]
public bool IsDeleted { get; set; }
}
软删除
实现 IDeletedEntity 接口的实体会被框架自动过滤已删除数据:
public class UserInfoEntity : IDeletedEntity
{
[Column(IsPrimary = true)]
public long UserSeq { get; set; }
public bool Deleted { get; set; }
}
在仓储中临时关闭软删除过滤:
public List<UserInfoEntity> GetAllWithDeleted()
{
using (this.DisableDeleted())
{
return this.ToList();
}
}
框架扩展的 PagedList
Aegis 在 FreeSql 的 ISelect<T> 上扩展了 ToPagedList / ToPagedListAsync 方法,返回 PagedList<T> 结构体。
支持单表和多表联查(最多 8 张表),详见 分页与列表数据。
配套阅读
- Repository 层 — 数据访问层的目录结构和写法
- 事务 — UnitOfWork 和事务最佳实践
- 分页与列表数据 — 分页查询和 Dto 转换
- FreeSql 官方文档