Flyway的基于SQL脚本的数据库迁移 对于大多数使用情况来说已经足够强大了。但有时,你必须更进一步,使你的现有数据适应新的数据库模式。例如,你可能需要从Blobs中提取数据或读取JSON文档,以便填充新增加的数据库列。在这些情况下,Flyway的Java迁移和回调方法为实现必要的迁移逻辑提供了一个简单而强大的方法。
让我们先来实现一个Java迁移步骤。正如你将看到的,这是很简单的。而在你实现了它之后,你可以以与我在本系列的前一篇文章中向你展示的SQL迁移步骤相同的方式来使用它。
在Java中实现复杂的迁移
在搜索可用的迁移步骤时,Flyway不仅会搜索迁移脚本。它还从db/migration包中获取了回调 接口的实现。如果你想使用不同的包,你可以在flyway.location属性中进行配置。
实现Callback接口的一个简单方法是扩展Flyway的BaseJavaMigration类。它处理了迁移步骤的所有技术复杂性,使你能够专注于实际的迁移工作。当你这样做时,你需要使用一个遵循Flyway的命名模式V__DESCRIPTION.java的类名。然后,Flyway会拾起你的迁移步骤,检查是否需要执行,如果有必要,就会执行。
下面是一个简单迁移类的例子,它将数据库更新到2.0版本。这个迁移的目标是将书的作者存储在一个单独的表中。这需要进行以下操作:
- 创建一个新的作者表和序列
- 从书表中读取所有记录,得到书的id和作者的名字
- 将每个作者作为一个新的记录保存在作者表中
- 将作者的ID设为书表的外键
public class V2__extract_author extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Connection connection = context.getConnection();
// create author table
Statement st = connection.createStatement();
st.execute(
"CREATE TABLE author(id bigint NOT NULL, firstname character varying(255), lastname character varying(255), CONSTRAINT author_pkey PRIMARY KEY (id));");
st.execute("CREATE SEQUENCE author_seq");
// add fk_author to book table
st.execute("ALTER TABLE book ADD COLUMN fk_author bigint REFERENCES author (id);");
// migrate author information
final PreparedStatement psAuthor = connection
.prepareStatement("INSERT INTO author (id, firstname, lastname) VALUES (?, ?, ?)");
final PreparedStatement psBook = connection.prepareStatement("UPDATE book SET fk_author = ? WHERE id = ?;");
ResultSet rs = st.executeQuery("select id, author from book");
Statement idSt = connection.createStatement();
while (rs.next()) {
// get data from book table
Long bookId = rs.getLong("id");
String author = rs.getString("author");
String[] name = author.split(",");
// get author id from sequence
ResultSet authorIdRs = idSt.executeQuery("select nextval('author_seq');");
authorIdRs.next();
Long authorId = authorIdRs.getLong(1);
// write new author
psAuthor.setLong(1, authorId);
psAuthor.setString(2, name[1]);
psAuthor.setString(3, name[0]);
psAuthor.execute();
// update book
psBook.setLong(1, authorId);
psBook.setLong(2, bookId);
psBook.execute();
}
rs.close();
psAuthor.close();
// add fk_author to book table
st.execute("ALTER TABLE book DROP COLUMN author;");
st.close();
}
}
正如你所看到的,这几乎不需要Flyway特有的代码。你只需要实现JavaMigration接口的migrate方法。在这个方法中,我们可以使用提供的Context对象来获得一个java.sql.Connection 到数据库。使用这个Connection,你就可以定义并执行所需的SQL语句。
这种方法使你可以完全灵活地从数据库中读取数据,以任何你需要的方式进行转换,并将其存储在数据库中。这使得基于Java的迁移步骤成为实现复杂的、多步骤迁移操作的最佳选择。
当你现在运行迁移时,Flyway会检测当前的数据库版本,扫描所有的SQL和Java迁移步骤并执行所需的步骤。你可以看到下面这些操作的日志输出:
15:42:53,864 INFO BaseDatabaseType:37 - Database: jdbc:postgresql://localhost:5432/test-flyway (PostgreSQL 10.14)
15:42:53,925 INFO DbValidate:37 - Successfully validated 2 migrations (execution time 00:00.023s)
15:42:53,966 INFO JdbcTableSchemaHistory:37 - Creating Schema History table "public"."flyway_schema_history" ...
15:42:54,038 INFO DbMigrate:37 - Current version of schema "public": << Empty Schema >>
15:42:54,049 INFO DbMigrate:37 - Migrating schema "public" to version "1 - create database"
15:42:54,097 INFO DbMigrate:37 - Migrating schema "public" to version "2 - extract author"
我在一个空数据库上触发了迁移,Flyway找到了版本1和2的迁移步骤。第2个是我作为一个Java类实现的迁移步骤,你在前面的代码片断中看到了。
Flyway成功执行迁移步骤后,会向flyway_schema_history表添加一条记录。

