库表校验(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] 是声明给工具链的拓扑关系:/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 允许的,不收进矩阵)。
矩阵三条规则:
| 规则 | 库列类型 | 实体属性类型 | 判定 |
|---|---|---|---|
| 1 | timestamp、timestamptz、date、time、timetz、interval、int2、int4、int8、numeric、float4、float8、bool、uuid、bytea、inet(16 种) | string | 不一致 |
| 2 | text、varchar、char、name | bool、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 的配置回调里,随数据源一次性配置:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
IgnoredEntities | HashSet<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 或迁移管,这里只提示,不判失败。
配套阅读
- FreeSql 数据访问(Aegis.Core.FreeSql) — 父组件:数据源注册、多数据源、schema 配置
- 诊断组件(Aegis.Diagnostics) —
/diag页面、健康矩阵、一键诊断机制