はじめに
Liquibase はデータベースのスキーマ定義の履歴を管理するためのツールです。
Liquibase はスキーマ定義の変更を『チェンジログ』(changelog) と呼んで管理しています。チェンジログは複数の『チェンジセット』(changeset) で構成され、個々のチェンジセットにはデータベースに対する具体的な変更が記述されています。
Liquibase の公式ドキュメントでは、チェンジログの構成方法として、オブジェクトを基準にする方法とリリースを基準にする方法の二つを示しています。ここではそれぞれを、オブジェクト指向構成、リリース指向構成、と呼ぶことにします。
双方の構成とも利点と欠点があります。この記事では、双方の利点を得て、双方の欠点を解消する、ハイブリッド構成を紹介します。
オブジェクト指向構成
オブジェクト指向構成では、テーブル、ビュー、インデックスといったオブジェクト視点でチェンジログの階層構造を作ります。
changelog-root.yaml
|
|-- changelog-table.yaml
| |
| |-- changelog-table-user.yaml
| |
| |-- ...
|
|-- changelog-view.yaml
|
この構成の良い点は、特定のオブジェクトに対する変更履歴を見つけやすいことです。逆に悪い点は、オブジェクトに対する変更がどのリリースに含まれるのかすぐに分からないことです。
リリース指向構成
リリース指向構成では、リリース視点でチェンジログの階層構造を作ります。
changelog-root.yaml
|
|-- changelog-1.0.yaml
| |
| |-- changelog-1.0.0.yaml
| |
| |-- ...
|
|-- changelog-1.1.yaml
|
この構成の良い点は、特定のリリースにどの変更が含まれるのかすぐに分かることです。逆に悪い点は、オブジェクト毎の変更履歴を追うのが難しくなることです。
ハイブリッド構成
双方の構成とも利点と欠点があります。しかし、これらの構成を組み合わせることで、双方の利点を得て、双方の欠点を解消することができます。その方法をここではハイブリッド構成と呼ぶことにします。
この構成には次のファイル群が登場します。
| ファイル名 | 説明 | |
|---|---|---|
| 1 | changelog.yaml |
ルートファイル |
| 2 | changelog-version-X.Y[.Z].yaml |
名称にバージョン番号を含むファイル |
| 3 | changelog-date-YY-MM-DD.yaml |
名称に日付を含むファイル |
| 4 | OBJECT-NAME-NNNN.yaml |
名称にオブジェクトの種類と名前、および通し番号を含むファイル (チェンジセット) |
これらのファイル群の関係は次のようになります。
- ルートファイルである
changelog.yamlは、全てのchangelog-version-X.Y.yamlファイル群を include します。
- 各
changelog-version-X.Y.yamlファイルは、一つ以上のchangelog-version-X.Y.Z.yamlファイルを include します。
- 各
changelog-version-X.Y.Z.yamlファイルは、一つ以上のchangelog-date-YY-MM-DD.yamlファイルを include します。
- 各
changelog-date-YY-MM-DD.yamlファイルは、一つ以上のOBJECT-NAME-NNNN.yamlファイルを include します。
これらのファイル群を次のディレクトリ階層に配置します。
changelog.yaml
changelogs/version/X.Y/changelog-version-X.Y.yaml
changelogs/version/X.Y/changelog-version-X.Y.Z.yaml
changelogs/date/YYYY/MM/changelog-date-YYYY-MM-DD.yaml
chnagesets/OBJECT/NAME/OBJECT-NAME-NNNN.yaml
ハイブリッド構成の例
下記はハイブリッド構成を用いた場合のディレクトリ階層・ファイル名の例です。
.
├── changelog.yaml
├── changelogs
│ ├── date
│ │ └── 2025
│ │ ├── 04
│ │ │ └── changelog-date-2025-04-09.yaml
│ │ ├── 06
│ │ │ └── changelog-date-2025-06-10.yaml
│ │ ├── 07
│ │ │ ├── changelog-date-2025-07-11.yaml
│ │ │ ├── changelog-date-2025-07-16.yaml
│ │ │ ├── changelog-date-2025-07-26.yaml
│ │ │ └── changelog-date-2025-07-28.yaml
│ │ ├── 08
│ │ │ └── changelog-date-2025-08-28.yaml
│ │ └── 09
│ │ └── changelog-date-2025-09-06.yaml
│ └── version
│ └── 1.0
│ ├── changelog-version-1.0.0.yaml
│ ├── changelog-version-1.0.1.yaml
│ ├── changelog-version-1.0.2.yaml
│ ├── changelog-version-1.0.3.yaml
│ ├── changelog-version-1.0.4.yaml
│ ├── changelog-version-1.0.5.yaml
│ ├── changelog-version-1.0.6.yaml
│ ├── changelog-version-1.0.7.yaml
│ └── changelog-version-1.0.yaml
└── changesets
└── table
├── arena_key
│ └── table-arena_key-0000.yaml
├── ssf_stream
│ ├── table-ssf_stream-0000.yaml
│ └── table-ssf_stream-0001.yaml
├── ssf_stream_event
│ └── table-ssf_stream_event-0000.yaml
├── ssf_stream_subject
│ └── table-ssf_stream_subject-0000.yaml
├── ssf_transmitter
│ ├── table-ssf_transmitter-0000.yaml
│ └── table-ssf_transmitter-0001.yaml
└── ssf_transmitter_key
└── table-ssf_transmitter_key-0000.yaml
# changelog.yaml
databaseChangeLog:
- logicalFilePath: changelog.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.yaml
# changelog-version-1.0.yaml
databaseChangeLog:
- include:
file: changelogs/version/1.0/changelog-version-1.0.0.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.1.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.2.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.3.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.4.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.5.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.6.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.7.yaml
# changelog-version-1.0.0.yaml
databaseChangeLog:
- include:
file: changelogs/date/2025/04/changelog-date-2025-04-09.yaml
# changelog-date-2025-04-09.yaml
databaseChangeLog:
- include:
file: changesets/table/ssf_transmitter/table-ssf_transmitter-0000.yaml
# table-ssf_transmitter-0000.yaml
databaseChangeLog:
- changeSet:
id: table-ssf_transmitter-0000
author: authlete
changes:
- createTable:
tableName: ssf_transmitter
columns:
# 以下省略
チェンジセットの考慮事項
各チェンジセットは id 属性と author 属性を持ちます。Liquibase は、これらの属性の値と、liquibase コマンドが実行された際のルートチェンジログファイルのパスを組み合わせ、チェンジセットを特定します。この組み合わせが変更追跡テーブル (DATABASECHANGELOG) 内に存在するかどうかによって、そのチェンジセットを適用すべきかどうかが判断されます。
しかしながら、私のプロジェクトでは、id 属性のみによってチェンジセットを一意に特定できるようにしています。author 属性とファイルパスは、意図的に全てのチェンジセットで同一にしています。
過去、author 属性にチェンジセットを作成した開発者の識別子を設定するという運用をしていたこともありますが、これまでの経験ではこの方針が実用的な利点をもたらしたことはありませんでした。逆に、開発者固有の任意に選ばれた文字列が、変更追跡テーブルに残り続けて後から変更もできないという点に違和感を感じるようになりました。そこで、新しいプロジェクトでは author 属性の値は固定文字列 authlete (会社名) にしています。
もう一つの問題は、ファイルパスがチェンジセットの識別子の一部となることです。チェンジセットの内容が変更されていなくても、ファイルパスを変更すると Liquibase はそれを異なるチェンジセットとして扱います。さらに、たとえファイルパスが変更されていなくても、Liquibase がチェンジログ・チェンジセットの場所を解決する方法が異なる場合 —たとえば環境変数 LIQUIBASE_CLASSPATH の値を変更した場合—、異なるチェンジセットとして扱われることになります。Liquibase を Docker イメージ経由で使う場合、チェンジログを含むボリュームをマウントするという操作でもパスが変わってしまいます。率直に言うと、ファイルパスをチェンジセット識別の一部として用いるという Liquibase の設計には問題があります。
チェンジログファイルの論理パスを logicalFilePath で changelog.yaml に固定することにより、ファイルパスに関する問題を回避することができます。下記は changelog.yaml ファイルの先頭部分です。logicalFilePath 属性が指定されていることに注目してください。
# changelog.yaml
databaseChangeLog:
- logicalFilePath: changelog.yaml
- include:
file: changelogs/version/1.0/changelog-version-1.0.yaml
おわりに
ハイブリッド構成は私が考案したものなので世の中的にはまだ実績はありませんが、Liquibase のチェンジログの構成に悩んでいるようでしたら試してみてください。