跳到主要内容
版本:3.1

库表校验(Aegis.Core.FreeSql.SchemaValidation)

库表校验把实体属性声明的类型和数据库列的真实类型逐一比对,结果展示在 /diag 诊断页面上,按需触发。它抓的是一类安静的事故:实体用 string 接了库里实际是 timestamp 的列,查询不报错、数据也返回,但 FreeSql 每读一行这一列都要抛一次 InvalidCastException 再走弱类型兜底转换,慢,而且兜底转换给出的值可能和强类型读取不一样。

本功能是 Aegis.Core.FreeSql 包内的子能力,随 AddDbSource 注册自动可用,不装新包。当前仅支持 PostgreSQL 数据源。

页面来自诊断组件

/diag 页面由诊断组件(Aegis.Diagnostics)提供(Component.deps.json 声明 Diagnostics)。未装配诊断组件时,本功能的三个注册是无人消费的惰性单例,零开销;库表校验本身也没有任何启动行为,不点按钮就是零成本。

组件概览​

字段说明
组件名称库表校验
真实类库Aegis.Core.FreeSql(父包内子能力,无独立 NuGet 包)
最新版本3.1.4-preview3(随父包发布)
组件定位实体归属声明([DbSource])+ /diag 按需的实体-库表类型比对
引入方式引用 Aegis.Core.FreeSql 并调用 AddDbSource,自动注册,无额外步骤
核心能力[DbSource] 归属声明、一键诊断、逐列比对表、白名单降级、导出 JSON
主要配置DbOptions.SchemaValidation(当前只有 IgnoredEntities 白名单)
典型配套FreeSql 数据访问、诊断组件

什么时候要用它​

适合场景:

  • 库里有视图、有别的系统建的表、有迁移过来的历史表,实体是照文档或记忆写的,没人逐列核过类型
  • 接手存量项目,想知道哪些实体和库对不上,先摸个底
  • 改表或换库之后,确认实体声明仍然成立
  • 排查"查询能出数据但特别慢"的现场,看是不是逐行异常兜底在拖

不适合场景:

  • 想在启动时自动校验并阻断启动。本功能只在 /diag 按需触发,无任何启动行为
  • 非 PostgreSQL 数据源。当前仅支持 PostgreSQL,其他库类型显示"未验证 · 该库类型暂不支持"
  • 管表结构建没建、该不该改。列在库里不存在时按"未验证"展示,不判失败,建表归 AutoSyncStructure 和迁移管

最小可运行路径​

以仓库 BaseDemo 为准,三步。

第一步:AddDbSource 照常注册​

不需要为库表校验加任何配置,AddDbSource 内部已把三个诊断接口指向同一个 provider:

services.AddDbSource<AegisDb>(x =>
{
x.ConnectionString = ConfigManager.Get("PostgreConnection");
x.DataType = "PostgreSQL";

// 白名单(可选):故意全 string 接的视图实体,登记后降级为提示级
// x.SchemaValidation.IgnoredEntities.Add("Aegis.Webapi.Repository.Entities.DemoMismatchEntity");
}, null, Array.Empty<EventHandler<ConfigEntityPropertyEventArgs>>());

第二步:打开 /diag,点「库表校验 · AegisDb」的一键诊断​

每个 AddDbSource 的数据源各有一段「库表校验 · 数据源标记类名」。BaseDemo 注册了两个源,页面上就有「库表校验 · AegisDb」和「库表校验 · LocalAegisDb」两段。没点过诊断时段里显示"未运行",点过之后变成上次结果摘要。段头只留一个跳转链接——校验与导出的操作入口都在「库表校验」专属 Tab 的比对表卡头。

按钮触发后跑五个步骤:实体扫描(声明绑定多少、全扫多少、按声明跳过多少)、绑定检查(悬空声明在这一步报出)、库表结构(命中表数、比对列数)、逐项不一致与提示(至多展示 20 条,超出部分指到比对表)、比对结论。

