ねこじゃすり
猫を魅了する魔法の装備品!
Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
ペヤング ソースやきそば 120g×18個
とりあえず保存食として買っておけば間違いなし!
レッドブル エナジードリンク 250ml×24本
翼を授けよう!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
スポンサーリンク
目次
パッチ管理を簡単にしよう
前回は、APIシステムをレイヤー構造にリファクタリングして動くところまでをご紹介しました。
今回は、データベース接続をするための準備として、DB定義用のパッチSQLファイルを用意し、Gradle経由でFlywayを用いて自動で適用させるところまでを整えていこうと思います。
Flywayとは
Flywayは、オープンソースで開発されているデータベースのマイグレーションツールです。
Version control for your database.
Robust schema evolution across all your environments.
With ease, pleasure and plain SQL.Flyway by Boxfuse • Database Migrations Made Easy.
特定のルールに沿ってパッチファイルを管理することで、コマンドライン一発でDBのマイグレーションを行うことが出来るので、当て忘れや順番を間違えたりなどのヒューマンエラーを避ける事が出来ます。
また、簡単にデータベースをリセットしたり、afterMigrate機能を使って開発データを入れる事が出来るので開発においても非常に有効なツールとなっています。
今回はGradleのSpringBootプロジェクトへのFlywayの導入方法と簡単なマイグレーション処理をご紹介していこうと思います。
手順
完成系
今回は以下のような非常にシンプルなテーブル構造を作ってみようと思います。
テーブル名 | 用途 |
---|---|
account.accounts | アカウントのメインレコードを管理 |
account.account_infomations | アカウントの基本情報を管理 |
account.sex_types | 性別マスター |
account.current_account_infomations | 各アカウントの基本情報レコードは蓄積を想定するため、現在の有効な基本情報をマッピングするためのテーブル |
application-{profile}.ymlの設定
まずは、Flywayを実行するにあたって必要な項目をapplication-{profile}.yml
に定義していきます。
なお、springbootには公式でFlywayの設定項目がありますが、今回は設定が反映されてしまって起動時に自動実行させたくないのもあるため、意図的にflywayMigrate
という独自階層で定義していきます。
1 2 3 4 5 6 7 8 9 10 11 |
spring: flywayMigrate: # 独自項目 driver: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/local_account_db username: root password: root encoding: UTF-8 sqlMigrationPrefix: patch_ migrateLocation: filesystem:../datasource/flyway/migration/migrate isAfterMigratable: true # afterMigrateは環境によっては実行したくないこともあると思うのでbool値を定義 afterMigrateLocation: filesystem:../datasource/flyway/migration/aftermigrate |
build.gradleの修正
datasourceプロジェクト
今回はシステム内部からFlywayを呼ぶことは想定していないので、dependencies
への追加は不要です。
代わりにGradleから実行するためにplugins
に指定をする必要があります。
datasource/build.gradle
を以下のように修正してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
plugins { id "org.flywaydb.flyway" version "5.1.3" // pluginsブロックはConstantの必要があるとのこと } def env = System.getenv( 'SPRING_PROFILES_ACTIVE' ) // 実行時に定義した環境変数からprofileを取得 def path = "${accountService.applicationYmlPath}"; def applicationYml = new org.yaml.snakeyaml.Yaml().load( new File( path + "application-" + env +".yml").newInputStream() )// 対象プロファイルのymlを読み込み flyway { // flywayの初期設定 driver="${applicationYml.spring.flywayMigrate.driver}" url = "${applicationYml.spring.flywayMigrate.url}" user = "${applicationYml.spring.flywayMigrate.username}" password = "${applicationYml.spring.flywayMigrate.password}" encoding = "${applicationYml.spring.flywayMigrate.encoding}" sqlMigrationPrefix = "${applicationYml.spring.flywayMigrate.sqlMigrationPrefix}" if("${applicationYml.spring.flywayMigrate.isAfterMigratable}".toBoolean()){ // afterMigrateをする場合のみafterMigrateのパスを追加 locations = [ "${applicationYml.spring.flywayMigrate.migrateLocation}", "${applicationYml.spring.flywayMigrate.afterMigrateLocation}" ] }else{ locations = [ "${applicationYml.spring.flywayMigrate.migrateLocation}" ] } } dependencies { implementation project(":value") implementation project(":entity") } |
devtoolsプロジェクト
devtools/build.gradle
ではgradle実行時に必要なPostgreSQLのドライバーとymlファイルを読み込むためのライブラリをクラスパスに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
buildscript { apply from: 'property.gradle' ext { } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${accountService.springBootVersion}") classpath "org.yaml:snakeyaml:${accountService.snakeyamlVersion}"// 追加 classpath "org.postgresql:postgresql:${accountService.postgresVersion}"// 追加 } } allprojects { apply plugin: 'eclipse' } subprojects { apply plugin: "java" apply plugin: "io.spring.dependency-management" group = accountService.group version = accountService.version sourceCompatibility = accountService.sourceCompatibility repositories { mavenCentral() } dependencyManagement { } dependencies { implementation "org.springframework.boot:spring-boot-starter:${accountService.springBootVersion}" testImplementation "org.springframework.boot:spring-boot-starter-test:${accountService.springBootVersion}" } } |
パッチファイル
次にパッチファイルを用意しましょう。今回は以下のような形で用意しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
.datasource ... 省略 └── flyway └── migration ├── afterMigrate │ └── afterMigrate.sql └── migrate └── 001 └── 000 ├── patch_001.000.001__create_schema_account.sql ├── patch_001.000.002__create_table_account.accounts.sql ├── patch_001.000.003__create_sequence_account.account_id.sql ├── patch_001.000.004__create_sequence_account.account_infomation_id.sql ├── patch_001.000.005__create_table_account.sex_type.sql ├── patch_001.000.006__insert_account.sex_type.sql └── patch_001.000.007__create_table_account.account_infomations.sql |
1 2 3 4 |
DROP SCHEMA IF EXISTS account CASCADE; CREATE SCHEMA IF NOT EXISTS account; COMMENT ON SCHEMA account IS 'アカウント'; |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CREATE TABLE account.accounts ( id integer NOT NULL, created_at timestamp without time zone NOT NULL DEFAULT now(), updated_at timestamp without time zone NOT NULL DEFAULT now(), CONSTRAINT pk_accounts PRIMARY KEY (id) ); COMMENT ON TABLE account.accounts IS 'アカウント一覧'; COMMENT ON COLUMN account.accounts.id IS 'ID'; COMMENT ON COLUMN account.accounts.created_at IS '作成日時'; COMMENT ON COLUMN account.accounts.updated_at IS '更新日時'; |
1 |
CREATE SEQUENCE account.account_id START WITH 10001; |
1 |
CREATE SEQUENCE account.account_infomation_id START WITH 20001; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
CREATE TABLE account.sex_types ( type character varying(50) NOT NULL, name character varying(50) NOT NULL, priority smallint NOT NULL, created_at timestamp without time zone NOT NULL DEFAULT now(), updated_at timestamp without time zone NOT NULL DEFAULT now(), CONSTRAINT pk_sex_types PRIMARY KEY (type) ); COMMENT ON TABLE account.sex_types IS '性別種別マスタ'; COMMENT ON COLUMN account.sex_types.type IS '性別種別'; COMMENT ON COLUMN account.sex_types.name IS '性別種別名'; COMMENT ON COLUMN account.sex_types.priority IS '優先度'; COMMENT ON COLUMN account.sex_types.created_at IS '作成日時'; COMMENT ON COLUMN account.sex_types.updated_at IS '更新日時'; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
INSERT INTO account.sex_types ( type, name, priority ) VALUES ( 'NONE', '未設定', 0 ),( 'MALE', '男性', 1 ),( 'FEMALE', '女性', 2 ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
CREATE TABLE account.account_infomations ( id integer NOT NULL, account_id integer NOT NULL, name character varying(50) NOT NULL, age integer NOT NULL, sex_type character varying(50) NOT NULL, display_name character varying(50) NOT NULL, created_at timestamp without time zone NOT NULL DEFAULT now(), updated_at timestamp without time zone NOT NULL DEFAULT now(), CONSTRAINT pk_account_infomations PRIMARY KEY (id), CONSTRAINT fk_this_accounts FOREIGN KEY (account_id) REFERENCES account.accounts (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_this_sex_types FOREIGN KEY (sex_type) REFERENCES account.sex_types (type) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); COMMENT ON TABLE account.account_infomations IS 'アカウント情報'; COMMENT ON COLUMN account.account_infomations.id IS 'ID'; COMMENT ON COLUMN account.account_infomations.account_id IS 'アカウントID'; COMMENT ON COLUMN account.account_infomations.name IS '名前'; COMMENT ON COLUMN account.account_infomations.age IS '年齢'; COMMENT ON COLUMN account.account_infomations.sex_type IS '性別'; COMMENT ON COLUMN account.account_infomations.display_name IS '画面表示名称'; COMMENT ON COLUMN account.account_infomations.created_at IS '作成日時'; COMMENT ON COLUMN account.account_infomations.updated_at IS '更新日時'; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
CREATE TABLE account.current_account_infomations ( account_id integer NOT NULL, CREATE TABLE account.current_account_infomations ( account_id integer NOT NULL, account_infomation_id integer NOT NULL, created_at timestamp without time zone NOT NULL DEFAULT now(), updated_at timestamp without time zone NOT NULL DEFAULT now(), CONSTRAINT pk_latest_account_infomations PRIMARY KEY (account_id), CONSTRAINT fk_this_accounts FOREIGN KEY (account_id) REFERENCES account.accounts (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_this_account_infomations FOREIGN KEY (account_infomation_id) REFERENCES account.account_infomations (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT uq_current_account_infomations_account_infomation_id UNIQUE (account_infomation_id) ); COMMENT ON TABLE account.current_account_infomations IS '現行アカウント情報'; COMMENT ON COLUMN account.current_account_infomations.account_id IS 'アカウントID'; COMMENT ON COLUMN account.current_account_infomations.account_infomation_id IS 'アカウント情報ID'; COMMENT ON COLUMN account.current_account_infomations.created_at IS '作成日時'; COMMENT ON COLUMN account.current_account_infomations.updated_at IS '更新日時'; |
afterMigrateファイル
次にafterMigrateファイルを用意しましょう。
afterMigrateとは、パッチファイルの適用が終了した際に一度だけ適用されるSQLファイルの事を指します。
こちらを使うことで、各環境で同一データを使って動作確認を行ったり、簡単にデータベースのデータを同一の状態に戻すことが出来るのでとても便利です♪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
-- #################### -- # アカウント1 -- #################### INSERT INTO account.accounts ( id ) VALUES ( (SELECT nextval('account.account_id')) ); INSERT INTO account.account_infomations ( id, account_id, name, age, sex_type, display_name ) values ( (SELECT nextval('account.account_infomation_id')), (SELECT MAX(id) FROM account.accounts), '山田 太郎', 19, 'MALE', '山田 ローカル 太郎' ); INSERT INTO account.current_account_infomations ( account_id, account_infomation_id ) values ( (SELECT MAX(id) FROM account.accounts), (SELECT MAX(id) FROM account.account_infomations) ); -- #################### -- # アカウント2 -- #################### INSERT INTO account.accounts ( id ) VALUES ( (SELECT nextval('account.account_id')) ); INSERT INTO account.account_infomations ( id, account_id, name, age, sex_type, display_name ) values ( (SELECT nextval('account.account_infomation_id')), (SELECT MAX(id) FROM account.accounts), '田中 花子', 20, 'FEMALE', '田中 ローカル 花子' ); INSERT INTO account.current_account_infomations ( account_id, account_infomation_id ) values ( (SELECT MAX(id) FROM account.accounts), (SELECT MAX(id) FROM account.account_infomations) ); -- #################### -- # アカウント3 -- #################### INSERT INTO account.accounts ( id ) VALUES ( (SELECT nextval('account.account_id')) ); INSERT INTO account.account_infomations ( id, account_id, name, age, sex_type, display_name ) values ( (SELECT nextval('account.account_infomation_id')), (SELECT MAX(id) FROM account.accounts), '山田 二郎', 1, 'NONE', '山田 ローカル 二郎' ); INSERT INTO account.current_account_infomations ( account_id, account_infomation_id ) values ( (SELECT MAX(id) FROM account.accounts), (SELECT MAX(id) FROM account.account_infomations) ); |
実行確認
以下のコマンドを実行してみてください。
1 2 3 4 |
Starting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details BUILD SUCCESSFUL in 7s 2 actionable tasks: 2 executed |
確認
それでは、DBの中身を見てみましょう。
1 2 3 4 5 6 7 |
local_account_db=# \dn List of schemas Name | Owner ---------+---------- account | root public | postgres (2 rows) |
1 2 3 4 5 6 7 8 9 |
local_account_db=# \dt List of relations Schema | Name | Type | Owner ---------+-----------------------------+-------+------- account | account_infomations | table | root account | accounts | table | root account | current_account_infomations | table | root account | sex_types | table | root (4 rows) |
1 2 3 4 5 6 7 8 |
local_account_db=# select * from account.accounts INNER JOIN account.account_infomations ON accounts.id = account_infomations.account_id INNER JOIN account.sex_types ON account_infomations.sex_type = sex_types.type INNER JOIN account.current_account_infomations ON accounts.id = current_account_infomations.account_id ; id | created_at | updated_at | id | account_id | name | age | sex_type | display_name | created_at | updated_at | type | name | priority | created_at | updated_at | account_id | account_infomation_id | created_at | updated_at -------+----------------------------+----------------------------+-------+------------+------------+-----+----------+----------------------+----------------------------+----------------------------+--------+--------+----------+----------------------------+----------------------------+------------+-----------------------+----------------------------+---------------------------- 10001 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 | 20001 | 10001 | 山田 太郎 | 19 | MALE | 山田 ローカル 太郎 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 | MALE | 男性 | 1 | 2019-04-20 20:29:26.269542 | 2019-04-20 20:29:26.269542 | 10001 | 20001 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 10002 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 | 20002 | 10002 | 田中 花子 | 20 | FEMALE | 田中 ローカル 花子 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 | FEMALE | 女性 | 2 | 2019-04-20 20:29:26.269542 | 2019-04-20 20:29:26.269542 | 10002 | 20002 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 10003 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 | 20003 | 10003 | 山田 二郎 | 1 | NONE | 山田 ローカル 二郎 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 | NONE | 未設定 | 0 | 2019-04-20 20:29:26.269542 | 2019-04-20 20:29:26.269542 | 10003 | 20003 | 2019-04-20 20:29:26.320925 | 2019-04-20 20:29:26.320925 (3 rows) |
正常にデータベース構造が構築され、AfterMigrateのデータも登録されていることが分かりますね♪
Flywayの適用状態はpublic.flyway_schema_historyが自動で生成されてそちらで管理されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
local_account_db=# select * from public.flyway_schema_history; installed_rank | version | description | type | script | checksum | installed_by | installed_on | execution_time | success ----------------+-------------+-----------------------------------------------+------+---------------------------------------------------------------------- --------+-------------+--------------+----------------------------+----------------+--------- 1 | 001.000.001 | create schema account | SQL | 001/000/patch_001.000.001__create_schema_account.sql | -1992181989 | root | 2019-04-16 22:34:44.833448 | 10 | t 2 | 001.000.002 | create table account.accounts | SQL | 001/000/patch_001.000.002__create_table_account.accounts.sql | -1355234609 | root | 2019-04-16 22:34:44.860296 | 14 | t 3 | 001.000.003 | create sequence account.account id | SQL | 001/000/patch_001.000.003__create_sequence_account.account_id.sql | -1825999221 | root | 2019-04-16 22:34:44.886777 | 5 | t 4 | 001.000.004 | create sequence account.account infomation id | SQL | 001/000/patch_001.000.004__create_sequence_account.account_infomation_id.sql | 882609325 | root | 2019-04-16 22:34:44.908961 | 5 | t 5 | 001.000.005 | create table account.sex type | SQL | 001/000/patch_001.000.005__create_table_account.sex_type.sql | -326431366 | root | 2019-04-16 22:34:44.965301 | 20 | t 6 | 001.000.006 | insert account.sex type | SQL | 001/000/patch_001.000.006__insert_account.sex_type.sql | -1712490080 | root | 2019-04-16 22:34:45.005898 | 5 | t 7 | 001.000.007 | create table account.account infomations | SQL | 001/000/patch_001.000.007__create_table_account.account_infomations.sql | -1486580974 | root | 2019-04-16 22:34:45.034252 | 24 | t (7 rows) |
また、afterMigrateを行いたくない場合はisAfterMigratable
をfalse
にするだけでスキップされます。
検証や本番ではfalse
にしておくと良いでしょう。
最初からDB定義を作り直したい場合
ローカル開発等で現在のデータベース構造をリセットして、最初からパッチやafterMigrateを当て直したい場合は、SPRING_PROFILES_ACTIVE=local ./gradlew flywayClean flywayMigrate
で簡単に出来ちゃいます。
終わりに
これでDB定義の更新がコマンド一つでサクッと出来るようになりました。
次回はStubにしているデータソースの処理を実際にDBから取得するように変えていきたいと思います。