javaでもrailsのようなmigrationをするツールはいくつかあります。
Liquibase,Flyway,MyBatis Migrationsなどです。
今回は、以前説明したDropwizardがLiquibaseを同梱しているので、そのサンプルを使って説明したいと思います。
ちなみにLiquibaseとFlywayの違いはこちらにわかりやすく書かれていました。
概要
Liquibaseを一言で説明すると「DBスキーマの変更管理を行うツール」になります。
アジャイルで開発していくとコードはどんどん変更されていきますし、それと同じようにDBスキーマも変更されていくことになります。
そういう場合にこういうツールがないと変更が面倒で、開発スピードが落ちたり、最悪の場合は、変更したくないために間違った方向へプロダクトを誘導しかねません。
現在のように、柔軟で迅速に開発できなければ生き残れない時代には必須のツールになります。
スキーマの変更履歴を次のフォーマットで記述できます。
- XML
- YAML
- JSON
- SQL
- Other(Groovy, Clojure)
その後コマンドを叩くことで対象のバージョンにすることができます。もちろん、前のバージョンに戻すこともできます。
また、DB製品に依存しないので、どのDB製品を利用するかの判断を後に持ち越すこともできますし、変更も容易にできます。
前提
以前の環境の続きになります。
前回のサンプルに追加していきます。
maven設定
今回はsampleなので組み込みDBのh2databaseを使います。
他のDBを使う場合は、対象のJDBCドライバーを依存させてください。
マイグレーションのためにdropwizard-migrationsを依存させます。
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.175</version>
</dependency>
Configurationクラス
DataSourceFactoryは、yamlのdatabase部分に対応するDropwizardのConfigurationクラスになります。
これは、Dropwizardが管理可能な状態を維持するために設定ファイルとConfigurationクラスをグループ化することを推奨している設計方針だからです。
@Valid
@NotNull
private DataSourceFactory database = new DataSourceFactory();
@JsonProperty("database")
public DataSourceFactory getDataSourceFactory() {
return database;
}
@JsonProperty("database")
public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
this.database = dataSourceFactory;
}
# Database settings.
database:
# the name of your JDBC driver
driverClass: org.h2.Driver
# the username
user: sa
# the password
password: sa
# the JDBC URL
url: jdbc:h2:target/example
Applicationクラス
MigrationsBundleクラスを登録します。これはDropwizardで用意されてるabstractクラスになります。
@Override
public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
・・・
bootstrap.addBundle(new MigrationsBundle<HelloWorldConfiguration>() {
@Override
public DataSourceFactory getDataSourceFactory(
HelloWorldConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
・・・
}
テーブル作成
Liquibaseのxml
まずは、テーブル作成するためにxmlに記述します。
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="1" author="ko2ic">
<createTable tableName="people">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fullName" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="jobTitle" type="varchar(255)"/>
</createTable>
</changeSet>
</databaseChangeLog>
<changeSet>にスキーマの変更を書きます。
DDLのトランザクションをサポートしているDBの場合は、<changeSet>ごとにトランザクションが発生するので、エラーの場合はロールバックされます。
属性のidは識別子、authorは変更者名を記述します。両方とも必須になります。
db migrate --dry-run
mvn package
後、テーブル作成します。その前に--dry-run
オプションを付けてどのようなSQLが流れるかを確認します。
$ java -jar target/spike-dropwizard-1.0-SNAPSHOT.jar db migrate example.yml --dry-run
・・・
-- Changeset migrations.xml::1::ko2ic
CREATE TABLE PUBLIC.people (id BIGINT AUTO_INCREMENT NOT NULL, fullName VARCHAR(255) NOT NULL, jobTitle VARCHAR(255), CONSTRAINT PK_PEOPLE PRIMARY KEY (id));
INSERT INTO PUBLIC.DATABASECHANGELOG (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, LIQUIBASE) VALUES ('1', 'ko2ic', 'migrations.xml', NOW(), 2, '7:466c815ef370d0ef6b1526a636507dab', 'createTable', '', 'EXECUTED', '3.1.1');
・・・
このようにxmlによってどのようなSQLが流れるかが確認できます。
問題ないので、テーブル作成します。
その前に毎回長いコマンドを打ち込むのは大変なので、aliasを定義しておきます。
$ alias spike="java -jar target/spike-dropwizard-1.0-SNAPSHOT.jar"
今後はalias名を使います。
db migrate
まずは、changelogをDBに反映させます。
$ spike db migrate example.yml
確認してみます。
$ java -cp ~/.m2/repository/com/h2database/h2/1.3.175/h2-1.3.175.jar org.h2.tools.Server
H2 Consoleの結果です。
src/main/resources/migrations.xmlで定義したPEOPLEテーブルを確認できます。
他にdatabasechangelog、databasechangeloglockが出来ています。
databasechangelogは、DBに対して実行された情報が格納されています。
databasechangeloglockは、同時に更新しないようにするためのLockテーブルになります。
db tag
tagを打っておきます。今回はfirstという名前にしました。
databasechangelogテーブルの現在の行(ID=1)のTAGに「first」としてupdateされます。
spike db tag example.yml first
テーブル変更
テーブルに論理削除フラグを追加しようと思います。
まずは、changeSetに追記します。id=2が追記したものです。
<changeSet id="1" author="ko2ic">
<createTable tableName="people">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="fullName" type="varchar(255)">
<constraints nullable="false" />
</column>
<column name="jobTitle" type="varchar(255)" />
</createTable>
</changeSet>
<changeSet id="2" author="ko2ic">
<addColumn tableName="people">
<column name="deleteFlag" type="boolean" defaultValue="0">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
db dump
更新する前に現在のスキーマの状態を表示してみましょう。
$ spike db dump example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="ko2ic (generated)" id="1402323515137-1">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="PEOPLE">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_PEOPLE"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="JOBTITLE" type="VARCHAR(255)"/>
</createTable>
</changeSet>
</databaseChangeLog>
更新します。
$ spike db migrate example.yml
確認します。DELETEFLAGカラムが追加されているのがわかります。
$ spike db dump example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="ko2ic (generated)" id="1402325233491-1">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="PEOPLE">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_PEOPLE"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="JOBTITLE" type="VARCHAR(255)"/>
<column defaultValueComputed="0" name="DELETEFLAG" type="BOOLEAN(1)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
スキーマバージョン変更
ロールバックのテスト用にスキーマ変更します。
<changeSet id="3" author="ko2ic">
<createTable tableName="temp">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="fullName" type="varchar(255)">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
スキーマ変更後,tempテーブルが追加されていることを確認。
$ mvn package
・・・
$ spike db migrate example.yml
・・・
$ spike db dump example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="ko2ic (generated)" id="1402332311215-1">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="PEOPLE">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_PEOPLE"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="JOBTITLE" type="VARCHAR(255)"/>
<column defaultValueComputed="0" name="DELETEFLAG" type="BOOLEAN(1)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="ko2ic (generated)" id="1402332311215-2">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="TEMP">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_TEMP"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
db rollback --tag
tagのバージョンにスキーマを戻します。
$ spike db rollback example.yml --tag first
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
INFO [2014-06-09 17:13:46,569] liquibase: Successfully acquired change log lock
INFO [2014-06-09 17:13:47,286] liquibase: Reading from PUBLIC.DATABASECHANGELOG
INFO [2014-06-09 17:13:47,297] liquibase: Reading from PUBLIC.DATABASECHANGELOG
INFO [2014-06-09 17:13:47,299] liquibase: migrations.xml::3::ko2ic: Rolling Back Changeset:migrations.xml::3::ko2ic
INFO [2014-06-09 17:13:47,302] liquibase: migrations.xml::3::ko2ic: Reading from PUBLIC.DATABASECHANGELOG
INFO [2014-06-09 17:13:47,311] liquibase: migrations.xml::3::ko2ic: Reading from PUBLIC.DATABASECHANGELOG
INFO [2014-06-09 17:13:47,314] liquibase: migrations.xml::2::ko2ic: Rolling Back Changeset:migrations.xml::2::ko2ic
INFO [2014-06-09 17:13:47,316] liquibase: migrations.xml::2::ko2ic: Reading from PUBLIC.DATABASECHANGELOG
INFO [2014-06-09 17:13:47,329] liquibase: migrations.xml::2::ko2ic: Reading from PUBLIC.DATABASECHANGELOG
INFO [2014-06-09 17:13:47,332] liquibase: Successfully released change log lock
$ spike db dump example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="ko2ic (generated)" id="1402334034829-1">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="PEOPLE">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_PEOPLE"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="JOBTITLE" type="VARCHAR(255)"/>
</createTable>
</changeSet>
</databaseChangeLog>
db status
DBに反映していないChange Set数を確認します。xml上では3つのChange Setがあり、上記で1番目のChange Setにしたので2つあるはずです。
$ spike db status example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
2 change sets have not been applied to SA@jdbc:h2:target/example
・・・
db migrate --count
DBに反映していないChange Setを反映させます。現在、1なので1つ反映させるには1を指定します。
$ spike db migrate example.yml --count 1
確認すると1つ反映されていることがわかります。
$ spike db status example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
1 change sets have not been applied to SA@jdbc:h2:target/example
db test
これは本番環境では使ってはいけません。ロールバックできるかどうかのテストをします。
その際にまず、すべてをmigrateし、その後すべてをrollbackして再びmigrateします。
$ spike db test example.yml
$ spike db dump example.yml
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="ko2ic (generated)" id="1402397482577-1">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="PEOPLE">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_PEOPLE"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="JOBTITLE" type="VARCHAR(255)"/>
<column defaultValueComputed="0" name="DELETEFLAG" type="BOOLEAN(1)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="ko2ic (generated)" id="1402397482577-2">
<createTable catalogName="EXAMPLE" schemaName="PUBLIC" tableName="TEMP">
<column autoIncrement="true" name="ID" type="BIGINT(19)">
<constraints primaryKey="true" primaryKeyName="PK_TEMP"/>
</column>
<column name="FULLNAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
db fast-forward
DBに反映していない1つ先のChange Setを実際はDBに適用しないで適用済みにします。
$ spike db fast-forward example.yml
テーブル削除
drop-all --confirm-delete-everything
自分の作ったテーブルを削除します。liquibaseの2つのテーブルは残りますが、行は削除されます。
$ spike db drop-all --confirm-delete-everything example.yml
ドキュメント生成
$ spike db generate-docs ./doc