Spring Boot多数据源下Flyway数据库版本管理实战指南
1. 项目缘起为什么我们需要一个数据库版本管理工具如果你经历过一个项目从零到一再到迭代上线你大概率会遇到这样的场景开发环境、测试环境、预生产环境、生产环境每个环境的数据库结构都可能存在微妙的差异。A同事在本地开发时加了一个字段B同事修复了一个索引C同事在测试环境手动执行了一个DDL脚本。等到要上线时你发现手头有一堆零散的SQL文件根本记不清哪个该执行哪个不该执行哪个已经执行过了。更糟糕的是一旦某个脚本在生产环境执行失败回滚和修复将是一场噩梦。这种混乱正是数据库版本管理工具要解决的痛点。Flyway就是这样一个将数据库的变更像管理代码版本一样管理起来的工具。它遵循一个核心哲学数据库的每一次变更都是一个版本化的、不可变的迁移脚本。通过将这些脚本纳入版本控制系统如Git并与应用程序一同发布Flyway能够确保在任何环境下数据库的结构都能被自动、可靠地同步到预期的状态。它解决了“我的数据库现在是什么版本”、“下一个版本需要执行哪些变更”以及“如何确保变更在所有环境中的一致性”这三个核心问题。在微服务架构和持续集成/持续部署CI/CD流程中Flyway已经成为保障数据层交付可靠性的基石。2. Flyway核心概念与工作流程拆解要正确配置和使用Flyway必须先理解它的几个核心概念和工作流程这能帮你避开很多“知其然不知其所以然”的坑。2.1 核心概念解析迁移脚本Migration Scripts这是Flyway的基石。每个脚本代表一次数据库变更可以是DDL如CREATE TABLE, ALTER TABLE或DML如INSERT, UPDATE。脚本分为两类版本化迁移Versioned Migrations这是最主要的类型。文件名格式为V{版本号}__{描述}.sql注意是双下划线。例如V1.0__Create_user_table.sql。版本号必须全局唯一且递增。Flyway会按版本号顺序依次执行这些脚本并且一旦执行就不会再重复执行除非进行特殊清理操作。可重复迁移Repeatable Migrations文件名格式为R__{描述}.sql。这类脚本在每次校验validate时如果其内容哈希值发生了变化就会被重新执行。常用于创建或更新视图、存储过程、函数等逻辑对象。模式历史表flyway_schema_history这是Flyway的“大脑”。当Flyway在一个新数据库中首次运行时它会自动创建这张表。这张表记录了所有已执行迁移脚本的详细信息包括版本号、描述、脚本内容的哈希值、执行成功与否、执行时间等。Flyway通过查询这张表就能精确知道当前数据库处于哪个版本以及接下来需要执行哪些脚本。迁移生命周期Flyway的操作命令Maven/Gradle插件或命令行遵循一个清晰的生命周期Baseline基线对于一个已存在数据的旧数据库你可以为其设置一个基线版本。Flyway会忽略所有版本号低于或等于基线版本的迁移脚本并将基线版本信息写入历史表。这用于项目引入Flyway时平滑接入已有数据库。Migrate迁移核心操作。Flyway会扫描类路径下的迁移脚本与历史表对比然后按顺序执行所有未应用的版本化迁移和内容已变更的可重复迁移。Clean清理危险操作会清空整个数据库的所有对象表、视图、数据等。通常仅用于开发和测试环境。Info信息打印出所有迁移脚本的状态已应用、待执行、失败等让你一目了然。Validate校验校验已应用的迁移脚本是否被篡改。通过计算现有脚本的哈希值与历史表中记录的哈希值进行比对如果不一致校验就会失败。这是保证迁移一致性的安全网。Repair修复修复历史表。例如当历史表中标记了某个失败的迁移但实际数据库状态是成功的可能手动修复了可以用此命令将状态修正为成功。2.2 Flyway工作流程详解当你运行mvn flyway:migrate或执行Java程序调用Flyway API时会发生以下事情连接与检查Flyway使用你配置的数据源连接到数据库。历史表检查检查flyway_schema_history表是否存在。如果不存在则创建它对于支持DDL事务的数据库此操作在一个事务内完成。脚本扫描与排序从配置的路径默认为classpath:db/migration扫描所有符合命名规范的.sql文件。然后按照版本号对版本化迁移进行排序可重复迁移则按描述排序。版本比对将扫描到的脚本列表与历史表中已成功应用的记录进行比对计算出“待应用迁移Pending Migrations”列表。执行迁移按顺序对每一个待应用迁移执行以下操作在一个事务中执行脚本内容如果数据库和语句支持事务如PostgreSQL对于MySQL每条语句可能自成一个事务取决于配置。执行成功后向flyway_schema_history表插入一条新记录包含版本、描述、哈希值、执行时间等。完成报告所有迁移执行完毕后输出成功/失败的报告。注意默认情况下Flyway会在迁移过程中自动提交事务。对于DDL操作很多数据库如Oracle、SQL Server是自动提交的即使你在事务中。MySQL的InnoDB引擎对部分DDL支持事务内回滚如MySQL 8.0的原子DDL但像DROP TABLE这样的操作依然是不可回滚的。因此永远不要假设Flyway迁移是原子性的。对于关键的上线变更务必先在测试环境充分验证并准备好回滚方案通常是编写一个向下的回滚脚本但Flyway社区版不自动支持回滚。3. Spring Boot中Flyway的单数据源标准配置在Spring Boot项目中集成Flyway非常简单得益于其出色的自动配置。但“简单”背后仍有许多细节值得深究。3.1 基础依赖与自动配置首先在pom.xml中添加依赖dependency groupIdorg.flywaydb/groupId artifactIdflyway-core/artifactId /dependency如果你的项目使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpaFlyway的自动配置将会被激活。Spring Boot会自动做以下几件事在应用启动时在所有DataSourceBean初始化之后自动执行Flyway.migrate()。使用主数据源Primary标注的或默认创建的DataSource作为Flyway的操作数据库。在classpath:db/migration目录下寻找迁移脚本。3.2 关键配置属性详解虽然自动配置很省心但生产环境我们通常需要更精细的控制。以下是在application.yml中常用的配置项spring: flyway: # 是否启用Flyway默认为true enabled: true # 迁移脚本的位置数组格式。默认是 classpath:db/migration locations: classpath:db/migration # 迁移脚本的前缀默认为 V sql-migration-prefix: V # 可重复迁移脚本的前缀默认为 R repeatable-sql-migration-prefix: R # 迁移脚本的后缀默认为 .sql sql-migration-suffixes: .sql # 版本号中的分隔符默认为两个下划线 __ sql-migration-separator: __ # 迁移时是否自动校验建议生产环境设为true validate-on-migrate: true # 当发现已应用的迁移脚本被篡改哈希校验失败时是否允许迁移继续。 # 生产环境强烈建议设为 false以确保一致性。 ignore-migration-patterns: *:ignored # 当迁移目标数据库非空且无元数据表flyway_schema_history时的行为。 # baseline-on-migrate: true 表示自动以初始版本为基线。 # 对于已有数据的库引入Flyway这个很有用但务必清楚当前数据库对应的版本。 baseline-on-migrate: false # 基线版本号当 baseline-on-migratetrue 时生效 baseline-version: 0 # 是否允许在非空数据库上执行clean操作。生产环境必须为false clean-disabled: false # 实际生产建议通过 clean-enabled: false 来禁用clean clean-enabled: false # 更直接的禁用方式 # 占位符替换配置 placeholders: table-prefix: myapp_ placeholder-prefix: ${ placeholder-suffix: } # 编码确保与SQL文件保存的编码一致防止乱码 encoding: UTF-8 # 生产环境建议关闭避免意外清空 clean-on-validation-error: false一个我踩过的坑ignore-migration-patterns这个配置。曾经在测试环境一个同事手动修改了已执行的SQL文件为了临时修复一个测试数据问题导致validate失败进而阻塞了后续的migrate。我们将ignore-migration-patterns设为了*:ignored这其实是一个“通配符”它告诉Flyway忽略所有类型的校验错误包括内容不匹配。这在测试环境或许可以接受但绝对不要在生产环境使用。生产环境的正确做法是永远不要手动修改已提交到版本库的历史迁移脚本。如果必须修正应该创建一个新的、版本号更高的迁移脚本。3.3 迁移脚本的命名与编写规范命名是重中之重。混乱的命名是Flyway项目混乱的开始。版本号规则强烈建议使用语义化版本号例如V1.0.1__或者使用日期时间戳如V20241015.1100__年月日.时分。团队内部必须统一规则。日期戳的优点是天然有序一目了然。描述清晰双下划线后的描述应简洁明了使用小写字母和下划线如create_user_table,add_email_to_user。让其他人一看就知道这个脚本是做什么的。一个脚本一个变更单元每个迁移脚本应该只完成一个逻辑完整的变更。不要将创建表、添加索引、初始化数据全部塞进一个脚本。例如V1.0__init_schema.sql创建所有表V1.1__add_indexes.sql添加索引V1.2__seed_basic_data.sql初始化基础数据。这样在出现问题时更容易定位和回滚。编写幂等性脚本理想情况下脚本应具备幂等性即执行多次的结果与执行一次相同。这可以通过CREATE TABLE IF NOT EXISTS、ALTER TABLE ... ADD COLUMN IF NOT EXISTS等语句实现。虽然Flyway本身会防止重复执行版本化迁移但幂等性在手动执行或修复时能提供额外的安全网。使用占位符对于表名前缀、特定环境变量等使用${table-prefix}这样的占位符提高脚本的复用性。示例V20241015.1100__create_${table-prefix}user.sqlCREATE TABLE IF NOT EXISTS ${table-prefix}user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;4. 多数据源场景下的Flyway高级配置现代应用尤其是微服务架构下一个服务连接多个数据库的情况非常普遍。比如主业务库、报表库、缓存库如Redis非SQL或者由于分库分表策略需要。这时让Flyway管理多个数据源就成为了一个挑战。Spring Boot的自动配置是为单数据源设计的我们需要手动介入。4.1 多数据源配置的常见陷阱在配置多数据源Flyway之前必须先理解Spring Boot自动配置的机制。当你定义了多个DataSourceBean时Spring Boot的FlywayAutoConfiguration会变得“困惑”因为它默认期望只有一个DataSource。如果你只是简单地在application.yml里为每个数据源配置spring.flyway.*属性你会发现只有其中一个通常是主数据源的Flyway会生效或者直接报错“Failed to configure a DataSource: url attribute is not specified”之类的错误。这是因为自动配置会尝试为每个DataSource创建Flyway实例但配置是全局的会造成冲突。我们必须关闭自动配置并手动、显式地为每个需要版本控制的DataSource创建独立的Flyway实例。4.2 基于Configuration的显式配置方案以下是经过生产验证的、清晰的多数据源Flyway配置方法。我们假设有两个数据源primaryDataSource主库和secondaryDataSource从库/报表库。第一步排除默认的Flyway自动配置可选但推荐在启动类或某个配置类上排除FlywayAutoConfiguration。这样可以完全接管Flyway的初始化过程避免自动配置的干扰。SpringBootApplication(exclude {FlywayAutoConfiguration.class}) public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }第二步配置主数据源及其FlywayConfiguration public class PrimaryDatabaseConfig { // 主数据源配置 Bean ConfigurationProperties(spring.datasource.primary) // 对应yml中的配置前缀 public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } Bean public PlatformTransactionManager primaryTransactionManager(Qualifier(primaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 主数据源的Flyway配置 Bean public Flyway primaryFlyway(Qualifier(primaryDataSource) DataSource dataSource) { return Flyway.configure() .dataSource(dataSource) // 指定主库的迁移脚本路径 .locations(classpath:db/migration/primary) .baselineOnMigrate(true) // 根据需要设置 .baselineVersion(0) // 基线版本 .placeholderReplacement(false) // 如果不需要占位符则关闭 .load(); } // 关键在Bean初始化后执行主库迁移 Bean DependsOn(primaryFlyway) // 确保Flyway Bean先初始化 public DataSourceInitializer primaryDataSourceInitializer(Qualifier(primaryDataSource) DataSource dataSource, Qualifier(primaryFlyway) Flyway flyway) { // 这里可以执行一些自定义的初始化逻辑但Flyway.migrate()通常会在Flyway Bean被Spring管理时自动调用 // 实际上Flyway Bean本身不会自动调用migrate。我们需要一个Runner。 return null; // 我们更推荐使用下面的Runner方式 } }第三步配置次数据源及其Flyway配置方式与主数据源类似但务必使用不同的Bean名称和脚本路径。Configuration public class SecondaryDatabaseConfig { Bean ConfigurationProperties(spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } Bean public PlatformTransactionManager secondaryTransactionManager(Qualifier(secondaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } Bean public Flyway secondaryFlyway(Qualifier(secondaryDataSource) DataSource dataSource) { return Flyway.configure() .dataSource(dataSource) // 指定次库的迁移脚本路径必须与主库不同 .locations(classpath:db/migration/secondary) .table(flyway_secondary_history) // 强烈建议使用不同的历史表名避免冲突 .baselineOnMigrate(true) .baselineVersion(0) .load(); } }第四步使用CommandLineRunner或ApplicationRunner触发迁移由于我们排除了自动配置Flyway的migrate()方法不会自动执行。我们需要显式地调用它。一个优雅的方式是使用CommandLineRunner并控制执行顺序。Component public class FlywayMigrationRunner { private final Flyway primaryFlyway; private final Flyway secondaryFlyway; public FlywayMigrationRunner(Qualifier(primaryFlyway) Flyway primaryFlyway, Qualifier(secondaryFlyway) Flyway secondaryFlyway) { this.primaryFlyway primaryFlyway; this.secondaryFlyway secondaryFlyway; } Bean Order(Ordered.HIGHEST_PRECEDENCE) // 确保在所有其他Bean初始化前先执行数据库迁移 public CommandLineRunner runMigrations() { return args - { log.info(开始执行主数据库迁移...); primaryFlyway.migrate(); log.info(主数据库迁移完成。); log.info(开始执行次数据库迁移...); secondaryFlyway.migrate(); log.info(次数据库迁移完成。); }; } }对应的application.yml配置spring: datasource: primary: url: jdbc:mysql://localhost:3306/primary_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8 username: root password: password123 driver-class-name: com.mysql.cj.jdbc.Driver secondary: url: jdbc:mysql://localhost:3306/secondary_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8 username: root password: password123 driver-class-name: com.mysql.cj.jdbc.Driver4.3 关键注意事项与避坑指南独立的脚本目录locations配置必须不同如db/migration/primary和db/migration/secondary。将脚本物理隔离是最清晰的做法。独立的历史表名通过.table(“flyway_secondary_history”)为第二个数据源的Flyway指定不同的历史表名。这是必须的否则两个Flyway实例会尝试操作同一张flyway_schema_history表如果它们在同一个数据库实例的不同schema中可能表名会冲突如果在不同实例则无此问题但显式指定仍是好习惯。迁移执行顺序使用Order或DependsOn确保数据库迁移在应用其他需要数据库的Bean如JPA的EntityManagerFactory初始化之前完成。否则应用启动时可能会因为表不存在而报错。关于dynamic-datasource等动态数据源组件如果你使用的是类似dynamic-datasource-spring-boot-starter这样的动态数据源组件情况会更复杂。这些组件通常提供一个主DataSource代理内部根据注解或Key路由到真实数据源。在这种情况下Flyway通常只配置和作用于这个主代理数据源所指向的“默认”物理库。如果你需要管理多个物理库的迁移上述手动配置多个FlywayBean的方式仍然是有效的你需要为每个物理库定义独立的DataSourceBean和FlywayBean动态数据源组件本身不负责数据库迁移。测试环境的隔离在集成测试中如果使用内存数据库如H2要确保Flyway的配置能正确指向测试数据源并且迁移脚本的语法与H2兼容MySQL和H2的SQL语法有差异。可以使用Flyway的clean和migrate在测试开始前准备数据库。5. 集成到CI/CD流程与最佳实践将Flyway集成到CI/CD管道中是实现“数据库即代码”和持续交付的关键一步。5.1 在Jenkins/GitLab CI中的集成核心思想是在应用打包和部署之前先对目标数据库执行迁移。这通常作为一个独立的CI阶段。示例 GitLab CI.gitlab-ci.yml片段stages: - build - migrate-database - deploy migrate-production-db: stage: migrate-database script: # 1. 使用Flyway命令行工具或者使用Maven/Gradle插件 # 假设我们使用Docker运行Flyway命令行客户端 - | docker run --rm -v $(pwd)/sql:/flyway/sql \ -v $(pwd)/conf:/flyway/conf \ flyway/flyway:latest \ -configFiles/flyway/conf/flyway-production.conf \ migrate only: - main # 仅在对main分支的合并/推送时触发 # 连接到生产数据库需要安全的凭据应使用CI/CD的Variables功能切勿硬编码 variables: # 这些变量在GitLab项目设置中预先配置好 FLYWAY_URL: $PRODUCTION_DB_URL FLYWAY_USER: $PRODUCTION_DB_USER FLYWAY_PASSWORD: $PRODUCTION_DB_PASSWORD更常见的做法Java项目在打包阶段mvn clean package并不执行迁移而是将迁移作为应用启动的一部分即我们之前在代码中配置的CommandLineRunner。这样每个应用实例在启动时都会自动检查并执行迁移。这种方式的优点是简单、一致。但需要注意并发启动在集群部署时多个应用实例可能同时启动并尝试迁移。Flyway通过历史表的行级锁具体机制因数据库而异提供了很好的并发控制通常只有一个实例会成功执行迁移其他实例会等待或跳过。但为了绝对安全在关键的生产部署中仍建议采用“先迁移后部署应用”的蓝绿部署模式。迁移失败如果迁移脚本本身有错误导致执行失败应用将无法启动。这实际上是一种“快速失败”的保护机制避免了应用在错误的数据库结构上运行。5.2 团队协作规范与流程脚本审核Code Review所有迁移脚本必须像代码一样经过Pull Request和Code Review。重点审核SQL语法、性能影响如在大表上加锁的DDL、是否包含敏感数据如密码明文、以及脚本的幂等性。预发布环境验证必须有一个与生产环境数据库版本和规模尽可能一致的预发布Staging环境。任何迁移脚本都必须先在Staging环境成功执行并通过完整回归测试后才能合并到主分支并部署到生产。向后兼容性在线上系统进行DDL变更时务必考虑向后兼容。例如删除一个列时应确保没有正在运行的程序依赖这个列。通常采用“扩展-收缩”模式先添加新列/新表迁移数据然后逐步将应用流量切换到新结构最后再删除旧的列/表。版本号管理团队必须严格遵循统一的版本号命名规则。建议在项目根目录维护一个CHANGELOG.md或类似文件记录每个版本对应的数据库变更内容。回滚方案Flyway社区版不支持自动向下迁移回滚。因此每次编写向上迁移V前缀脚本时都应该同步考虑并手动准备一个向下的回滚脚本。这个脚本可以命名为U{版本号}__{描述}.sqlUndo并存储在另一个目录如db/rollback中。虽然Flyway不会自动执行它但在紧急回滚时DBA或运维人员可以手动执行对应的回滚脚本。这是一个至关重要的安全网。5.3 监控与告警迁移历史监控定期检查flyway_schema_history表了解迁移执行的成功率、耗时等信息。可以将其集成到公司的监控系统如Prometheus Grafana。校验失败告警在CI/CD管道或应用启动日志中严密监控validate步骤。任何校验失败都应触发最高级别的告警因为这意味着生产环境的数据库状态与代码库中的定义已不一致是重大风险。迁移时长监控对于执行时间较长的迁移如为亿级数据表添加索引需要监控其执行时间并设置超时阈值避免阻塞应用部署流程过长。对于这类“大动作”应考虑在业务低峰期通过运维工具手动执行而非通过应用启动流程。6. 常见问题排查与实战心得即使配置得当在实际使用中还是会遇到各种问题。以下是一些典型场景和解决思路。6.1 “Validate failed” 校验失败这是最常见的问题之一。错误信息通常是“Detected failed migration to version ... (校验和 mismatch)”。原因与排查本地开发脚本被修改开发者本地修改了已经提交到版本库且已被其他环境应用的SQL文件。这是绝对禁止的操作。修复方法恢复被修改的脚本到原始版本或者创建一个新的、版本号更高的迁移脚本来进行修正。不同环境脚本不一致可能由于部署错误导致某个环境应用的脚本版本与其他环境不同。检查版本控制系统中的脚本是否一致。编码问题SQL文件保存的编码如UTF-8 with BOM与Flyway配置的encoding不一致导致计算出的哈希值不同。确保所有SQL文件使用无BOM的UTF-8编码。处理命令如果确定是无关紧要的差异比如只是注释的增减并且你确信可以忽略可以使用flyway repair命令来更新历史表中的校验和使其与当前文件匹配。但请务必谨慎并理解其风险。6.2 “Out of order” 迁移顺序错误错误信息“Detected out of order migration ...”。原因Flyway发现了一个版本号比当前已应用的最新版本号小但尚未应用的迁移脚本。例如历史表中最新版本是V1.2但类路径下存在一个V1.1的脚本未应用。解决方案预期内的顺序外迁移如果你确实需要在已应用更高版本后插入一个低版本的迁移这种情况应尽量避免可以通过配置flyway.outOfOrdertrue来允许。但这会破坏迁移的线性历史不推荐。非预期情况通常是版本号管理混乱或部署错误。需要仔细核对所有环境的迁移历史和脚本找出缺失的脚本并补上或者修正版本号。6.3 多数据源配置下迁移未执行现象按照上述多数据源配置后应用启动时没有为某个数据源执行迁移。排查步骤检查Bean是否被创建在启动日志中搜索Flyway关键词看是否有多个FlywayBean的初始化日志。检查CommandLineRunner确保你的CommandLineRunner被正确注册并且注入了正确的FlywayBean通过Qualifier。可以在runMigrations方法开始处加日志看是否被调用。检查数据源连接确保DataSourceBean的配置URL用户名密码是正确的能够正常连接数据库。可以在Flyway.configure()之后、.load()之前调用.connectRetries(10)等参数增加容错。检查脚本路径确认locations配置的路径下确实存在SQL文件且命名规范。路径是相对于类路径的。6.4 迁移脚本中的事务管理这是一个高级但至关重要的话题。默认情况下Flyway在每个迁移脚本执行后自动提交。但对于一个脚本内的多条语句呢支持DDL事务的数据库如PostgreSQL, SQL ServerFlyway默认会将整个脚本包装在一个事务中。如果脚本中间任何一条语句失败整个脚本所做的所有更改都会被回滚历史表也不会记录这次迁移。不完全支持DDL事务的数据库如MySQL, Oracle情况复杂。在MySQL中每条DDL语句如CREATE TABLE,ALTER TABLE都会隐式提交当前事务。这意味着如果一个脚本包含两条ALTER TABLE语句第一条成功第二条失败那么第一条的更改无法被自动回滚。历史表会记录这次迁移为失败。实战建议保持脚本精简一个脚本只做一件小事降低单脚本失败的影响范围。进行预检查在脚本中可以使用SELECT语句检查前置条件。例如在添加列之前先检查列是否已存在虽然更推荐使用ADD COLUMN IF NOT EXISTS。手动事务控制谨慎使用对于MySQL你可以在脚本开头写START TRANSACTION;在结尾写COMMIT;。但这只对DML语句INSERT,UPDATE,DELETE有效对DDL无效。对于包含DDL的脚本这种写法可能产生误导。最重要的测试测试再测试任何迁移脚本尤其是涉及生产数据变更的必须在非生产环境进行多次、完整流程的测试。模拟失败场景验证你的回滚方案是否有效。我个人在管理一个大型金融项目的数据库迁移时曾因一个ALTER TABLE脚本在测试环境通过但在生产环境因锁超时失败导致了一次线上事故。自那以后我们团队强制规定所有超过1秒执行时间的DDL必须附有详细的执行计划评估和低峰期执行手册并且必须在同数据量的Staging环境验证通过。Flyway是你的自动化工具但严谨的流程和敬畏之心才是数据库安全的最终保障。

相关新闻