服务器上没有浏览器时,同一入口可用 curl 触发:

curl -s -G "http://localhost:5000/diag" --data-urlencode "diagnose=库表校验 · AegisDb"

第三步:看「库表校验」Tab 的比对表​

「库表校验」专属 Tab 里每个源一张卡,卡名是「源名 · 库名」,卡头有「校验」和「导出 JSON」两个按钮。「导出 JSON」走 ?schemacompare=库表校验 结构化端点,下载 aegis-库表校验-年月日-时分秒.json——面向编程消费(把 JSON 交给 AI 生成建表/迁移脚本,或喂给迁移工具),不是页面表格的字符串形态。载荷结构:sources[] 每源含 state/coverage/issues/tables/missingTables;tables[] 按表分组、每列带 property/propertyType/column/dbType/verdict/suggestedType;missingTables[] 是声明绑定本源但库中无表的实体,带 declaredColumns 全列声明(属性/CLR 类型/映射列名)——AI 据此可直接生成 CREATE TABLE;notes[] 是给 AI 消费者的语义说明(未验证不是故障、未声明实体不建表等),防止生成脚本时误读。

卡内按实体分组、左右对照展示:每个实体一个可折叠组块,组头是「实体名 → schema.表名」加该实体最差判定的徽标;组内每列一行,左边是实体声明(属性 · 类型),右边是数据库实际(列 · 库类型),中间是对照符(✓ 匹配 / ✕ 不一致 / ◆ 提示 / ? 未验证),行尾是判定徽标,不一致的行整行红底并附建议类型(如"建议 DateTime?")。有问题的实体默认展开、全部匹配的默认收起成一行"N 列全部匹配";"表不存在"的实体合并成一个汇总行(如"12 个实体在库中无表"),点开看逐个清单。组徽标与组序分层:红(不一致/列名不符)> 黄(提示、表不存在、列不存在)> 绿——"表不存在/列不存在"是"声明了却在库里没有"的实事,黄级;灰级(未比对/未验证)是"没证据"不是"有问题",不拖累组色:绿+灰的组仍亮绿并计数(如"15 匹配 · 1 未比对");只有红灯才让组变红。组序同层:红组在顶、提示次之、表不存在汇总桶(黄)再次、绿组沉底。

BaseDemo 里有完整的演示现场。Startup 启动时幂等自建一张错配表:

fsql.Ado.ExecuteNonQuery(
"CREATE TABLE IF NOT EXISTS demo_mismatch(id int4 primary key, bornat timestamp, createdat timestamp)");

对应实体故意把 BornAt 声明成 string:

[Table(Name = "demo_mismatch")]
[DbSource(typeof(AegisDb))]
public class DemoMismatchEntity
{
[Column(IsPrimary = true)] public int Id { get; set; }

/// <summary>故意声明错:库里是 timestamp——ISelect 逐行读会抛 InvalidCastException 再走兜底转换</summary>
public string BornAt { get; set; } = "";

/// <summary>正确类型对照组(此列校验为"匹配")</summary>
public DateTime CreatedAt { get; set; }
}

点一键诊断,BornAt 这一行被判"不一致",建议改成 DateTime?;CreatedAt 判"匹配"。把 BornAt 改回 DateTime? 再点一次诊断,结果变绿,修复回环走完。

/diag 页面上的三处出口​

出口位置内容
信息段Database 分组,紧跟「数据库 · 源名」段状态(未运行 / 通过 / 发现 N 处不一致 / 未验证原因)、上次校验时间与耗时、覆盖统计、悬空绑定明细、前 5 条问题
校验「库表校验」Tab 卡头的「校验」按钮现场执行一次校验,结果登记进进程内登记表,段与比对表随之下次刷新
比对表「库表校验」Tab每源一张实体分组左右对照卡,卡头另有「导出 JSON」动作

