官方文檔
https://flywaydb.org/getstarted/firststeps/api[https://flywaydb.org/getstarted/firststeps/api]
入門示例
Java代碼
package foobar;
import org.flywaydb.core.Flyway;
public class App {
public static void main(String[] args) {
Flyway flyway = new Flyway();
// 指定數據源
flyway.setDataSource("jdbc:mysql://localhost/test", "root", "root");
// 開始數據遷移
flyway.migrate();
}
}
在classpath下添加SQL文件 db/migration/V1__Create_person_table.sql
create table PERSON (
ID int not null,
NAME varchar(100) not null
);
運行程序,在test數據庫會自動創建PERSON表。
后續新增表和字段,只需在db/migration目錄下新增SQL文件,格式為V${version}__${name}.sql,version值依次增加,比如V2__name.sql,V3__name.sql。
原理介紹
Flyway的數據庫遷移的實現原理是,從classpath或文件系統中找到符合規則的數據庫遷移腳本,比如db/migration目錄下命名規則為V${version}__${name}.sql的文件,將腳本按照version進行排序,依次執行。執行過的腳本會作為一條記錄,存儲在schema_version表中。當下次執行遷移時,判斷腳本已經執行,則跳過。
MigrationResolver接口負責查找數據庫遷移腳本,方法為resolveMigrations(),數據庫遷移腳本用ResolvedMigration對象表示。MigrationResolver包含多種實現類,比如SqlMigrationResolver會從classpath下查找sql文件。查詢通過Scanner類實現,Location類指定查詢路徑,sql文件的命名規則需要符合V${version}__${name}.sql,規則中的前綴V、后綴.sql、分隔符__均在FlywayConfiguration接口中定義。另一種實現類JdbcMigrationResolver會從classpath下查找實現JdbcMigration接口的類,類的命名規則需要符合V${version}__${name}。需要擴展自己的實現類,可以繼承BaseMigrationResolver。
MetaDataTable接口負責查找已執行的數據庫遷移腳本,方法為findAppliedMigrations(),已執行的數據庫遷移腳本用AppliedMigration對象表示。MetaDataTable只有一種實現類MetaDataTableImpl,從數據庫schema_version表查詢所有記錄。
ResolvedMigration集合包含了已經執行的AppliedMigration集合,在執行ResolvedMigration前,需要對比AppliedMigration,找到已執行和未執行的ResolvedMigration,對比通過MigrationInfoServiceImpl.refresh()實現。已執行的ResolvedMigration需要校驗文件有沒有發生變化,有變更則提示錯誤。未執行的ResolvedMigration依次執行,執行結果記錄在schema_version表中。
Flyway的主要方法:
public class Flyway {
/**數據庫遷移*/
public int migrate();
/**校驗已執行的遷移操作的變更情況*/
public void validate();
/**清理數據庫*/
public void clean();
/**設置數據庫的基准版本*/
public void baseline();
/**刪除執行錯誤的遷移記錄*/
public void repair();
/**准備執行環境,並執行Command操作,以上方法都調用了execute()來執行操作*/
<T> T execute(Command<T> command);
}
接下來我們分析Flyway.migrate()代碼執行的主邏輯。
public void migrate() {
//由Flyway.execute()准備Command.execute()執行所需要的參數
return execute(new Command<Integer>() {
public Integer execute(Connection connectionMetaDataTable, MigrationResolver migrationResolver, MetaDataTable metaDataTable, DbSupport dbSupport, Schema[] schemas, FlywayCallback[] flywayCallbacks) {
//為了簡化代碼,忽略參數傳遞
doMigrate();
}
});
}
private void doMigrate() {
//校驗已執行的遷移操作的變更情況
if (validateOnMigrate) {
doValidate(connectionMetaDataTable, dbSupport, migrationResolver,
metaDataTable, schemas, flywayCallbacks, true);
}
//如果尚未進行數據遷移,即schema_version表中不存在數據,
//並且數據庫不為空,則插入一條baseline信息
if (!metaDataTable.exists()) {
//數據庫不為空
if (!nonEmptySchemas.isEmpty()) {
//插入一條baseline信息
new DbBaseline(connectionMetaDataTable, dbSupport, metaDataTable,
schemas[0], baselineVersion, baselineDescription, flywayCallbacks).baseline();
}
}
//進行數據遷移
DbMigrate dbMigrate = new DbMigrate(connectionUserObjects, dbSupport,
metaDataTable,schemas[0], migrationResolver, ignoreFailedFutureMigration,
Flyway.this);
return dbMigrate.migrate();
}
接下來看DbMigrate.migrate()的代碼片段。
MigrationInfoServiceImpl對比ResolvedMigration和AppliedMigration對象,找出需要執行數據庫遷移腳本,通過pending()方法返回。最后執行數據庫遷移腳本。
public int migrate() {
int migrationSuccessCount = 0;
while (true) {
int count = metaDataTable.lock(new Callable<Integer>() {
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> Integer <span class="hljs-title">call</span><span class="hljs-params">()</span> </span>{
<span class="hljs-comment">//為了簡化代碼,忽略參數傳遞</span>
<span class="hljs-keyword">return</span> doMigrate();
}
}
<span class="hljs-keyword">if</span> (count == <span class="hljs-number">0</span>) {
<span class="hljs-comment">// No further migrations available</span>
<span class="hljs-keyword">break</span>;
}
migrationSuccessCount += count;
}
return migrationSuccessCount;
}
private int doMigrate() {
//收集已經入庫的數據庫遷移記錄,和以文件形式存在的數據庫遷移腳本
MigrationInfoServiceImpl infoService = new MigrationInfoServiceImpl(
migrationResolver, metaDataTable, configuration.getTarget(),
configuration.isOutOfOrder(), true, true, true);
infoService.refresh();
//infoService.pending()記錄將要執行的數據庫遷移腳本
LinkedHashMap<MigrationInfoImpl, Boolean> group =
groupMigrationInfo(infoService.pending());
if (!group.isEmpty()) {
//執行數據庫遷移操作
applyMigrations(group);
}
}
DbMigrate.doMigrateGroup() 執行數據庫遷移腳本
private void doMigrateGroup() {
//執行遷移腳本
migration.getResolvedMigration().getExecutor().execute(connectionUserObjects);
//存入數據庫
AppliedMigration appliedMigration = new AppliedMigration(migration.getVersion(),
migration.getDescription(), migration.getType(), migration.getScript(),
migration.getResolvedMigration().getChecksum(), executionTime, true);
metaDataTable.addAppliedMigration(appliedMigration);
}
擴展練習
在一個已經上線的游戲項目中引入Flyway框架,對於已經存在的游戲服,只執行新增的sql語句,對於新搭建的游戲服,需要創建數據庫,並執行新增的sql語句。
在這個需求中,需要做的是對於新搭建的游戲服,需要創建數據庫。我們可以通過FlywayCallback實現這一點,如果指定數據庫為空,則執行初始化數據庫的語句。
代碼如下:
public static void main(String[] args) {
final Flyway flyway = new Flyway();
flyway.setDataSource("jdbc:mysql://localhost/test", "root", "root");
FlywayCallback flywayCallback = <span class="hljs-keyword">new</span> BaseFlywayCallback() {
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">beforeMigrate</span><span class="hljs-params">(Connection connection)</span> </span>{
DbSupport dbSupport = DbSupportFactory.createDbSupport(connection, <span class="hljs-keyword">false</span>);
<span class="hljs-keyword">if</span>(!hasTable(dbSupport)) {
initDb(dbSupport);
}
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">hasTable</span><span class="hljs-params">(DbSupport dbSupport)</span> </span>{
Schema<?> schema = dbSupport.getOriginalSchema();
Table[] tables = schema.allTables();
<span class="hljs-keyword">if</span>(tables == <span class="hljs-keyword">null</span> || tables.length == <span class="hljs-number">0</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
<span class="hljs-comment">//忽略表 schema_version</span>
<span class="hljs-keyword">if</span>(tables.length == <span class="hljs-number">1</span> &&
tables[<span class="hljs-number">0</span>].getName().equalsIgnoreCase(<span class="hljs-string">"schema_version"</span>)) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">initDb</span><span class="hljs-params">(DbSupport dbSupport)</span> </span>{
Scanner scanner = <span class="hljs-keyword">new</span> Scanner(
Thread.currentThread().getContextClassLoader());
Resource[] resources = scanner.scanForResources(
<span class="hljs-keyword">new</span> Location(<span class="hljs-string">"db/init"</span>), <span class="hljs-string">""</span>, <span class="hljs-string">".sql"</span>);
<span class="hljs-keyword">if</span>(resources == <span class="hljs-keyword">null</span> || resources.length == <span class="hljs-number">0</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(
<span class="hljs-string">"db/init/*.sql not found in the classpath. "</span>);
}
<span class="hljs-keyword">for</span>(Resource resource : resources) {
SqlMigrationExecutor executor = <span class="hljs-keyword">new</span> SqlMigrationExecutor(
dbSupport, resource, PlaceholderReplacer.NO_PLACEHOLDERS, flyway);
executor.execute(dbSupport.getJdbcTemplate().getConnection());
}
}
};
List<FlywayCallback> callbacks = <span class="hljs-keyword">new</span> ArrayList<FlywayCallback>(
Arrays.asList(flyway.getCallbacks()));
callbacks.add(flywayCallback);
flyway.setCallbacks(callbacks.toArray(<span class="hljs-keyword">new</span> FlywayCallback[callbacks.size()]));
flyway.setBaselineOnMigrate(<span class="hljs-keyword">true</span>);
flyway.repair();
flyway.migrate();
}
</div>