JHipster によるエンタープライズアプリケーションの構築(4) Liquibaseによるデータベース管理

カバー

JHipsterはデータベースの管理にLiquibaseを使用しています。
ソースコードの自動生成だけでなく、データベースの管理も自動化されていることが大きなメリットとなっています。
この記事ではLiquibaseの概要と、JHipsterでのLiquibaseの使い方について紹介します。

Liquibaseの概要

LiquibaseはFlywayと並んで人気のあるデータベース・スキーマ管理ツールです。
FlywayがDDL(SQL)ファイルを管理することに重点を置いているのに対し、Liquibaseはスキーマの変更を抽象化して作業を効率化することに重点が置かれています。

そのため、Liquibaseはデータベース・リファクタリングツールと呼ばれることもあります。

Liquibaseではデータベースへの変更内容をXMLで管理1しており、変更内容を記載したファイルをChangelogと呼んでいます。
Changelogにはテーブルの作成、シーケンスの作成、テーブルへのカラム追加等の変更内容を記載します。
これらの1つ1つの変更内容をChangesetと呼びます。
公式ページのXMLの例を見るとイメージがしやすいかと思います。

Changelogは1回のデータベース更新を1ファイルとして作成します。1回のデータベース更新≒ロールバックの単位と考えてもらえれば良いかと思います。

そして、複数のChangelogの実行順番を取りまとめるために親となるChangelogファイルを作成します。
親となる取りまとめ用のChangelogは公式サイトではrootと命名されていますが、JHipster(Spring Boot)を使用する場合はmasterと命名されているため、この記事ではmasterと呼ぶことにします。

まとめると、Liquibaseで扱うChangelogファイルは以下のような構成になっています。

- Changelog master(XMLファイル)
  ├ Changelog(XMLファイル)
  ├ ・・・
  └ Changelog(XMLファイル)
    ├ Changeset(XMLタグ)
    ├ ・・・
    └ Changeset(XMLタグ)
Changelog master複数のChangelogを取りまとめるファイル。データベース・スキーマ1つに対して1ファイル。
Changelogデータベースに対する1回の変更内容を取りまとめるファイル。Changelog masterから参照(include)される。
Changesetテーブルの追加・削除、カラムの追加・削除等の変更内容を記載する。ChangelogにXMLタグで記載する。

ChangesetでできることはChange Typesにまとめられています。

Liquibaseの実行方法

Liquibaseにはコマンドラインツールが準備されており、Changelog masterファイルと接続先のデータベースをコマンドラインパラメータやプロパティファイルに指定して実行します。

コマンドラインツールにLiquibaseへのコマンドを指定することで、

  • 更新の実行
  • 更新時に発行されるSQLの確認
  • 更新のロールバック

等が行えます。
Liquibaseで実行できるコマンドはこちらにまとまっています。

また、コマンドラインツールの他にGradleMavenのプラグインが用意されており、コマンドラインツールと同等の機能を提供しています。

Spring Bootアプリケーションとの統合

Spring Bootプロジェクトに組み込むことでアプリケーションの起動時にLiquibaseを実行し、Changelogの反映を行うことができます。

Changelogの自動生成

公式ページのサンプルを見て、これを手で書くのはきついと思われた方もいると思います。
Liquibaseではいくつかの方法でChangelogを自動生成することができます。

Hibernate拡張機能についてはGitHubのWikiページにも詳しい情報があります。

Changelogの管理

Changelogはテキストファイルですので、Git等のバージョン管理ツールで比較的容易に管理することができます。
データベースへのChangelogの反映状態はLiquibase専用のテーブルで管理されています。
実行時には、テーブルとファイルを比較し、反映されていないものだけが実行されます。
また、テーブルでは個々のChangelog(masterではないもの)のハッシュ値を管理しており、反映済みのChangelogファイルが変更されていないかをチェックしています。
そのため、反映済みのChangelogファイルを修正することはNGです。
データベースに変更が入る場合は既存のChangelogファイルを修正・更新するのではなく、変更内容を記載した新しいChangelogファイルを作成・追加するようにします。


以上でLiquibaseの紹介を終わります。
Liquibaseを初めて使用する場合はSome best practices to keep in mind when using Liquibaseを一読しておくことをお勧めします。
実際のXMLファイルも記載されていますので、イメージを掴むのに役立つと思います。

JHipsterとLiquibase

ここからJHipsterでLiquibaseがどのように使われるかについて紹介していきます。
JHipsterではGradle / Mavenのどちらのプロジェクトも生成することができますが、この記事ではGradleプロジェクトのみを扱います。
また、自動生成時の選択肢でデータベースにSQL(≒RDB)を選択した場合のみを扱います。