信息段的"覆盖"一行长这样:实体 42(声明绑定 10 / 未声明全扫 30 / 按声明跳过 2)· 命中表 38 · 比对列 411(MapType 未比对 5)。多环境连错库的问题靠表名里的库名显形,比对表表名始终带库名。

判定列有徽标配色:不一致红、提示黄、匹配绿,表不存在/未验证/未比对灰。

实体归属声明 [DbSource]​

[DbSource] 声明"这个实体归哪个数据源管",名字取数据源标记类的短名(就是 AddDbSource<TDbSource> 里那个 TDbSource 的类名)。两种形态、可重复、比对忽略大小写:

// 类型形态(BaseDemo 的 DemoMismatchEntity):编译期可查,优先用
[DbSource(typeof(AegisDb))]

// 名字形态(BaseDemo 的 CodeTemplateEntity):实体在独立工程、引用不到标记类型时用
[DbSource("AegisDb")]

一个实体被多个源共用时写多行,逐个声明。

对某个数据源来说,实体的去处由下表决定:

实体情况在该源的校验面里说明
声明了该源参与;库里没有同名表时给出"表不存在"行显式声明压过框架程序集排除
只声明了其他源不参与,计入"按声明跳过"该实体由它声明的源去校验
没有任何 [DbSource]参与全扫兜底:该源库里有同名表就照常比对忘挂不假安全。但 Aegis. 前缀框架程序集里的未声明实体除外(Jobs、Audit 等框架自管表)
声明的名字对不上任何已注册源(悬空)视同未声明,回落全扫兜底绑定检查步骤会报出,并给编辑距离最近(不超过 3)的源名建议

悬空多半是手滑拼错名字,比如把 AegisDb 写成 Aegisdb 不会悬空(忽略大小写),写成 AegisDB1 就会报「最接近:AegisDb」。

[DbSource] 不改变运行时行为

[DbSource] 是声明给工具链的拓扑关系:/diag 库表校验按它收窄校验面,后续迁移、SyncStructure 圈范围也按它来。FreeSql 运行时不据此做任何限制,任何数据源仍然可以读写任何实体,注入哪个 IFreeSql<T> 就连哪个库。挂错了特性不会拦住错误的跨库访问,运行期的数据源纪律要靠代码评审和仓储分层保证。

判定语义​

"匹配"只授予实证放行组合表内的组合(库类型×属性类型逐格实测不抛——组合粒度而非属性粒度:"属性类型在白名单"曾让 jsonb×int、integer[]×int、varchar×Guid 等实测必抛的组合误亮绿)。常见组合(文本族×string、时间族×DateTime、整数族×对应整数、json/jsonb×string、uuid×Guid、bytea/text×byte[]、数值宽化如 int4→long)均在表内;表外组合一律"未验证"。版本分歧格(如 date×int:Npgsql 8 不抛、FreeSql 3.5.303 捆绑的 Npgsql 5 抛)两边都不收,落"未验证"。MapType 列不再跳过:FreeSql 真实读取第一段是 GetFieldValue<MapType>(过 Npgsql),矩阵对"列类型×MapType"完全适用——MapType 列按 MapType 代入矩阵判定,issue 的属性类型带"(经 MapType)"标注;第二段(MapType→属性 Convert)由编译期类型检查保证。枚举×整数族经 FreeSql 真实读写回路实证判"匹配";date/time 族、xml、数组族(integer[]×int[] 等 11 格)已补实证放行。"列名不符"是红级判定:属性名与库列名对不上时 FreeSql 引号寻址寻不到,查询直接报列不存在——实测裸枚举的 FreeSql 合成配置不触发 MapType 跳过,直接判匹配属于无证据的绿标。自定义类属性 FreeSql 默认不映射,不会出现在比对行里。

判定只标"强类型读取必然抛 InvalidCastException"的组合。强类型读取指 FreeSql ISelect 编译的读取器按实体属性类型直接取列值(GetFieldValue<T>)。这个口径内建为一张 PostgreSQL(Npgsql 类型处理器)兼容矩阵,54 个必抛格全部对真实 PG 逐格实证过,另有 13 个刻意放行的组合做对照(比如 json、jsonb、xml 列读成 string 是 Npgsql 允许的,不收进矩阵)。

