首页 / 资讯中心 / 文章详情

Spring Boot 用 Flyway 管理数据库表结构:自动建表、兼容已有库、改表不丢数据

Spring Boot 用 Flyway 管理数据库表结构:自动建表、兼容已有库、改表不丢数据 ★ FEATURED ARTICLE
文章目录一、为什么不再用一个 schema.sql二、Flyway 是怎么工作的三、接入步骤3.1 依赖3.2 配置3.3 脚本四、最关键的问题已经有库了怎么办五、初始数据为什么不写在 SQL 里六、H2 和 MySQL 共用一套脚本七、用测试证明迁移是对的八、团队使用约定九、总结一、为什么不再用一个 schema.sql我们课程小组的项目一开始用的是最常见的做法写一个schema.sql大家手动执行一次建库建表CREATEDATABASEIFNOTEXISTSonlineshopDEFAULTCHARACTERSETutf8mb4;USEonlineshop;CREATETABLEIFNOTEXISTSseller(...);INSERTINTOseller(username,password)VALUES(admin,123456);-- 其他表 ...用了几天问题就出来了要手动执行每个组员都得记得先跑一遍脚本新同学经常漏不能重复执行里面有INSERT跑第二次会因为用户名重复报错改表很麻烦以后给表加一列只能让大家要么删库重建数据丢失要么各自手动ALTER每个人的库不一致没有记录谁的库是哪个版本的表结构说不清楚。我负责后端的持久层就把表结构改成了用Flyway管理。二、Flyway 是怎么工作的Flyway 是数据库表结构的版本管理工具可以理解为「给数据库用的 Git」每次建表、改表都写成一个带版本号的脚本放在db/migration目录V1__init_schema.sql V2__add_query_indexes.sql V3__add_seller_password_version.sql应用启动时Flyway 先查数据库里的flyway_schema_history表看已经执行到哪个版本只执行还没执行过的脚本执行完记录下来包括每个脚本的校验和。两条规则改表就新增一个脚本下一个是 V4已经执行过的脚本不能再改Flyway 启动时会比对校验和改过就报错防止大家的库悄悄变得不一致。三、接入步骤3.1 依赖!-- 数据库表结构版本管理启动时自动执行 db/migration 下的脚本 --dependencygroupIdorg.flywaydb/groupIdartifactIdflyway-core/artifactId/dependencydependencygroupIdorg.flywaydb/groupIdartifactIdflyway-mysql/artifactId/dependency版本由 Spring Boot 的父工程统一管理不用写。注意新版 Flyway 把MySQL 的支持拆成了单独的flyway-mysql模块只引flyway-core连 MySQL 会报不支持的数据库。3.2 配置spring:datasource:# createDatabaseIfNotExisttrue库不存在时自动创建url:${DB_URL:jdbc:mysql://localhost:3306/onlineshop?createDatabaseIfNotExisttrueuseUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai}username:${DB_USERNAME:root}password:${DB_PASSWORD:root}flyway:locations:classpath:db/migration# 脚本里有中文注释和 COMMENT按 UTF-8 读取encoding:UTF-8# 库里已经有表、但还没有 Flyway 记录时以版本 0 为基线继续执行全部脚本baseline-on-migrate:truebaseline-version:0几个要点MySQL 连接串加上createDatabaseIfNotExisttrue连建库这一步也省了数据库密码通过环境变量DB_PASSWORD传入不写进仓库baseline-*两项是为了兼容已有的库第四节专门讲。3.3 脚本V1 建三张表和原来的schema.sql一致但去掉了CREATE DATABASE、USE和INSERT并且用IF NOT EXISTS-- V1 初始表结构-- 由 Flyway 在应用启动时执行每个版本只执行一次已经执行过的脚本不要再修改改表请新增 V{n}__说明.sql。-- 用 IF NOT EXISTS库里已经存在的表原样保留表里的数据不受影响。CREATETABLEIFNOTEXISTSseller(idBIGINTAUTO_INCREMENTPRIMARYKEY,usernameVARCHAR(50)NOTNULLUNIQUECOMMENT用户名,passwordVARCHAR(100)NOTNULLCOMMENT密码)ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT卖家;-- product、purchase_intent 两张表省略V2 加常用查询的索引-- 单件在售检查、当前商品、历史商品都按状态查商品历史商品再按发布时间排序CREATEINDEXidx_product_status_timeONproduct(status,create_time);-- 排队、意向人列表都按商品和意向状态查CREATEINDEXidx_intent_product_statusONpurchase_intent(product_id,status);后来做「修改密码后所有登录失效」时需要给卖家表加一列就是新增一个 V3-- V3 卖家密码版本号每次修改密码加 1修改之前的所有登录随之失效ALTERTABLEsellerADDCOLUMNpassword_versionINTNOTNULLDEFAULT0COMMENT密码版本号修改密码时加 1;组员拉代码、启动表就自动多了这一列原有数据不受影响。这就是用 Flyway 最大的好处。四、最关键的问题已经有库了怎么办接入 Flyway 时组员们的电脑上已经有用旧schema.sql建好的库里面有数据但是没有flyway_schema_history表。Flyway 默认遇到「非空、但没有历史记录」的库会直接报错拒绝执行。baseline-on-migrate: true的意思是遇到这种库先记一条「基线」再从基线之后开始执行。问题是基线版本设成多少。我一开始想设成 1认为旧库就等于 V1但把三种可能的情况列出来以后发现不对情况基线版本设为 1基线版本设为 0 V1 用 IF NOT EXISTS全新的库正常执行 V1、V2、V3正常执行 V1、V2、V3用旧 schema.sql 建过、有数据跳过 V1执行 V2、V3正确执行 V1表已存在跳过再执行 V2、V3正确库里只有一张无关的表比如别的程序留下的跳过 V1业务表根本没建V2 建索引直接报错执行 V1 建出全部表正确第三种情况是我在自己电脑上真实遇到的本机的onlineshop库是之前别的练习项目建的里面只有一张无关的表。所以最终方案是基线版本设为 0V1 全部用CREATE TABLE IF NOT EXISTS。这样不管库里原来是什么状态都会把 V1V3 执行一遍已存在的表跳过、数据保留缺的表和索引补上。五、初始数据为什么不写在 SQL 里原来的schema.sql里有一句INSERT插初始账号。迁移后我把它去掉了改成在 Java 里处理启动时卖家表为空才按配置创建初始账号。原因有两个密码要用 BCrypt 加密后存储加密要在 Java 里做SQL 里写不出来写在 Java 里可以先判断「卖家表是不是空的」不会覆盖已经改过的密码也不会重复插入。Overridepublicvoidrun(ApplicationArgumentsargs){if(sellerMapper.selectCount(null)0){createDefaultSeller();// 按 application.yml 里的 shop.default-seller 创建密码加密保存}else{encryptPlainPasswords();// 已有卖家把还是明文的密码转成加密}}经验是表结构交给迁移脚本和业务逻辑有关的初始数据交给代码。六、H2 和 MySQL 共用一套脚本测试用 H2 内存数据库不需要装 MySQL。为了让测试和正式环境执行同一套Flyway 脚本H2 开了 MySQL 兼容模式# application-test.ymlspring:datasource:driver-class-name:org.h2.Driverurl:jdbc:h2:mem:onlineshop;MODEMySQL;DATABASE_TO_LOWERTRUE;CASE_INSENSITIVE_IDENTIFIERSTRUE;DB_CLOSE_DELAY-1username:sapassword:MODEMySQL能识别ENGINEInnoDB、COMMENT这些 MySQL 写法DATABASE_TO_LOWERTRUE表名、列名统一小写和 MySQL 的习惯一致DB_CLOSE_DELAY-1同一个 JVM 里多个测试共享这个内存库。H2 毕竟不是真正的 MySQL所以关键场景我又在本机 MySQL 8 上手动验证了一遍见下一节的三种情况。七、用测试证明迁移是对的SpringBootTestActiveProfiles(test)classDatabaseMigrationTest{TestDisplayName(启动时执行完全部迁移脚本)voidallMigrationsApplied(){assertThat(Arrays.stream(flyway.info().applied()).map(MigrationInfo::getVersion).map(Object::toString)).contains(1,2,3);assertThat(flyway.info().pending()).isEmpty();}TestDisplayName(再次启动不会重复执行脚本)voidmigrateAgainDoesNothing(){assertThat(flyway.migrate().migrationsExecuted).isZero();}TestDisplayName(库里只有无关的表时照常建出全部业务表)voiddatabaseWithUnrelatedTableIsInitialized(){DataSourceotherh2(other_db);newJdbcTemplate(other).execute(CREATE TABLE app_config (config_key VARCHAR(50) PRIMARY KEY));migrateWithAppSettings(other);JdbcTemplatejdbcnewJdbcTemplate(other);assertThat(jdbc.queryForObject(SELECT COUNT(*) FROM product,Long.class)).isZero();}/** 用 application.yml 里的 Flyway 配置迁移另一个库 */privatevoidmigrateWithAppSettings(DataSourcetarget){Flyway.configure().dataSource(target).locations(flywayProperties.getLocations().toArray(String[]::new)).encoding(flywayProperties.getEncoding()).baselineOnMigrate(flywayProperties.isBaselineOnMigrate()).baselineVersion(flywayProperties.getBaselineVersion()).load().migrate();}// 其他用例和辅助方法省略}migrateWithAppSettings直接注入 Spring Boot 的FlywayProperties用和正式配置完全相同的参数去迁移另一个测试库这样改了application.yml测试也会跟着变不会出现「测试配置和正式配置不一致」。另外还有一个用例先用旧schema.sql的内容建表、插入数据再执行迁移断言原有数据还在、新索引已经补上。在本机 MySQL 8.0 上实测的结果情况结果全新的库不存在自动建库建表重启后提示Schema is up to date不重复执行用旧 schema.sql 建过、有数据原有数据保留补上两个索引升级到 V3库里只有一张无关的表三张业务表正常建出原来的表不动八、团队使用约定接入后我在 README 里写了三条约定改表结构就新增脚本文件名V{下一个版本号}__说明.sql比如V4__add_intent_requeue_column.sql已经执行过的脚本不要再改否则启动时校验失败两个人同时加脚本要先沟通版本号。如果两个分支都加了 V4后合并的那个启动会报版本重复。人多的项目可以改用日期作版本号比如V20261018_1__xxx.sql来避免撞号。九、总结问题Flyway 的做法要手动建库建表启动时自动执行连接串createDatabaseIfNotExisttrue自动建库脚本不能重复执行每个版本只执行一次有历史表记录改表要删库新增一个版本脚本已有数据保留已有的旧库baseline-on-migrate 基线版本 0 IF NOT EXISTS测试和正式环境不一致H2 MySQL 兼容模式执行同一套脚本测试直接复用正式的 Flyway 配置这次最大的收获是设计方案时把各种可能的初始状态都列出来。基线版本设成 1 看起来最「自然」但遇到「库里只有无关表」这种情况就会出错而这恰好是我自己电脑上的真实情况。
阅读完成 · 觉得有帮助?
咨询建站