ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

09-数据库版本管理:Flyway 项目自动化迁移

09-数据库版本管理:Flyway 项目自动化迁移 数据库版本管理Flyway 项目自动化迁移黒漂技术佬 · 2026年7月前言你的团队是不是这样管理数据库变更的DBA 在群里发了个alter_table.sql文件说各位记得在本地执行一下。开发环境执行了测试环境忘了执行生产环境等到上线时才发现新代码引用的字段不存在直接报错。更刺激的是两个人同时改了同一个表结构SQL 文件冲突了手动合并合到怀疑人生。还有人把建表 SQL 放在init.sql里每次加字段就改这个文件。但已经跑过init.sql的环境怎么办再跑一遍报表已存在。于是又加了IF NOT EXISTS文件越来越长最后没人知道这个表经历了什么。这些问题的根源是数据库变更没有版本管理。代码有 Git 管版本数据库变更也得有。Flyway 就是干这个的。一、为什么需要数据库版本管理1.1 手动管理 SQL 的风险问题后果本地改了表结构没同步给团队别人拉代码跑不起来测试环境和生产环境表结构不一致测试通过上线炸变更脚本没有执行顺序先执行了加字段的脚本后执行建表脚本报错无法回滚加错了字段不知道怎么撤销新人接手不知道表怎么演变的对着一张有30个字段的表发呆1.2 Flyway 解决了什么Flyway 是一个数据库版本管理工具核心思路很简单把每次数据库变更写成一个 SQL 文件文件名带版本号Flyway 启动时自动检测哪些 SQL 还没执行过按版本号顺序执行未执行的 SQL执行成功后记录到一个flyway_schema_history表中这样不管你部署到哪个环境Flyway 都能自动把数据库结构升级到最新版本。不需要手动执行 SQL不需要担心漏执行或多执行。二、Flyway 核心概念2.1 Migration迁移脚本每个 SQL 文件就是一个 migration代表一次数据库变更。比如建一张表、加一个字段、加一个索引、修改一个存储过程都可以是一个 migration。2.2 版本号Flyway 用版本号管理执行顺序。版本号在文件名中定义比如V1__init.sql的版本号是1V2__add_column.sql的版本号是2。Flyway 按版本号从小到大依次执行。版本号支持多种格式V1、V2、V3…简单递增V1.0、V1.1、V2.0…语义化版本V20260730.1…日期序号2.3 校验ValidationFlyway 在执行前会校验已执行过的 migration 文件有没有被修改。如果你改了已经执行过的V1__init.sqlFlyway 启动时会报校验错误拒绝启动。这是为了防止篡改历史——已经应用到生产环境的变更不能修改只能新增。2.4 flyway_schema_history 表Flyway 在数据库中自动创建一张表flyway_schema_history记录每个 migration 的执行情况installed_rankversiondescriptiontypescriptchecksuminstalled_byinstalled_onsuccess11initSQLV1__init.sql1234567890root2026-07-30 10:00:00true22add columnSQLV2__add_column.sql9876543210root2026-07-30 11:00:00trueFlyway 每次启动时查这张表对比version和checksum决定要不要执行新的 migration 或报校验错误。三、SpringBoot 整合 Flyway 配置3.1 引入依赖dependencygroupIdorg.flywaydb/groupIdartifactIdflyway-core/artifactIdversion9.22.3/version/dependencydependencygroupIdorg.flywaydb/groupIdartifactIdflyway-mysql/artifactIdversion9.22.3/version/dependencySpringBoot 3.x 需要额外引入flyway-mysqlFlyway 9.x 把 MySQL 支持拆成了独立模块。3.2 配置文件spring:flyway:enabled:true# 开启Flywaylocations:classpath:db/migration# SQL脚本目录baseline-on-migrate:true# 已有数据库时自动建基线baseline-version:0# 基线版本号validate-on-migrate:true# 迁移前校验clean-disabled:true# 禁止clean生产环境必须禁用table:flyway_schema_history# 历史记录表名encoding:UTF-8# SQL文件编码datasource:url:jdbc:mysql://localhost:3306/vending_machine?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghaiusername:rootpassword:123456关键配置解释baseline-on-migrate: true如果数据库已经存在表但没有flyway_schema_historyFlyway 会在第一次执行时创建一个基线版本0之后只执行版本号大于0的 migration。这个配置对已有项目接入 Flyway 非常重要——否则 Flyway 会试图重新建表和已有表冲突。clean-disabled: trueflyway clean会删除所有表然后重建。生产环境必须禁用否则手滑执行一下数据全没了。validate-on-migrate: true每次迁移前校验已执行的脚本是否被修改。如果 checksum 变了启动报错。3.3 SQL 脚本目录结构src/ └── main/ └── resources/ └── db/ └── migration/ ├── V1__init_schema.sql ├── V2__add_product_category.sql ├── V3__add_order_index.sql ├── V4__add_device_table.sql └── V5__add_stock_version.sql脚本放在classpath:db/migration下和配置文件中的locations对应。四、SQL 脚本命名规则4.1 标准命名格式V{版本号}__{描述}.sql注意是两个下划线分隔版本号和描述。文件名版本号描述V1__init_schema.sql1init schemaV1.1__add_user_table.sql1.1add user tableV2__add_product_category.sql2add product categoryV20260730.1__add_index.sql20260730.1add index4.2 版本号排序规则Flyway 按版本号的数值排序不是字符串排序V1→V2→V10数值排序10 大于 2V1.1→V1.2→V1.10小数点分隔每段独立比较V1→V1.1→V21 1.1 24.3 Repeatable 脚本可重复执行除了V开头的版本化脚本Flyway 还支持R开头的可重复执行脚本R__refresh_views.sqlR脚本没有版本号当脚本内容checksum变化时重新执行。适合放视图、存储过程等可以覆盖重建的对象。-- R__refresh_views.sqlCREATEORREPLACEVIEWv_device_salesASSELECTdevice_id,COUNT(*)ASorder_count,SUM(total_amount)AStotal_salesFROMt_orderWHEREstatus2GROUPBYdevice_id;每次修改这个视图改这个文件就行Flyway 会自动重新执行。4.4 命名规范建议团队协作时建议统一命名风格变更类型命名示例说明建表V1__create_table_product.sqlcreate_table 前缀加字段V2__add_column_product_status.sqladd_column 前缀加索引V3__add_index_order_user_id.sqladd_index 前缀改字段V4__alter_column_product_price.sqlalter_column 前缀建视图R__view_device_sales.sql可重复执行五、Flyway 执行流程5.1 启动时的执行逻辑SpringBoot 启动 ↓ Flyway 初始化 ↓ 检查 flyway_schema_history 表是否存在 ↓不存在 创建 flyway_schema_history 表 ↓存在 查询已执行的 migration 列表 ↓ 扫描 db/migration 目录下的所有 SQL 文件 ↓ 对比已执行列表和文件列表 1. 新文件 → 标记为待执行 2. 已执行文件 → 校验 checksum不一致则报错 3. 已执行但文件被删除 → 根据 validateMigrationNaming 配置决定是否报错 ↓ 按版本号顺序执行待执行的 migration ↓ 每个 migration 执行成功后记录到 flyway_schema_history ↓ 全部执行完毕SpringBoot 继续启动5.2 执行失败的处理如果某个 migration 执行失败SQL 语法错误等Flyway 会记录该 migration 为失败状态success 0停止后续 migration 执行SpringBoot 启动失败修复方法-- 先手动修复数据库中的残留如果SQL执行了一半-- 然后删除失败的记录DELETEFROMflyway_schema_historyWHEREsuccess0;-- 修复SQL文件后重启生产环境务必先在测试环境验证 SQL避免上线时 migration 失败。六、版本迁移示例建表 → 加字段 → 加索引6.1 V1初始化建表-- V1__create_table_product.sqlCREATETABLEt_product(idBIGINTAUTO_INCREMENTPRIMARYKEYCOMMENT商品ID,nameVARCHAR(100)NOTNULLCOMMENT商品名称,priceDECIMAL(10,2)NOTNULLDEFAULT0COMMENT价格,category_idBIGINTDEFAULTNULLCOMMENT分类ID,statusTINYINTNOTNULLDEFAULT1COMMENT状态0下架 1上架,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT创建时间,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT更新时间,INDEXidx_category(category_id),INDEXidx_status(status))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT商品表;CREATETABLEt_order(idBIGINTAUTO_INCREMENTPRIMARYKEYCOMMENT订单ID,order_noVARCHAR(32)NOTNULLUNIQUECOMMENT订单号,user_idBIGINTNOTNULLCOMMENT用户ID,device_idBIGINTNOTNULLCOMMENT设备ID,statusTINYINTNOTNULLDEFAULT0COMMENT状态0待支付 1已支付 2已取消,total_amountDECIMAL(10,2)NOTNULLDEFAULT0COMMENT订单金额,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,UNIQUEINDEXuk_order_no(order_no),INDEXidx_user_id(user_id),INDEXidx_device_id(device_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT订单表;6.2 V2加字段-- V2__add_column_product_version.sql-- 为商品表加乐观锁版本号字段ALTERTABLEt_productADDCOLUMNversionINTNOTNULLDEFAULT0COMMENT乐观锁版本号;6.3 V3加索引-- V3__add_index_order_create_time.sql-- 订单表按创建时间查询频繁加索引ALTERTABLEt_orderADDINDEXidx_create_time(create_time);6.4 V4加新表-- V4__create_table_device_stock.sql-- 设备库存表CREATETABLEt_device_stock(idBIGINTAUTO_INCREMENTPRIMARYKEY,device_idBIGINTNOTNULLCOMMENT设备ID,product_idBIGINTNOTNULLCOMMENT商品ID,stockINTNOTNULLDEFAULT0COMMENT库存,versionINTNOTNULLDEFAULT0COMMENT乐观锁版本号,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,UNIQUEKEYuk_device_product(device_id,product_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT设备库存表;6.5 V5修改字段-- V5__alter_column_order_status.sql-- 订单状态扩展增加退款相关状态-- MySQL不能直接改TINYINT的注释含义这里只改注释ALTERTABLEt_orderMODIFYCOLUMNstatusTINYINTNOTNULLDEFAULT0COMMENT状态0待支付 1已支付 2已取消 3退款中 4已退款;每个文件只做一件事文件名清晰描述变更内容。这样后人看flyway_schema_history就能知道数据库的演变历史。七、Flyway 与 Liquibase 对比Liquibase 是另一个主流的数据库版本管理工具和 Flyway 的主要区别对比项FlywayLiquibase脚本格式SQL 文件XML/YAML/JSON/SQL学习成本低会写SQL就会用中要学Liquibase的XML标签回滚支持社区版不支持需手写撤销脚本内置回滚数据库无关性每种数据库写不同SQLXML抽象层自动适配生态社区更活跃老牌企业用户多适合团队偏好直接写SQL的团队需要跨多种数据库的团队我的建议90% 的项目用 Flyway。原因很简单SQL 是 DBA 和开发都看得懂的东西XML 格式的变更脚本还要额外学习。而且大多数项目只用一种数据库MySQL不需要数据库无关性。Liquibase 的 XML 抽象层确实强大但那是给一套代码跑在 Oracle MySQL PostgreSQL 上的场景用的。如果你只跑 MySQL用 XML 写变更反而是累赘。八、团队协作中的 Flyway 使用规范8.1 只新增不修改铁律已经执行过的 migration 文件永远不要修改。如果你发现 V2 的 SQL 有 bug不要改 V2而是新建一个 V6 来修复-- V6__fix_product_price_type.sql-- 修复V2中price字段类型不当的问题ALTERTABLEt_productMODIFYCOLUMNpriceDECIMAL(12,2)NOTNULLDEFAULT0;原因修改已执行的文件会导致 checksum 变化Flyway 校验失败所有环境都无法启动。8.2 版本号不要冲突两个人同时创建了V6__xxx.sql合并代码时版本号冲突。解决方案使用日期时间戳作为版本号V202607301000__add_column.sql或者团队约定版本号段张三用V100-V199李四用V200-V2998.3 SQL 要幂等安全虽然 Flyway 保证每个 migration 只执行一次但 SQL 最好写成幂等的方便手动执行调试-- 差直接加字段如果手动执行过会报错ALTERTABLEt_productADDCOLUMNbarcodeVARCHAR(50);-- 好判断字段不存在才加MySQL没有IF NOT EXISTS语法用存储过程SETdb_nameDATABASE();SETtablenamet_product;SETcolumnnamebarcode;SETpreparedStatement(SELECTIF((SELECTCOUNT(*)FROMINFORMATION_SCHEMA.COLUMNSWHERETABLE_SCHEMAdb_nameANDTABLE_NAMEtablenameANDCOLUMN_NAMEcolumnname)0,CONCAT(ALTER TABLE ,tablename, ADD COLUMN ,columnname, VARCHAR(50) COMMENT 条码),SELECT 1));PREPAREstmtFROMpreparedStatement;EXECUTEstmt;DEALLOCATEPREPAREstmt;这种方式比较繁琐实际项目中如果严格遵循 Flyway 流程直接写ALTER TABLE也行——只要不手动执行就不会重复。8.4 大表变更要注意锁加字段、加索引在大表上可能锁表很长时间。比如给 1000 万行的订单表加索引可能锁表几分钟。-- V7__add_index_large_table.sql-- 注意此脚本在1000万行表上执行约需5分钟请在低峰期执行-- 建议先用 pt-online-schema-change 工具在应用层执行再通过Flyway标记ALTERTABLEt_orderADDINDEXidx_payment_time(payment_time);对于大表变更建议先在测试环境验证执行时间生产环境用pt-online-schema-change或gh-ost在线变更变更完成后手动在flyway_schema_history中插入记录让 Flyway 认为已执行8.5 多环境管理不同环境的 Flyway 配置可以不同# application-dev.yml开发环境spring:flyway:clean-disabled:false# 开发环境允许clean方便重置# application-prod.yml生产环境spring:flyway:clean-disabled:true# 生产环境禁止cleanvalidate-on-migrate:true九、无人售货柜项目 Flyway 完整配置实战9.1 项目结构src/main/ ├── java/com/example/vending/ │ ├── config/ │ │ └── FlywayConfig.java │ └── VendingApplication.java └── resources/ ├── db/migration/ │ ├── V1__create_core_tables.sql # 核心表商品、订单、用户 │ ├── V2__create_device_tables.sql # 设备表、设备库存表 │ ├── V3__add_order_payment_columns.sql # 订单加支付字段 │ ├── V4__add_performance_indexes.sql # 性能索引 │ ├── V5__create_report_tables.sql # 报表统计表 │ └── R__v_device_sales_summary.sql # 销售汇总视图可重复执行 └── application.yml9.2 配置类ConfigurationpublicclassFlywayConfig{BeanpublicFlywayMigrationInitializerflywayInitializer(Flywayflyway){FlywayMigrationInitializerinitializernewFlywayMigrationInitializer(flyway);// 确保Flyway在JPA/MyBatis之前执行initializer.setOrder(0);returninitializer;}}9.3 初始化脚本示例-- V1__create_core_tables.sql-- 无人售货柜核心表结构初始化-- 用户表CREATETABLEt_user(idBIGINTAUTO_INCREMENTPRIMARYKEY,openidVARCHAR(64)NOTNULLUNIQUECOMMENT微信openid,nicknameVARCHAR(64)DEFAULTNULL,phoneVARCHAR(20)DEFAULTNULL,statusTINYINTNOTNULLDEFAULT1COMMENT0禁用 1正常,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_phone(phone))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT用户表;-- 商品表CREATETABLEt_product(idBIGINTAUTO_INCREMENTPRIMARYKEY,nameVARCHAR(100)NOTNULLCOMMENT商品名称,barcodeVARCHAR(50)DEFAULTNULLCOMMENT条码,priceDECIMAL(10,2)NOTNULLDEFAULT0COMMENT售价,cost_priceDECIMAL(10,2)DEFAULT0COMMENT成本价,categoryVARCHAR(50)DEFAULTNULLCOMMENT分类,image_urlVARCHAR(255)DEFAULTNULL,weightDECIMAL(10,2)DEFAULTNULLCOMMENT重量(克)用于重力柜,statusTINYINTNOTNULLDEFAULT1COMMENT0下架 1上架,versionINTNOTNULLDEFAULT0COMMENT乐观锁版本号,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_barcode(barcode),INDEXidx_category(category),INDEXidx_status(status))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT商品表;-- 订单表CREATETABLEt_order(idBIGINTAUTO_INCREMENTPRIMARYKEY,order_noVARCHAR(32)NOTNULLUNIQUECOMMENT订单号,user_idBIGINTNOTNULL,device_idBIGINTNOTNULL,statusTINYINTNOTNULLDEFAULT0COMMENT0待支付 1已支付 2已取消 3退款中 4已退款,total_amountDECIMAL(10,2)NOTNULLDEFAULT0,pay_timeDATETIMEDEFAULTNULL,close_timeDATETIMEDEFAULTNULLCOMMENT关门时间,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_user_id(user_id),INDEXidx_device_id(device_id),INDEXidx_status(status),INDEXidx_create_time(create_time))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT订单表;9.4 销售汇总视图可重复执行-- R__v_device_sales_summary.sql-- 设备销售汇总视图修改后自动重新执行CREATEORREPLACEVIEWv_device_sales_summaryASSELECTd.idASdevice_id,d.nameASdevice_name,d.store_id,COUNT(o.id)AStotal_orders,SUM(CASEWHENo.status1THEN1ELSE0END)ASpaid_orders,SUM(CASEWHENo.status1THENo.total_amountELSE0END)AStotal_sales,SUM(CASEWHENo.statusIN(3,4)THEN1ELSE0END)ASrefund_orders,MAX(o.create_time)ASlast_order_timeFROMt_device dLEFTJOINt_order oONd.ido.device_idGROUPBYd.id,d.name,d.store_id;十、已有项目接入 Flyway如果项目已经有数据库了表已经存在怎么接入 Flyway第一步配置baseline-on-migrate: true和baseline-version: 0。第二步把当前数据库结构导出为V1__baseline.sqlmysqldump-uroot-p--no-data vending_machineV1__baseline.sql第三步手动在数据库中插入基线记录如果 Flyway 没有自动建基线INSERTINTOflyway_schema_history(installed_rank,version,description,type,script,checksum,installed_by,installed_on,success)VALUES(1,1,baseline,BASELINE,V1__baseline.sql,NULL,root,NOW(),1);第四步之后所有变更都新增 V2、V3… 文件。总结Flyway 的核心要点每个变更一个 SQL 文件文件名带版本号Flyway 自动按序执行只新增不修改已执行的文件永远不改修复问题新建文件baseline-on-migrate: true已有项目接入的关键配置clean-disabled: true生产环境必须禁用 clean大表变更特殊处理用在线变更工具手动标记 Flyway 记录团队约定版本号规则避免冲突Flyway 把数据库变更纳入版本管理让数据库结构和代码一样可追踪、可回溯。上线时再也不用记得执行那个SQL文件——SpringBoot 启动时自动搞定。
返回列表