矩阵三条规则:

规则库列类型实体属性类型判定
1timestamp、timestamptz、date、time、timetz、interval、int2、int4、int8、numeric、float4、float8、bool、uuid、bytea、inet(16 种)string不一致
2text、varchar、char、namebool、DateTime、decimal、double、float、short、int、long不一致
3数值族(int2、int4、int8、numeric、float4、float8)DateTime不一致

规则 1 就是 v_employee 事故的形态:视图列是 timestamp、numeric,实体属性是 string,查询不炸但每行每列抛一次异常走兜底。

其余判定词:

判定含义
匹配组合在矩阵放行面内,或同族正常读取
未识别类型库列是 PG 扩展/domain/自定义类型(citext、geometry、业务 domain)——不认识就不判定,行内带类型名
组合未实证类型双方都认识但组合不在实证放行表——行内带组合名(如 date × TimeSpan)
列不存在实体声明了该属性但库里没有此列——结构缺失是实事(黄级,同表不存在层),建列属 UseAutoSyncStructure/迁移范围,不做类型判定、不产生 issue
未比对属性挂了 MapType(类型转换器),读写都经转换,直接比对会误报,跳过并计入统计。[Column(IsIgnore = true)] 的列不进数据库,不出现行
提示命中必抛组合,但实体登记在白名单里,降级展示
表不存在声明绑定本源的实体,库里没有同名表。表名比对忽略大小写

一条不一致的完整文案(EntitySchemaIssue.BuildMessage 的输出):

实体 DemoMismatchEntity 的属性 BornAt(string)对应库 aegis_demo 里 public.demo_mismatch 的列 bornat(数据库类型 timestamp without time zone)。用 ISelect 查询时每读一行这一列都会抛一次 InvalidCastException 再走兜底转换:数据能返回,但速度慢,且兜底转换可能与强类型读取给出不一样的值。改法:把属性类型改成 DateTime?,或确认列实际类型后修正实体。若这张表故意全用 string 接,在 AddDbSource 的 SchemaValidation.IgnoredEntities 登记该实体。

两条实现口径,判读结果时用得上:

  • 列名寻址:与 FreeSql 行为逐字一致——引号精确寻址、区分大小写(实测 FreeSql 生成 DELETE FROM "T" WHERE ("Id" = 1))。[Column(Name)] 覆写优先,缺省用属性名:属性 BornAt 寻址的是列 "BornAt",库里未加引号建的小写列 bornat 会被它寻址不到(查询直接报 42703)。因此属性名与库列名大小写/拼写不同会判"列名不符"(红级,比类型不匹配更硬:整条查询失败),文案给 [Column(Name)] 对齐建议。应用开 UseNameConvert(如 PascalCase→snake_case)时 FreeSql 把转换列名合成进实体配置,同一解析链自动生效
  • 结构查询走 pg_catalog(覆盖表、分区表、普通视图、物化视图),不走 information_schema.columns——后者有权限过滤和大小写陷阱,实测可能返回空。v_employee 事故里不匹配的列就在视图上
  • schema 取舍顺序与 PG/Npgsql 实际寻址一致:① [Table(Name = "dbo.xxx")] 前缀声明的 schema(表确实在该 schema 时);② 连接串 SearchPath(如 SearchPath=his,public)逐个解析——连接串指定了搜索路径而实体没写前缀时,真实表在搜索路径的 schema 里,固定回落 public 会比错同名表;③ public(PG 默认搜索路径兜底);④ 任一命中(保证不因选 schema 失败误标整源不可达)。迁移遗产前缀找不到目标表时也不会把整源误标成库不可达

配置项说明(DbOptions.SchemaValidation)​

挂在 AddDbSource 的配置回调里,随数据源一次性配置:

