跳到主要内容
版本:3.1

FreeSql 数据访问(Aegis.Core.FreeSql)

解决什么问题

Aegis.Core.FreeSql 负责数据库连接初始化、IFreeSql 实例注册和仓储自动扫描。它封装了 FreeSql ORM 的启动配置,让项目不需要手动构建 FreeSqlBuilder

版本一致性

Aegis 框架当前基于 FreeSql 3.5.303。业务项目中如果需要直接引用 FreeSql 的扩展包(如数据库提供程序、FreeSql.DbContext 等),版本号必须与框架保持一致。版本不一致会导致运行时类型冲突或方法找不到。

需要保持一致的包:

包名版本
FreeSql3.5.303
FreeSql.Repository3.5.303
FreeSql.Provider.PostgreSQL3.5.303
FreeSql.Provider.SqlServer3.5.303
FreeSql.DbContext3.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 配置回调:

配置项类型默认值说明
DataTypestring数据库类型:PostgreSQLSqlServer
ConnectionStringstring数据库连接字符串
UseAutoSyncStructureboolfalse自动同步实体结构到数据库(仅开发环境)
UseMonitorCommandboolfalse输出执行 SQL 到控制台(本地调试用)

DataType 支持的值

FreeSql 支持的数据库类型均可用,Aegis 项目中常用的有:

  • PostgreSQL
  • SqlServer

注册 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数据库配置(连接字符串、类型等)
freeSqlSetupIFreeSql 实例的额外配置(如 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 建表,表正确创建在 audit schema
  • 运行时 INSERT 报错 42P01: relation "audit_record" does not exist,审计写入持续失败
  • 错误信息里是裸名 audit_record 而非限定名 audit.audit_record

根因

FreeSql 在 search_path 存在时,对带 schema 前缀的表名做了不一致的标准化处理

  • SyncStructure(DDL 路径):保留限定名,生成 CREATE TABLE audit.audit_record,表正确建到 audit schema
  • 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 设置方式。

全局拦截器(推荐)

AddDbSourcefreeSqlSetup 里订阅 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 张表),详见 分页与列表数据

配套阅读