Dropwizard(Java)でrailsのようにDBマイグレーションをする


javaでもrailsのようなmigrationをするツールはいくつかあります。
[Liquibase](http://www.liquibase.org/),[Flyway](http://flywaydb.org/),[MyBatis Migrations](http://mybatis.github.io/migrations/)などです。

今回は、[以前](http://qiita.com/ko2ic/items/069c5f75823fd8964bfa)説明したDropwizardがLiquibaseを同梱しているので、そのサンプルを使って説明したいと思います。

ちなみにLiquibaseとFlywayの違いは[こちら](http://qiita.com/scova0731/items/a0c634f4a374ec8b1c9a)にわかりやすく書かれていました。

# 概要

Liquibaseを一言で説明すると「DBスキーマの変更管理を行うツール」になります。
アジャイルで開発していくとコードはどんどん変更されていきますし、それと同じようにDBスキーマも変更されていくことになります。
そういう場合にこういうツールがないと変更が面倒で、開発スピードが落ちたり、最悪の場合は、変更したくないために間違った方向へプロダクトを誘導しかねません。

現在のように、柔軟で迅速に開発できなければ生き残れない時代には必須のツールになります。

スキーマの変更履歴を次のフォーマットで記述できます。

* XML
* YAML
* JSON
* SQL
* Other(Groovy, Clojure)

その後コマンドを叩くことで対象のバージョンにすることができます。もちろん、前のバージョンに戻すこともできます。

また、DB製品に依存しないので、どのDB製品を利用するかの判断を後に持ち越すこともできますし、変更も容易にできます。

# 前提

[以前の環境](http://qiita.com/ko2ic/items/069c5f75823fd8964bfa)の続きになります。
前回のサンプルに追加していきます。

# maven設定

今回はsampleなので組み込みDBのh2databaseを使います。
他のDBを使う場合は、対象のJDBCドライバーを依存させてください。
マイグレーションのためにdropwizard-migrationsを依存させます。

```pom.xml
    <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クラスをグループ化することを推奨している設計方針だからです。

```HelloWorldConfiguration.java
	@Valid
	@NotNull
	private DataSourceFactory database = new DataSourceFactory();

	@JsonProperty("database")
	public DataSourceFactory getDataSourceFactory() {
		return database;
	}

	@JsonProperty("database")
	public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
		this.database = dataSourceFactory;
	}
```

```example.yml
# 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クラスになります。

```HelloWorldApplication.java
	@Override
	public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
       ・・・
		bootstrap.addBundle(new MigrationsBundle<HelloWorldConfiguration>() {
			@Override
			public DataSourceFactory getDataSourceFactory(
					HelloWorldConfiguration configuration) {
				return configuration.getDataSourceFactory();
			}
		});
       ・・・
    }
```

# テーブル作成

## Liquibaseのxml

まずは、テーブル作成するためにxmlに記述します。

```src/main/resources/migrations.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の結果です。

![table1.png](https://qiita-image-store.s3.amazonaws.com/0/10494/d72c9661-bc2d-318f-0440-dd2b1f1eb871.png "table1.png")
![table2.png](https://qiita-image-store.s3.amazonaws.com/0/10494/117fce6f-4293-420a-5c96-2a796cb818b6.png "table2.png")

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が追記したものです。

```migrations.xml
	<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>
```

# スキーマバージョン変更

ロールバックのテスト用にスキーマ変更します。

```migrations.xml
    <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
```

こんなhtmlドキュメントができます。
![liquidbase-doc.png](https://qiita-image-store.s3.amazonaws.com/0/10494/8071526f-204d-0d46-fbb4-f20ac66775ef.png "liquidbase-doc.png")