正如你所看到的,一个Java迁移步骤的使用方式与SQL脚本相同,并且完全融入到你的迁移过程中。所以,当你遇到无法用SQL描述所需迁移的情况时,你只需要实现JavaMigration接口并遵循Flyway的命名规则即可。
使用回调来完成重复性的任务
对于复杂的迁移场景,另一个有用的功能是Flyway的回调机制。它允许你在Flyway内部触发Event枚举中定义的生命周期事件时,执行一个SQL脚本或Java类。这些事件的几个例子是 AFTER_BASELINE, AFTER_CLEAN, AFTER_EACH_MIGRATE, AFTER_EACH_MIGRATE_ERROR, AFTER_UNDO, 和AFTER_MIGRATE。你可以在官方javadoc中找到所有支持的事件的列表。
在之前的文章中,我们没有讨论Flyway的回调功能。因此,在我讨论Java回调的更多细节之前,让我们也快速看一下SQL回调。
SQL回调
SQL回调的实现是很简单的。你只需要在你的迁移目录中添加一个SQL脚本,其名称为你想使用的生命周期触发器。迁移目录是Flyway命令行客户端的sql文件夹或你的Java应用程序的src/main/resources/db/migration文件夹。
所以,如果你想在Flyway迁移数据库后执行一个SQL脚本,你需要把所有的SQL语句放到一个名字为afterMigrate.sql的文件中,并把它复制到sql或者src/main/resources/db/migration文件夹中。
Java回调
如果你的回调操作对于SQL脚本来说过于复杂,你可以用Java来实现它。
回调的实现与之前讨论的迁移步骤非常相似。你需要实现Flyway的回调接口,并将你的类添加到db/callback包或由flyway.callbacks属性配置的包中。
实现回调 接口的最简单方法是扩展Flyway的BaseCallback类。它提供了所有需要的技术模板代码,这样你就可以专注于实现回调操作。
对于每个回调的 实现,Flyway都会调用之前描述的每个事件的处理 方法。当这样做时,Flyway提供了一个事件 枚举值和一个当前迁移的Context 对象。与之前描述的迁移步骤的实现类似,你可以使用Context 对象来获得一个与数据库的连接并执行回调的操作。
我在下面的例子中使用了这一点,实现了一个回调,在数据库为空的情况下添加一些示例数据。要做到这一点,我首先检查书表 是否包含任何数据。如果没有,我就向作者和书表插入一条记录:
public class FillDatabaseAfterMigrate extends BaseCallback {
Logger log = Logger.getLogger(FillDatabaseAfterMigrate.class.getSimpleName());
@Override
public void handle(Event event, Context context) {
if (event == Event.AFTER_MIGRATE) {
log.info("afterMigrate");
Statement st;
try {
st = context.getConnection().createStatement();
ResultSet rs = st.executeQuery("SELECT count(id) FROM book");
rs.next();
if (rs.getInt(1) == 0) {
st.execute(
"INSERT INTO author (id, firstname, lastname) VALUES ((SELECT nextval('author_seq')), 'Thorben', 'Janssen');");
st.execute(
"INSERT INTO book (id, publishingdate, title, fk_author, price) VALUES ((SELECT nextval('book_seq')), '2017-04-04', 'Hibernate Tips - More than 70 solutions to common Hibernate problems', 1, 9.99);");
log.info("Database was empty. Added example data.");
} else {
log.info("Database contains books. No example data needed.");
return;
}
} catch (SQLException e) {
throw new MigrationException(e);
}
}
}
public class MigrationException extends RuntimeException {
public MigrationException(Throwable cause) {
super(cause);
}
}
}
这就是你实现回调所需要做的一切。当你现在启动你的应用程序并触发数据库迁移时,Flyway将调用回调 的实现。
下面的日志输出显示,Flyway在完成迁移后调用了我们的回调实现。然后,回调实现用2条示例记录初始化了空数据库:
16:06:27,515 INFO BaseDatabaseType:37 - Database: jdbc:postgresql://localhost:5432/test-flyway (PostgreSQL 10.14)
16:06:27,605 INFO DbValidate:37 - Successfully validated 2 migrations (execution time 00:00.030s)
16:06:27,659 INFO JdbcTableSchemaHistory:37 - Creating Schema History table "public"."flyway_schema_history" ...
16:06:27,745 INFO DbMigrate:37 - Current version of schema "public": << Empty Schema >>
16:06:27,760 INFO DbMigrate:37 - Migrating schema "public" to version "1 - create database"
16:06:27,822 INFO DbMigrate:37 - Migrating schema "public" to version "2 - extract author"
16:06:27,893 INFO DbMigrate:37 - Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.162s)
16:06:27,909 INFO FillDatabaseAfterMigrate:19 - afterMigrate
16:06:27,919 INFO FillDatabaseAfterMigrate:30 - Database was empty. Added example data.
总结
我在本系列的前一篇文章中向大家展示了Flyway为实现基于版本的迁移过程提供了一种简单而强大的方法。你只需要提供一个带有所需SQL语句的脚本来更新你的数据库结构和迁移数据。
根据我的经验,你应该能够在这些SQL脚本中实现几乎所有的迁移。但正如你在这篇文章中所看到的,你并不局限于这种方法。如果你需要更多的灵活性来执行复杂的迁移操作,你可以用Java来实现它们。
而对于所有的重复性任务,如存储过程的重新编译,用样本数据进行数据库初始化,或动态创建数据库触发器,你可以在SQL脚本或Java类中实现生命周期的回调。
结合所有这些,你可以得到一个强大的工具集来实现基于版本的数据库迁移方法。