深入 Hibernate 02:注解映射在启动时接受什么检查
实验材料状态:本仓库尚未包含
examples/hibernate-lab/工程及原始证据。下文命令和输出来自云端文稿记录,待补交原文件后复核,当前不能据此认定实验已验收;运行命令暂不可在本仓库复跑。
错列名不该等第一笔订单暴露
创建订单接口引用了一个叫 does_not_exist 的数据库列,而真实 purchase_order 表没有该列。若应用启动只登记了 Java 注解却从未核查数据库,错误会延迟到写入或查询时才显露;订单服务的第一笔请求便充当了上线校验。第 00 篇建立的单表模型和第 01 篇的对象身份实验仍然适用。本篇只换启动映射:正确的 PurchaseOrder 可以建工厂,故障映射则指向不存在的列。
实验要守住两个分开的边界:有效映射可以建立工厂;无效映射在还未打开业务 Session 时就失败,独立连接读回订单行数不变。为区分失败阶段,还对比了两种错误:关联的 Java 目标不是实体,以及数据库缺少声明的列。它不试图从启动成功推断所有业务 SQL 都有效,也不把自动建表当作迁移方案。
服务注册、启动模型和运行时工厂
教学工程用 StandardServiceRegistryBuilder 注入 JDBC URL、账号、密码和 hibernate.hbm2ddl.auto=validate;MetadataSources.addAnnotatedClass(...) 注册实体类;buildMetadata().buildSessionFactory() 完成模型到可运行工厂的构建。这里有三种不同的责任:Jakarta Persistence 的 @Entity、@Id、@Column 给出映射声明;Hibernate 解释声明并建立运行时模型;数据库提供实际表与列,供模式验证查询。JDBC 承担元数据和操作的驱动接口,Spring 尚未参与。
冻结 commit ca7715d7c1afbd46d518752115848bffd9322413 中,StandardServiceRegistryBuilder.build() 处理配置并建立服务注册;MetadataSources.buildMetadata() 实际进入 metadata builder,不等于此时已有 SessionFactory。MetadataImpl.buildSessionFactory() 再调用 builder;SessionFactoryBuilderImpl.build() 创建运行时工厂。
validate 的数据库核对并非 addAnnotatedClass() 的副作用。该版本在 builder 中安装 SessionFactoryObserverForSchemaExport;工厂创建时 observer 的 sessionFactoryCreated() 进入 schema 工具。目标列在数据库元信息中不存在时,AbstractSchemaValidator.validateTable() 抛出 SchemaManagementException;如果列存在还要进行列类型校验。本例在缺失列分支中止:没有机会构造业务 Session,数据库也没有因为验证失败而新增订单行。
关联错误更早。测试实体 BrokenAssociationOrder 的 @ManyToOne 指向普通 Java 类 NotAnEntity,而非 @Entity。冻结源码的 ToOneFkSecondPass.doSecondPass() 在元数据第二遍处理时查找目标实体映射;查不到目标就按注解来源抛出 AnnotationException。此时无需借数据库元信息推断 customer_id 是否存在,也没有进入工厂创建后的 validate。这和“列写错但实体本身能构建元数据”是两个失败阶段;将它们统称“数据库启动报错”会丢掉修复线索。
相关上游 IndividuallySchemaValidatorImplTest 验证了“模型列不存在时抛出 Schema-validation: missing column”的检查行为。上游测试显式调用 schema validator,本章则通过 SessionFactory 的 observer 触发,二者不能被表述为完全相同的启动路径。只有同时阅读调用入口和反例,才能解释失败发生的时点。
用故障映射限定启动承诺
flowchart TD
C["配置 JDBC 与 validate"] --> R["StandardServiceRegistry:服务注册"]
R --> S["MetadataSources:登记注解实体"]
S --> M["buildMetadata:建立启动映射模型"]
M --> A{"关联目标是否有实体映射?"}
A -- 否 --> E1["AnnotationException:元数据构建失败"]
A -- 是 --> B["buildSessionFactory:构造运行时工厂"]
B --> O["创建期间的 schema observer"]
O --> V["validate:经 JDBC 读取表列元数据"]
V --> T{"列是否存在且类型匹配?"}
T -- 否 --> E2["SchemaManagementException:工厂启动失败"]
T -- 是 --> F["返回可用 SessionFactory"]
F --> U["打开业务 Session"]
图中只列本篇两种故障经过的检查,不是全部启动校验清单。服务注册保存配置与服务,启动模型保存实体映射;运行时工厂在构造期间触发数据库验证,通过后才返回给应用。非实体关联在启动模型阶段失败,缺失列在工厂创建期间失败,两者都早于业务 Session。validate 不修改表,也不承诺数据库列的可空性、索引或所有业务约束都与注解一致。
实验中的错误实体 BrokenPurchaseOrder 与 BrokenAssociationOrder 只存在于 Chapter02Test,不是另一套订单业务实现。前者对应同一张 purchase_order 表,却声明了真实表没有的 @Column(name = "does_not_exist");后者声明的关联目标不是实体。正常服务依旧使用 PurchaseOrder。工厂设置 validate,不用 create、update 来掩盖错列。失败后,LabDatabase 销毁未成功启动的服务注册,防止连接或资源在测试进程内泄漏。
在 PostgreSQL 16.15、Hibernate ORM 7.1.36.Final 和 JDK 21.0.12.1 下,执行 ./mvnw -B -ntp '-Dtest=Chapter00Test,Chapter01Test,Chapter02Test,Chapter03Test' test,这轮六项测试通过、零跳过;原始命令、退出码、SQL 与环境见 examples/hibernate-lab/evidence/02/20261002T045151Z/。关键输出保留异常类型和文字,而不是仅用“启动失败”概括:
1 | |
19 是该专用实验库在执行本测试时已有的订单总数,包含此前章节多轮测试的已提交行,不能当作本篇新增订单数。新连接再次查询未见新增行;也不能反过来认为任意启动失败都会保持所有外部资源不变,实验只覆盖两种配置故障。Chapter02Test 分别断言缺失列与非实体关联的异常信息、前后行数,并在正常工厂路径检查 isOpen();单纯看 PostgreSQL SQL 日志里没有 INSERT 不能取代这些断言。
练习与验收边界
手算反例。 真实表有 id 和 amount,实体另外声明了 @Column(name="absent")。如果依次调用 addAnnotatedClass()、buildMetadata()、buildSessionFactory(),哪一步按本文配置需要读取数据库表列并抛错?答案是构建工厂触发 schema observer 的 validate 时;不应说“Java 编译不通过”,也不应说“第一笔 INSERT 才失败”。去掉 validate 会改变检测时点,但不会凭空创建这一列。
改动练习。 把故障映射中的列名改成真实存在的 status,重新运行第 02 篇测试。原先的 assertThrows(SchemaManagementException.class, ...) 将不再观察到相同的缺失列异常;还要注意声明的 Java 类型、列类型与可空性是否兼容。修复练习不是删掉断言,而是让测试明确区分“正确映射应启动”和“故障映射应拒绝”,然后恢复故障映射以供下一位读者复跑。
本文只证明两种错误映射在启动阶段被拒绝,以及正常模型通过当前 PostgreSQL 表的验证;不证明运行时表没有被其他进程修改,也不证明所有关联已在生产负载下执行。后续操作语义、类型与标识符边界仍需在同一个累计工程里继续验证。
参考资料与实验入口
- Jakarta Persistence 3.2 规范:实体注解与规范边界。
- Hibernate ORM 7.1 User Guide:模式管理;系列指南会滚动更新,具体执行路径以上述固定 SHA 为准。
- 教学测试:
examples/hibernate-lab/src/test/java/blog/hibernate/Chapter02Test.java;原始实验:examples/hibernate-lab/evidence/02/20261002T045151Z/。错误实体只用于测试,不能把它当成 Hibernate 上游内部实现。
