《SpringBoot4进阶实战》第13招 数据库升级不能靠ddl-auto
# 数据库迁移:Entity 改字段为何不等于生产库已升级 ```text 旧方案:Hibernate ddl-auto 自动改表 → 生产库结构失控 → 数据丢失与发布事故 ``` 我是老李,在深圳做架构师,常年跟 Spring Boot 应用和 MySQL 打交道。 Petclinic 项目跑得好好的,可每次改个 Entity 字段,测试环境一切正常,一到生产就出幺蛾子。 Java Entity 改一个字段,为什么完全不能代表生产数据库已经安全升级? 选择 ddl-auto、schema.sql 还是 Flyway,哪种方式才能让 Schema 变更可控、可追溯、可回滚? 如果选错迁移方案,百万行数据的表升级失败,你拿什么恢复业务? # 01、故事 ```text 周五 17:50 → 深圳科兴科学园 3 栋 11 层 → 任务是给 Petclinic 的 owners 表加一个 email 字段 → 产品经理催着上线,测试环境一切正常,生产却报错 ``` 周五傍晚,我正准备收拾东西下班,产品经理小跑过来,说下周要给宠物主发营销邮件,owners 表必须加 email 字段。 任务是给 Petclinic 的 owners 表加一个 email 字段,改动不大,Entity 加个属性,数据库加一列,半小时搞定。 可我刚把代码提交,测试环境跑得飞起,生产数据库却直接抛异常,说 owners 表不存在 email 列。 我盯着屏幕,心里发毛:明明 Entity 改了,为什么生产库没跟上? > 代码改一行, > 数据库无响, > 生产报错急, > 谁在管 Schema? # 02、问题 ```text 需求变化:owners 表新增 email 字段 → 冲突:Entity 改了但生产库结构没变 → 后果:应用启动失败,查询报错,业务中断 ``` Petclinic 一直用 Hibernate 的 ddl-auto=update 管理表结构,开发时很方便,加个字段自动建列。 但生产环境我早就把 ddl-auto 设为 none,因为自动改表太危险,可能锁表、误删数据。 结果就是:代码里 Entity 加了 email,生产库还是老结构,应用一启动,Hibernate 校验失败,直接抛异常。 影响范围是全部依赖 owners 表的接口,比如查询宠物主列表、编辑宠物主信息,全部 500。 可验证的验收标准:应用启动时能自动执行数据库迁移,把 owners 表从 V1 升级到 V2,且迁移过程有日志、有版本记录。 > 开发一时爽, > 生产火葬场, > 结构无人管, > 升级全靠撞。 # 03、原理 ```text 核心机制:Schema 版本管理 = 把数据库结构变更当作代码,用版本化脚本顺序执行,记录当前版本,保证可重复、可审计 ``` 先看三种常见方式的职责边界。 Hibernate ddl-auto 是 ORM 层的自动建表工具,适合开发期快速原型,但生产环境必须关闭,因为它无法感知已有数据,可能删列、改类型导致数据丢失。 schema.sql 是 Spring Boot 启动时执行的 SQL 脚本,适合初始化表结构,但它是全量执行,没有版本概念,每次启动都跑一遍,无法增量升级。 Flyway 和 Liquibase 是专门的迁移工具,用版本号管理每个变更脚本,记录已执行版本,只执行未执行的脚本,保证幂等和可追溯。 反直觉判断:很多人以为 schema.sql 能替代 Flyway,其实 schema.sql 每次启动都执行,如果脚本里有 INSERT 语句,会导致重复数据; 而 Flyway 只执行一次,靠版本表记录。 另一个反直觉判断:数据库回滚通常比代码回滚复杂,因为代码回滚是替换文件,数据库回滚要处理数据变更,比如删了一列,回滚要恢复数据,可能涉及大量数据操作。 > 自动建表一时快, > 生产数据埋祸根, > 版本脚本管增量, > 迁移工具定乾坤。 # 04、架构 ```text 输入:Schema 变更需求(加字段、加约束、加索引) → 本模块:Flyway 迁移引擎 → 输出:数据库结构升级到目标版本,版本表记录历史 ``` 引入 Flyway 后,Petclinic 的数据库管理分三层。 第一层是应用代码层,Entity 定义 Java 对象,只负责 ORM 映射,不直接改表。 第二层是迁移脚本层,每个版本一个 SQL 文件,放在 src/main/resources/db/migration 下,Flyway 按版本号顺序执行。 第三层是数据库层,Flyway 创建 flyway_schema_history 表,记录每个脚本的执行状态,保证不重复执行。 边界很清晰:应用启动时,Flyway 先检查版本,执行未执行的脚本,然后 Hibernate 再启动,此时表结构已就绪,不会校验失败。 收益是 Schema 变更可审计、可回滚(通过编写反向脚本)、可协作(多个开发者各自加版本)。 代价是每个变更都要写 SQL 脚本,不能只改 Entity 就完事,增加了开发成本。 适用条件是团队有数据库变更规范,能接受 SQL 脚本作为变更的唯一入口。 > 三层各司其职, > 迁移脚本管版本, > 应用代码管映射, > 数据库稳定运行。 # 05、第一版实战 ```text 环境:Spring Boot 3.2 + MySQL 8 + Flyway 9.22 → 配置:添加依赖和配置 → 代码:编写 V1、V2、V3 迁移脚本 → 启动:应用启动时自动执行 → 验证:查询版本表和表结构 ``` 先看当前 Petclinic 的 pom.xml,添加 Flyway 依赖。 ```xml org.flywaydb flyway-core 9.22.3 org.flywaydb flyway-mysql 9.22.3 ``` 在 application.properties 中配置 Flyway,并关闭 Hibernate 的 ddl-auto。 ```properties spring.jpa.hibernate.ddl-auto=none spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true ``` baseline-on-migrate=true 表示如果数据库已有表但没有版本表,Flyway 会创建基线,避免报错。 现在创建第一个迁移脚本 V1__init_schema.sql,初始化 Petclinic 的核心表。 ```sql -- V1__init_schema.sql CREATE TABLE owners ( id INT AUTO_INCREMENT PRIMARY KEY, first_name VARCHAR(30) NOT NULL, last_name VARCHAR(30) NOT NULL, address VARCHAR(255), city VARCHAR(80), telephone VARCHAR(20) ); CREATE TABLE pets ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, birth_date DATE, owner_id INT NOT NULL, CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners(id) ); ``` V2 脚本添加 email 字段,并加唯一约束。 ```sql -- V2__add_email_to_owners.sql ALTER TABLE owners ADD COLUMN email VARCHAR(100); ALTER TABLE owners ADD CONSTRAINT uk_owners_email UNIQUE (email); ``` V3 脚本添加索引,优化按姓氏查询。 ```sql -- V3__add_index_on_owners_last_name.sql CREATE INDEX idx_owners_last_name ON owners(last_name); ``` 启动应用,观察日志。 ```text 示例输出: Flyway Community Edition 9.22.3 Successfully validated 3 migrations Current version of schema: 1 Migrating schema to version 2 Migrating schema to version 3 Successfully applied 2 migrations ``` 验证版本表。 ```sql SELECT version, description, success FROM flyway_schema_history; ``` ```text 示例输出: version | description | success 1 | init schema | 1 2 | add email to owners | 1 3 | add index on owners last | 1 ``` 验证表结构。 ```sql SHOW COLUMNS FROM owners; ``` ```text 示例输出: Field | Type | Null | Key | Default id | int | NO | PRI | NULL first_name | varchar(30) | NO | | NULL last_name | varchar(30) | NO | | NULL address | varchar(255) | YES | | NULL city | varchar(80) | YES | | NULL telephone | varchar(20) | YES | | NULL email | varchar(100) | YES | UNI | NULL ``` 成功标准:应用启动时自动执行迁移,owners 表新增 email 列和唯一约束,版本表记录 V1-V3。 未在当前环境实测,以上为预期结果,需按当前官方文档核验。 > 版本脚本写清楚, > 启动自动执行, > 表结构升级好, > 数据安全有保障。 # 06、排查 ```text 诊断树:迁移失败 → 检查版本表 → 检查 SQL 语法 → 检查权限 → 检查连接 ``` 问题一:应用启动报错,提示 Flyway 迁移失败。 现象:启动日志出现 ERROR,说 Migration V2 failed。 怀疑:SQL 语法错误或约束冲突。 检查:查看详细日志。 ```bash mvn spring-boot:run ``` ```text 示例输出: Migration V2__add_email_to_owners.sql failed SQL State : 23000 Error Code : 1062 Message : Duplicate entry 'john@example.com' for key 'owners.uk_owners_email' ``` 证据:owners 表已有重复 email 数据,导致唯一约束创建失败。 根因:生产数据不干净,直接加唯一约束会失败。 修复:先清理重复数据,再创建约束。 ```sql -- V2__add_email_to_owners.sql 修改版 ALTER TABLE owners ADD COLUMN email VARCHAR(100); UPDATE owners SET email = CONCAT('user', id, '@example.com') WHERE email IS NULL; ALTER TABLE owners ADD CONSTRAINT uk_owners_email UNIQUE (email); ``` 问题二:Flyway 校验失败,提示已应用的迁移被修改。 现象:启动报错,说 Migration checksum mismatch。 怀疑:有人改了已执行的 V1 脚本。 检查:查看版本表。 ```sql SELECT installed_rank, version, checksum FROM flyway_schema_history; ``` 证据:V1 的 checksum 与文件当前不一致。 根因:团队有人改了 V1 脚本,但 V1 已在生产执行过。 修复:禁止修改已执行的脚本,只能新增版本。如果必须改,用 Flyway repair 命令更新校验和。 ```bash mvn flyway:repair ``` **错误尝试:** 我一开始以为 Flyway 失败是数据库连接问题,反复检查连接池配置,浪费了半小时。后来看日志才发现是 SQL 约束冲突,教训是先看错误码和消息,别瞎猜。 > 迁移失败莫慌张, > 日志里面找真相, > 重复数据先清理, > 校验和变要 repair。 # 07、优化 ```text V1:直接加唯一约束 → 证据:生产数据有重复 → V2:先清理数据再加约束,并增加回滚脚本 ``` V1 的迁移脚本太天真,没考虑生产数据质量。 优化方案:在 V2 脚本中增加数据清理步骤,并编写 V2__rollback.sql 用于回滚。 修改后的 V2 脚本。 ```sql -- V2__add_email_to_owners.sql ALTER TABLE owners ADD COLUMN email VARCHAR(100); -- 清理重复数据,保留 id 最小的记录 UPDATE owners o1 JOIN owners o2 ON o1.email = o2.email AND o1.id > o2.id SET o1.email = CONCAT('user', o1.id, '@example.com'); ALTER TABLE owners ADD CONSTRAINT uk_owners_email UNIQUE (email); ``` 同时创建回滚脚本,放在 db/rollback 目录。 ```sql -- V2__rollback.sql ALTER TABLE owners DROP INDEX uk_owners_email; ALTER TABLE owners DROP COLUMN email; ``` Flyway 本身不支持自动回滚,但我们可以通过编写反向脚本,在需要时手动执行。 优化后,迁移脚本更健壮,能处理脏数据,且具备回滚能力。 > 数据质量先保证, > 约束才能建得稳, > 回滚脚本备一手, > 生产升级不慌张。 # 08、演进 ```text V2:Flyway 管理 Schema → 能力:版本化迁移、失败回滚、数据清理 → 遗留:百万数据表的兼容升级策略 ``` 引入 Flyway 后,Petclinic 的 Schema 变更有了版本控制,每次发布都自动执行迁移。 收益是开发、测试、生产环境结构一致,不再出现 Entity 改了但库没改的问题。 代价是每个变更都要写 SQL,且要维护回滚脚本,增加了工作量。 适用场景是任何需要长期演进的数据库应用,尤其是多人协作的项目。 遗留问题是:如果 owners 表已有百万行数据,加字段或加索引时,直接 ALTER TABLE 会锁表,导致业务中断。 用 V1 同一组请求验证:启动应用,查询 owners 列表,返回正常,说明迁移成功。 ```http GET /api/owners ``` ```text 示例输出: HTTP/1.1 200 OK [{"id":1,"firstName":"George","lastName":"Franklin","email":"george@example.com"}] ``` > 版本管理已就绪, > 百万数据待处理, > 锁表风险要规避, > 兼容升级需策略。 # 09、洞见 ```text 从现象到判断:Entity 改字段只是代码层变化,数据库结构必须通过迁移脚本显式变更,否则生产环境必然不一致 ``` ## 代码与数据库是两套生命周期 ```text 代码:编译 → 打包 → 发布 → 回滚(秒级) 数据库:结构变更 → 数据迁移 → 验证 → 回滚(分钟到小时级) ``` 代码和数据库的变更速度不同步,代码可以快速迭代,但数据库结构变更涉及数据安全,必须谨慎。 反直觉判断:很多人以为改 Entity 就等于改数据库,实际上 Entity 只是映射,数据库结构不会自动跟随。 ## 数据库回滚比代码回滚复杂得多 ```text 代码回滚:替换 jar 包 → 重启应用 → 完成(分钟级) 数据库回滚:执行反向 SQL → 数据恢复 → 验证一致性 → 可能丢失新数据(小时级) ``` 代码回滚是替换文件,数据库回滚要处理数据变更,比如删了一列,回滚要恢复数据,但新写入的数据可能无法恢复。 反直觉判断:Flyway 不支持自动回滚,因为自动回滚可能造成数据丢失,所以通常用 forward 修复,而不是 backward 回滚。 ## 迁移脚本是团队契约 ```text 开发 A:V2 加字段 开发 B:V3 加索引 生产:按顺序执行 V1→V2→V3 ``` 迁移脚本是团队协作的契约,每个版本只能追加,不能修改已执行的脚本,否则校验和会失败。 ## 生产环境必须用迁移工具 ```text ddl-auto=update:开发方便,生产危险 schema.sql:初始化可以,增量不行 Flyway:版本化,可追溯,适合生产 ``` 生产环境必须用 Flyway 或 Liquibase,因为只有它们能保证 Schema 变更的可控性。 > 代码库是代码, > 数据库是数据, > 迁移脚本连两者, > 版本管理保平安。 # 10、系统落地 ```text 入口:Petclinic 应用启动 → 服务:Flyway 迁移引擎 → 本模块:Schema 版本管理 → 存储:MySQL 数据库 → 未实现:百万数据表的在线迁移策略 ``` 原来 Petclinic 用 Hibernate ddl-auto 管理表结构,开发方便但生产不可控。 本篇增加了 Flyway 依赖和配置,创建了 V1-V3 迁移脚本,实现了 Schema 版本管理。 现在 Petclinic 启动时会自动执行迁移,表结构变更可追溯、可回滚。 下一篇将处理百万数据表的兼容升级,比如用 Expand/Contract 模式,先加字段(Expand),再迁移数据,最后删旧字段(Contract)。 官方文档:Spring Boot 参考文档、Flyway 官方文档。 > 版本管理已落地, > 百万数据待挑战, > Expand 再 Contract, > 在线升级不中断。 # 11、小结 ```text 三问三答:Entity 改字段不等于数据库升级;选择 Flyway 而非 ddl-auto;选错会导致生产事故 ``` 第一问:Java Entity 改一个字段,为什么不能代表生产数据库已经安全升级? 因为 Entity 只是 Java 对象,数据库结构不会自动跟随,必须通过迁移脚本显式变更。 第二问:选择 ddl-auto、schema.sql 还是 Flyway? 生产环境必须用 Flyway,因为它有版本记录、可重复执行、可回滚,而 ddl-auto 危险,schema.sql 无版本。 第三问:选错迁移方案会有什么后果? 选错会导致生产库结构不一致,应用启动失败,甚至数据丢失,回滚复杂且耗时。 当前状态:Petclinic 已引入 Flyway,Schema 变更纳入版本管理,启动自动迁移。 > 三问三答已清晰, > 迁移工具选 Flyway, > 版本脚本管变更, > 生产升级有保障。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:为什么 Hibernate ddl-auto=update 不适合生产环境? 因为 ddl-auto=update 会在启动时自动修改表结构,可能删除列、改变数据类型,导致数据丢失,且无法控制执行顺序,没有版本记录。 ## 实战题:给 Petclinic 的 visits 表添加一个 visit_date 索引,并创建 V4 迁移脚本。 创建 V4__add_index_on_visits_visit_date.sql。 ```sql CREATE INDEX idx_visits_visit_date ON visits(visit_date); ``` 启动应用,验证版本表新增 V4。 ## 排障题:Flyway 迁移失败,提示 Duplicate column name 'email',如何排查? 先查看版本表,确认 V2 是否已执行。 ```sql SELECT version, success FROM flyway_schema_history; ``` 如果 V2 已成功,但表里没有 email 列,可能是有人手动删了列,导致 Flyway 认为已执行。 修复:手动添加列,或用 Flyway repair 更新状态。 ## 架构判断题:百万数据表加索引,直接 CREATE INDEX 会锁表,如何设计兼容升级? 使用 Expand/Contract 模式:先创建索引(用 ALGORITHM=INPLACE, LOCK=NONE),然后逐步迁移数据,最后删除旧索引。 > 作业四题练一练, > 理解实战加排障, > 架构判断想清楚, > 迁移技能更上一层。 # 13、升华 ```text 工程结论:数据库迁移是生产级应用的必备能力,版本化脚本是唯一可靠的方式,Entity 只是代码层,不能替代数据库变更管理 ``` 从 Petclinic 的实战中,我们验证了 Flyway 的价值:它让 Schema 变更像代码一样可版本化、可审计、可回滚。 但真正的挑战在百万数据表的在线迁移,需要结合 Expand/Contract 模式、在线 DDL 和 CI/CD 流程。 下一篇将深入探讨如何让迁移进入 CI/CD,以及如何设计零停机升级。 > 迁移工具已就位, > 百万数据待处理, > CI/CD 集成好, > 生产升级零停机。 **系列项目:** Spring Petclinic 生产级演进,服务对象是宠物诊所业务。 **已完成:** 环境搭建、启动机制、依赖与自动配置、配置体系、Web 安全、数据访问、事务管理、远程调用、数据库迁移基础(Flyway 版本管理)。 **本篇:** 将 Petclinic Schema 纳入 Flyway 版本管理,建立 V1→V3 迁移链,处理脏数据与回滚脚本。 **下一篇:** 百万数据表的 Expand/Contract 兼容升级与 CI/CD 集成。