JHipsterはJHipster Domain Language(JDL)という形式でデータモデルを定義してCRUD画面を持つアプリケーションを自動生成します。
ソースコードだけではなくLiquibaseと統合されたGradleプロジェクトが生成されます。

  • Liquibase Gradle Pluginが組み込まれたプロジェクトが生成されるため、GradleタスクでLiquibaseの各種操作を行うことができます。
  • JDLのデータモデル定義に従ったChangelogファイルが自動生成されるため、ソースコードと整合性のあるテーブル作成・更新が自動で行えるようになっています。
  • Spring BootアプリケーションはLiquibaseと統合されているため、デフォルト2でアプリケーションの起動時にLiquibaseが実行され、差分があればデータベースが更新されます。
  • サンプル用のデータファイルが自動生成され、データベースにimportするChangelogも生成されます。自動生成直後にアプリケーションを起動すると、データの入った状態で動作を確認することができます。

最初の自動生成後に以下のディレクトリに認可・認証情報のChangelogが生成されます。

src/main/resources/config/liquibase/

プロジェクト生成時点ではJDLが未適用なためEntityは存在しません。
この時点では以下のファイルが生成されています。

  • Changelog masterのXMLファイル

    master.xml
  • 個々のChangelog(XMLファイル)を配置するディレクトリ

    changelog/
  • JHipsterの認可・認証で使用するテーブルの定義

    [ディレクトリ]
    changelog/
    [ファイル]
    00000000000000_initial_schema.xml

    それぞれのテーブルへのデータファイルのロードについても定義されています。
    データファイルのロードも1つのChangesetです。

  • インサート用のデータファイルを配置するディレクトリ

    data/
  • JHipsterの認可・認証で使用するデータの定義

    [ディレクトリ]
    data/
    [ファイル]
    authority.csv  
    user.csv  
    user_authority.csv

認可・認証情報以外のテーブルのChangelogはJDLから生成されます。

最新JDLからのChangelog新規(再)生成

JHipsterのjdlコマンドでEntityを自動生成する時に、デフォルトで「現在のJDLに合わせた最新のChangelog」が全て出力されます。
開発初期時は更新が多発することが予想されるため、データモデルの形が落ち着くまではこの機能を使用して作り直しながら作業することが多いと思います。

$ jhipster jdl 【JDLファイルのパス】
   # バージョン7以前はこれのみでChangelogまで生成される
$ jhipster entities
   # 執筆時点(2023年9月)の最新であるバージョン8(ベータ版)では
   # entitiesの実行も必要

このコマンドを実行すると幾つかのファイルをオーバライドするかどうかの確認メッセージが表示されます。
基本的に上書きで問題ないですが、差分をみるため自動生成前にgit commitしておくようにしましょう。

以下、実行後に生成・更新されるファイルの説明になります。

Changelogファイル

[ディレクトリ]
src/main/resources/config/liquibase/changelog/
[ファイル]
xxxxxxxxxxxxxx_added_entity_【Entity名】.xml

これはJDLに定義されたEntityのテーブルを作成するためのChangelogファイルです。

ここで、「xxxxxxxxxxxxxx」は14桁のタイムスタンプ、【Entity名】はJDLに定義したEntity名です。
個々のEntity単位でファイルが作成されます。

また、以下のようなファイルが作成されることもあります。

[ディレクトリ]
src/main/resources/config/liquibase/changelog/
[ファイル]
xxxxxxxxxxxxxx_added_entity_constraints_【Entity名】.xml

これはテーブル間のリレーショシップがある場合に外部キーの定義などが記載されています。

fake data(開発用のサンプルデータ)

以下のようなディレクトリ・ファイルが作成されます。

[ディレクトリ]
src/main/resources/config/liquibase/fake-data/
[ファイル]
【Entity名(Lower snake case)】.csv

これはサンプルデータのインサート用です。ファイル名はLower snake caseになっています3

JHipsterは各Entity(テーブル)に10件のサンプルデータを自動生成してくれます。
csvデータをインサートするChangesetはEntityのChangelogファイルに含まれています。
ファイルをインサートするChangesetには

<changeSet ・・・ context="faker">

のようにcontext属性が付与されています。
この属性が付与されている場合devプロファイルの場合にのみ実行されるように制御されるため、fake-dataがインサートされるのはdevプロファイルでSpring Bootアプリケーションを動かした場合のみです4

Changelog master

master.xmlも作成されたChangelogファイルをincludeするように自動で更新されます。

JDL更新時の動作

JDLのEntity定義を変更してコマンドを再実行した場合、EntityのChangelogファイルがまるごと書き換えられます。
ファイル名に付与されているタイムスタンプ(上の「xxxxxxxxxxxxxx」)も変わらずに元のファイル名で上書きされます。

そのため、一度Liquibaseの反映を行ったデータベースではハッシュ値のチェックエラーとなり、そのままでは反映できなくなります。
開発時にはEntityの形が決まるまで、Liquibaseで全てdropをしながらJDLの更新を反映していくことになると思います。
必要なサンプルデータはdorpされることを想定し、手動でインサートせずにfake-dataディレクトリのcsvファイルで管理していくのが良いでしょう。