配置项类型默认值说明
IgnoredEntitiesHashSet<string>,实体类型 FullName,忽略大小写空白名单。登记的实体即使命中必抛组合,也只降级为"提示"(步骤与比对表都标提示,不计入不一致项,不把结果判成 Failed)

白名单的用途:有些视图实体故意全用 string 接一切列。与其每次诊断对着满屏"不一致",不如登记成提示级,看得见,不算错。

这个配置类没有查询超时选项,也不该有。FreeSql 3.5.303 的 ADO 面没有可设超时的入口,强加一个选项就是一个不生效的旋钮;兜底由 /diag 一键诊断端点的 15 秒整体超时负责,到点返回 504。

已知边界​

边界说明
仅 PostgreSQL其他库类型点一键诊断返回"库类型检查 unavailable(仅支持 PostgreSQL)",信息段显示"未验证 · 该库类型暂不支持",段和 Tab 照常出现
无查询超时选项见上节。端点 15 秒整体兜底
升级驱动须重跑矩阵回归FreeSql 或 Npgsql 升版本后,PgCompatMatrixFactTests 必须重跑:设 AEGIS_FREESQL_RUN_POSTGRES_TESTS=true(连接串可用 AEGIS_FREESQL_POSTGRES_CONNECTION 覆盖,默认本机 15432)。必抛格实测不抛就从矩阵删格,宁少报不误报
结果是进程内登记重启后清零回到"未运行",不落盘
库不可达不判失败标"未验证 · 库不可达"加异常摘要,连接类问题归网络诊断与健康探针管

接入后怎么确认生效​

  • /diag 页面 Database 分组出现「库表校验 · 源名」段,每个 AddDbSource 数据源一段,未运行时段头有跳转「库表校验」Tab 的链接
  • 点一键诊断,五个步骤齐全:实体扫描带实体计数、绑定检查、库表结构带命中表与比对列计数、逐项、比对结论
  • 「库表校验」Tab 出现比对表,表名「源名 · 库名」,五列齐全,问题行在顶
  • BaseDemo 现场:demo_mismatch 的 BornAt(string 对 timestamp)被抓成"不一致"且建议 DateTime?;改成 DateTime? 后再点诊断变绿
  • 比对表卡头「导出 JSON」能下载带时间戳的 JSON 文件

常见问题​

忘挂 [DbSource] 会漏报吗​

不会。未声明的业务实体参与每个源的全扫兜底,该源库里有同名表就照常比对。两个例外要知道:声明了其他源的实体对本源跳过;Aegis. 前缀框架程序集里的未声明实体不进兜底(框架自管表自带 [Table] 且自己 SyncStructure)。业务工程按 Aegis. 开头命名的实体,靠显式 [DbSource] 声明进来。

声明写错名字了会怎样​

悬空绑定在"绑定检查"步骤和信息段里报出:实体 FullName、声明的名字、编辑距离最近(不超过 3)的已注册源名建议。该实体视同未声明,回落全扫兜底,不会被静默跳过。

「未验证」是有问题吗​

不是。未验证只表示没有实测证据:类型组合不在矩阵里,或列在库里不存在。判定只信对真实 PG 实证过的必抛格,没证据的宁可不标。

为什么有的列显示「未比对」​

该属性挂了 MapType,读写都经转换器,拿属性类型直接对库类型会误报。这些列计入"MapType 未比对"统计,不算问题。

非 PostgreSQL 数据源会有开销吗​

没有启动行为,不点按钮零成本。点一键诊断返回一步 unavailable,信息段显示"未验证 · 该库类型暂不支持"。

校验结果重启后还在吗​

不在。结果是进程内静态登记,重启清零,回到"未运行"。

比对表里「表不存在」是什么意思​

该实体声明绑定本源,但库里没有同名表。多半是没建表、连错了库或表名拼写对不上。建表归 AutoSyncStructure 或迁移管,这里只提示,不判失败。

配套阅读​