Entityを追加しただけの場合は別ファイルが新規に作成されるため上書きは発生しません。この場合の既存ファイルの更新はChangelog masterのファイルにincludeが増えるだけです5


以上がChangelog新規(再)生成の説明になります。次は、差分のChangelogを生成する機能について説明します。

差分のChangelogを生成する

一度データベースにChangelogの反映を行った場合、それ以降は既存のChangelogファイルは更新せずに新しいChangelogファイルを追加していく必要があります。
JHipsterは以下の場合に限り、インクリメンタルChangelogという機能で追加差分のChangelogを生成することができます。

  • 既存Entityへのフィールドの追加・削除
  • リレーションの追加・削除

条件の詳細等は公式ドキュメントの「Would you like to use incremental Liquibase changelogs?」を参照ください。

実行方法はJHipsterのjdlインポート時に「--incremental-changelog」というオプションを付けるだけです。

$ jhipster jdl 【JDLファイルのパス】 --incremental-changelog

一度このオプションを付けて実行すると、JHipsterの設定ファイル(.yo-rc.json)に

{
  "generator-jhipster": {
(略)
     "incrementalChangelog": true,
(略)

という設定が追加され、その後はインクリメンタルChangelogがデフォルトで有効になります。
インクリメンタルChangelogでJDLの更新をした場合、以下のようなファイルが作成されます。

  • Entity更新(カラムの追加や削除)用のChangelogファイル

    [ディレクトリ]
    src/main/resources/config/liquibase/changelog/
    [ファイル]
    xxxxxxxxxxxxxx_updated_entity_【Entity名】.xml
  • Entity更新(外部キー制約の追加や削除)用のChangelogファイル

    [ディレクトリ]
    src/main/resources/config/liquibase/changelog/
    [ファイル]
    xxxxxxxxxxxxxx_updated_entity_constraints_【Entity名】.xml
  • サンプルデータ更新用のChangelogファイル

    [ディレクトリ]
    src/main/resources/config/liquibase/changelog/
    [ファイル]
    xxxxxxxxxxxxxx_updated_entity_migrate_【Entity名】.xml

    初期追加時とは異なり、Changelogが個別のファイルで作成されます。
    初期追加時と同様に「context="faker"」として定義されます。

  • 更新用のCSVファイル

    [ディレクトリ]
    src/main/resources/config/liquibase/fake-data/
    [ファイル]
    xxxxxxxxxxxxxx_entity_【Entity名(Lower snake case)】.csv]

    INSERTではなくUPDATEとなるため、主キーと追加対象のカラムのみが記載されています。
    注意点として、JHipsterが自動生成した10件のデータ(idが1~10)への更新のみが記載されます。
    手動で元のcsvファイルを修正(行の追加や削除)している場合はこちらも手動で更新する必要があります。


以上のように、カラムの追加・削除など、単純な変更であればJHipsterの自動生成だけで対応できるようになっています。
次に、JHipsterの自動生成機能だけでは対応できない修正を行う場合について説明します。

Gradleタスク

Entityの追加やカラム(およびリレーション)の追加・削除程度であればJHipsterの自動生成で対応できますが、より複雑な修正が入った場合は手作業でChangelogを作成・修正していく必要があります。
と言っても完全な手作業ではなく、Liquibaseの機能を使用していきます。

JHipsterで生成されたGradleプロジェクトには、Liquibase Gradle Pluginが含まれていますので、Liquibaseの公式ドキュメント、及び、プラグインのGitHubにある機能をそのまま使用できます。

使用できるタスクは「./gradlew tasks」コマンドを実行して表示される一覧の中の「Liquibase tasks」で確認できます。
注意点としては、gradle.propertiesに「liquibaseTaskPrefix=liquibase」が設定されていますので、Gradleのタスク名には公式ドキュメントと異なり「liquibase~」というプレフィックスが付きます。

バージョン7以前はLiquibase関連の設定はbuild.gradleに記載されていましたが、バージョン8からはbuild.gradleから分離され、

gradle/liquibase.gradle

というファイルに書かれています。

build.gradle(liquibase.gradle)にはLiquibaseのactivityとして「main」と「diffLog」が定義されています。
activityはgradle実行時にLiquibaseの設定を切り替えるためのものです。詳細はプラグインのGitHubを参照ください。
JHipsterのデフォルトではmainが動作し、diffLogを使用する場合はgradleコマンドのオプションに「-PrunList=diffLog」を付与して実行します。

main activity

main activityは「JHipsterが自動生成したChangelogファイル」と「接続先のデータベース」とが関連付けられた設定です。
以下のタスクをよく使います。

  • 次の更新で実行されるSQLを確認する

    $ ./gradlew liquibaseUpdateSql
  • Spring Bootを起動せずに現在のChangelogを反映させる

    $ ./gradlew liquibaseUpdate
  • 全てのChangelogをdropしてデータベースを初期状態に戻す

    $ ./gradlew liquibaseDropAll

diffLog activity

JHipsterのデータベースアクセスはSpring Data JPA(Hibernate)を使用しています。
JDLの定義に従い、EntityクラスやRepositoryインターフェースを自動作成してくれます。

JHipsterで自動生成されたGradleプロジェクトにはLiquibaseのHibernate拡張機能が含まれています。
diffLog activityはLiquibaseのHibernate拡張機能を使用してEntity Managerに登録されたEntityと接続先のデータベースとを比較するための設定です。

インクリメンタルChangelogの機能で対応できない変更が発生した場合、生成されたEntityと現行のデータベースとを比較して差分Changelogを出力し、手動でマージすることになります。
JHipsterのjdlコマンドでJDLをインポートした後に以下のコマンドを実行します。

$ ./gradlew -PrunList=diffLog liquibaseDiffChangelog

実行後、以下のパスに現状のEntityと接続先データベースとの差分Changelogが出力されます。

[ディレクトリ]
src/main/resources/config/liquibase/changelog/
[ファイル]
xxxxxxxxxxxxxx_changelog.xml

注意点として、Entity Managerを経由して実行されるためにビルドパスに最新のクラスファイルが存在している必要があります。
つまり、一度コンパイルされている必要があります。

また、ここで生成されたファイルはChangelog masterからincludeされません。
作業を効率化するための参照用のファイルとなりますので、使い終わったら削除しましょう。

この後は手動でChangelogを作成する作業になりますが、Changesetの細かい内容は自動で出力してくれるので、ほぼコピー&ペーストで対応できると思います。
例えば以下のような手動での作業が想定されます。

  • 全てのEntityの差分が1つのChangelogファイルに出力されているため、Entityごとにファイルを分ける。
  • Changesetタグのidやauthor属性を変更する。
    idはユニークであれば良くソート順もありませんので、プロジェクトで手動更新の場合のルールを作成しておきます。
    authorも何でも良いのですが、自動生成のものとは変えたほうがわかりやすいと思います。
  • 不要なChangesetが出力されていることがあるため、JDLの更新と無関係のものは削除する。
  • 追加したChangelogファイルをincludeするようにChangelog masterファイルを更新する。

JHipsterの自動生成ではプロジェクトがgitリポジトリ(初回commit済み)として生成されますので、差分を見ながらマージしていく作業になります。


以上、JHipsterとLiquibaseの紹介でした。

最後に

この記事ではChangelog(XML)のコードは記載しませんでしたが、XMLの構造自体は単純なものですので参考にリンクしたサイト等を見て頂ければご理解いただけると思います。

LiquibaseとFlywayを比較した場合、LiquibaseがXML等のデータモデルからSQLを自動生成してデータベースを管理することに不安がある方もいると思います。
ですがLiquibaseのChangelogはSQLでも作成でき、XMLと混在させるこができるため、自動化で効率化しつついざという時にマニュアル対応もできるツールになっています。

JHipster関連記事

Footnotes

  1. XML以外にもYAMLやJSON等のフォーマットで管理することができますが、記載する内容は同じです。
    JHipsterが生成するChangelogはXMLフォーマットですので、この記事ではXMLのみを扱います。
    また、SQLでの管理も可能ですが、この場合はロールバック処理のSQLも自分で記載しなければならない等の制限があります。
    (XMLで記載する場合は更新処理もロールバック処理もChangelogの定義に従いLiquibaseがSQLを自動で生成してくれます)

  2. なんらかの事情で起動時にLiquibaseを実行したくない場合はSpring Bootのプロファイルに「no-liquibase」を指定することでLiquibaseの実行を抑止することができます。

  3. JHipsterではEntity名にUpper camel caseのみ使用可能です。具体的には次の正規表現でチェックされます: /^[A-Z][A-Za-z0-9]\*$/

  4. context属性は実行要否の切り替えを行うためのもので、「spring.liquibase.contexts」というSpringプロパティで制御します。 この値はapplication.ymlにsrc/main/resources/config/application-dev.yml : dev, faker src/main/resources/config/application-prod.yml : prodと定義されているため、fake-dataはdevプロファイルの場合にのみインサートされることになります。なお、dataディレクトリに置かれている認可・認証用のcsvをインサートするChangesetにはcontext属性が付与されていないため、常にインサート対象となります。

  5. Entity追加の場合に新規Entityと既存Entityにリレーションがある場合で、かつ、既存Entity側に外部キーが追加される場合(ManyToOneのMany側等)は既存ファイルの上書き更新が発生します。


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。