- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. Flywayとは?
- 2. 依存関係と基本設定
- 3. マイグレーションファイルの命名規則
- 4. 実践マイグレーション例
- 5. Javaベースのマイグレーション
- 6. マルチ環境設定
- 7. テスト戦略
- 8. 本番環境での注意事項
- 9. CI/CD統合
- クイズ:Flyway知識チェック
1. Flywayとは?
Flywayはデータベーススキーマのバージョン管理を自動化するオープンソースのマイグレーションツールです。アプリケーションコードとDBスキーマの変更を一緒に追跡し、チームメンバー全員が同じDB状態を保てるよう支援します。
Flyway vs Liquibase 比較
| 項目 | Flyway | Liquibase |
|---|---|---|
| マイグレーション形式 | SQL または Java | XML, YAML, JSON, SQL |
| 学習コスト | 低い | 中程度 |
| ロールバック対応 | 有料(Teams) | 無料(内蔵) |
| Spring Boot統合 | 自動設定 | 自動設定 |
| コミュニティ | 非常に活発 | 活発 |
FlywayはSQLを中心としたシンプルなアプローチと低い学習コストから、多くのSpring Bootプロジェクトで採用されています。
flyway_schema_historyテーブル
Flywayはflyway_schema_historyテーブルを通じてマイグレーション実行履歴を管理します。各マイグレーションのバージョン、チェックサム、実行日時、成功・失敗を記録します。
-- flyway_schema_historyの確認
SELECT installed_rank, version, description, type, script, checksum, installed_on, success
FROM flyway_schema_history
ORDER BY installed_rank;
2. 依存関係と基本設定
Maven依存関係
<!-- Maven pom.xml -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- MySQL使用時に追加 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<!-- PostgreSQLはflyway-coreのみでOK -->
Gradle依存関係
// build.gradle
dependencies {
implementation 'org.flywaydb:flyway-core'
// MySQL使用時
implementation 'org.flywaydb:flyway-mysql'
}
application.yml 基本設定
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: myuser
password: mypassword
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
out-of-order: false
table: flyway_schema_history
encoding: UTF-8
connect-retries: 3
主要な設定項目の説明:
enabled: Flyway自動実行の有効化フラグlocations: マイグレーションスクリプトの場所(複数指定可)baseline-on-migrate: 既存DBにFlywayを初めて適用する際に現在の状態をベースラインとして設定validate-on-migrate: マイグレーション前にチェックサムを検証out-of-order: バージョン順序を無視して適用するかどうか(デフォルトfalse)table: 履歴テーブル名のカスタマイズ
3. マイグレーションファイルの命名規則
マイグレーションタイプ別の規則
バージョン付きマイグレーション:
V{version}__{description}.sql
例:
V1__create_users_table.sqlV2__add_email_index.sqlV1.2.3__fix_user_status_column.sql
アンドゥマイグレーション(Flyway Teams専用):
U{version}__{description}.sql
例:
U2__add_email_index.sql
リピータブルマイグレーション:
R__{description}.sql
例:
R__create_views.sqlR__update_stored_procedures.sql
命名規則の詳細
- バージョン区切り文字:数字とドット/アンダースコア可 —
V1、V1.1、V1_1すべて有効 - 説明区切り文字:アンダースコア2個(
__) - 説明内のアンダースコア(
_)はスペースとして表示 - 大文字小文字の区別:
Vは大文字が必須
src/main/resources/
└── db/
└── migration/
├── V1__init_schema.sql
├── V2__add_user_status.sql
├── V3__insert_seed_data.sql
├── V4__add_indexes.sql
└── R__create_reporting_views.sql
4. 実践マイグレーション例
V1__init_schema.sql - 初期スキーマ作成
-- V1__init_schema.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
V2__add_user_status.sql - カラム追加
-- V2__add_user_status.sql
ALTER TABLE users
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN last_login TIMESTAMP,
ADD COLUMN phone_number VARCHAR(20);
UPDATE users SET status = 'ACTIVE' WHERE status IS NULL;
V3__insert_seed_data.sql - 初期データ挿入
-- V3__insert_seed_data.sql
INSERT INTO products (name, description, price, stock) VALUES
('ノートPC', '高性能開発者向けノートPC', 150000, 50),
('マウス', 'ワイヤレスエルゴノミクスマウス', 8000, 200),
('キーボード', 'メカニカルRGBキーボード', 12000, 150);
INSERT INTO users (username, email, password, status) VALUES
('admin', 'admin@example.com', '$2a$10$hashedpassword', 'ACTIVE');
V4__add_indexes.sql - インデックス最適化
-- V4__add_indexes.sql
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created ON orders(created_at DESC);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
5. Javaベースのマイグレーション
SQLだけでは処理しにくい複雑なデータ変換はJavaマイグレーションで実装します。
BaseJavaMigrationの実装
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* V5__MigrateUserData
* usersテーブルのfull_nameカラムをfirst_nameとlast_nameに分割
*/
public class V5__MigrateUserData extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
JdbcTemplate jdbcTemplate = new JdbcTemplate(
new SingleConnectionDataSource(context.getConnection(), true)
);
List<String[]> users = new ArrayList<>();
try (Statement stmt = context.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT id, full_name FROM users WHERE full_name IS NOT NULL")) {
while (rs.next()) {
long id = rs.getLong("id");
String fullName = rs.getString("full_name");
String[] parts = fullName.split(" ", 2);
users.add(new String[]{
String.valueOf(id),
parts[0],
parts.length > 1 ? parts[1] : ""
});
}
}
for (String[] user : users) {
jdbcTemplate.update(
"UPDATE users SET first_name = ?, last_name = ? WHERE id = ?",
user[1], user[2], Long.parseLong(user[0])
);
}
System.out.printf("%d件のユーザーレコードをマイグレーションしました%n", users.size());
}
}
Spring BeanをJavaマイグレーションに注入する
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class V6__EncryptSensitiveData extends BaseJavaMigration {
// @ComponentによりSpring Bean注入が可能
// FlywayAutoConfigurationがJavaMigration Beanを自動検出
@Autowired
private EncryptionService encryptionService;
@Override
public void migrate(Context context) throws Exception {
var jdbcTemplate = new org.springframework.jdbc.core.JdbcTemplate(
new org.springframework.jdbc.datasource.SingleConnectionDataSource(
context.getConnection(), true)
);
jdbcTemplate.query(
"SELECT id, phone_number FROM users WHERE phone_number IS NOT NULL",
(rs) -> {
long id = rs.getLong("id");
String phone = rs.getString("phone_number");
String encrypted = encryptionService.encrypt(phone);
jdbcTemplate.update(
"UPDATE users SET phone_number = ? WHERE id = ?",
encrypted, id
);
}
);
}
}
6. マルチ環境設定
Spring Profile別のFlyway設定
# application.yml (共通)
spring:
flyway:
enabled: true
locations: classpath:db/migration
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/dev
clean-on-validation-error: true # 開発環境のみ許可
---
# application-test.yml
spring:
config:
activate:
on-profile: test
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/test
clean-disabled: false # テスト前のDB初期化を許可
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
flyway:
locations: classpath:db/migration
clean-disabled: true # 本番環境でのcleanコマンドを無効化
out-of-order: false
validate-on-migrate: true
ベンダー別マイグレーション分岐
spring:
flyway:
locations:
- classpath:db/migration
- classpath:db/migration/{vendor}
db/migration/
├── V1__create_tables.sql # 共通
├── V2__add_indexes.sql # 共通
├── postgresql/
│ └── V3__add_pg_specific.sql # PostgreSQL専用
└── mysql/
└── V3__add_mysql_specific.sql # MySQL専用
Java Configによる細かい設定
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy migrationStrategy() {
return flyway -> {
System.out.println("Flywayマイグレーション開始...");
flyway.migrate();
System.out.println("Flywayマイグレーション完了。");
};
}
@Bean
@Profile("!prod")
public FlywayMigrationStrategy devMigrationStrategy() {
return flyway -> {
// 開発環境: スキーマをクリアしてから再実行
flyway.clean();
flyway.migrate();
};
}
}
7. テスト戦略
@FlywayTestアノテーションの活用
<dependency>
<groupId>org.flywaydb.flyway-test-extensions</groupId>
<artifactId>flyway-spring-test</artifactId>
<version>9.5.0</version>
<scope>test</scope>
</dependency>
@SpringBootTest
@FlywayTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@FlywayTest(locationsForMigrate = {"classpath:db/migration/test"})
void testUserCreation() {
User user = new User("testuser", "test@example.com");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
}
}
H2インメモリDBテスト
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
flyway:
enabled: true
locations: classpath:db/migration
TestContainers + Flyway(推奨)
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class IntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void flywayMigration_appliesAllScripts() {
assertThat(userRepository.count()).isGreaterThanOrEqualTo(0);
}
}
8. 本番環境での注意事項
カラム削除の3ステップ戦略
稼働中のシステムでカラムを即座に削除するとローリングデプロイ中にアプリケーションエラーが発生する可能性があります。3ステップのアプローチを推奨します。
ステップ1:カラムをNULL許容に変更(デプロイN)
-- V10__make_old_column_nullable.sql
ALTER TABLE users ALTER COLUMN old_field DROP NOT NULL;
ステップ2:アプリケーションコードから該当カラムの参照を削除(デプロイN+1)
- JPAエンティティ、クエリ、リポジトリから参照を削除
ステップ3:カラムの実際の削除(デプロイN+2)
-- V12__drop_old_column.sql
ALTER TABLE users DROP COLUMN IF EXISTS old_field;
PostgreSQL大規模インデックス作成戦略
public class V15__AddLargeIndexConcurrently extends BaseJavaMigration {
@Override
public boolean canExecuteInTransaction() {
return false; // トランザクション外で実行
}
@Override
public void migrate(Context context) throws Exception {
try (Statement stmt = context.getConnection().createStatement()) {
// CONCURRENTLYはテーブルロックなしでインデックス作成
// ただしトランザクション内では使用不可
stmt.execute(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS " +
"idx_orders_large ON orders(created_at, status)"
);
}
}
}
大規模テーブルのバッチマイグレーション
public class V16__BatchMigrateLargeTable extends BaseJavaMigration {
private static final int BATCH_SIZE = 1000;
@Override
public void migrate(Context context) throws Exception {
JdbcTemplate jdbc = new JdbcTemplate(
new SingleConnectionDataSource(context.getConnection(), true)
);
Long maxId = jdbc.queryForObject("SELECT MAX(id) FROM orders", Long.class);
if (maxId == null) return;
long processedCount = 0;
for (long offset = 0; offset <= maxId; offset += BATCH_SIZE) {
final long batchOffset = offset;
int updated = jdbc.update(
"UPDATE orders SET new_status_code = CASE status " +
"WHEN 'PENDING' THEN 1 WHEN 'COMPLETED' THEN 2 ELSE 0 END " +
"WHERE id > ? AND id <= ? AND new_status_code IS NULL",
batchOffset, batchOffset + BATCH_SIZE
);
processedCount += updated;
System.out.printf("進捗: %d件更新済み%n", processedCount);
}
}
}
flyway repairコマンド
マイグレーション失敗後に履歴を復旧する際に使用します。
# Maven
./mvnw flyway:repair
# Gradle
./gradlew flywayRepair
# CLI
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=myuser \
-password=mypassword \
repair
9. CI/CD統合
GitHub ActionsでのFlyway検証
# .github/workflows/flyway-validate.yml
name: Flyway Validation
on:
pull_request:
paths:
- 'src/main/resources/db/migration/**'
- 'src/main/java/db/migration/**'
jobs:
flyway-validate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Flyway検証
run: |
./mvnw flyway:validate \
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
-Dflyway.user=testuser \
-Dflyway.password=testpass
- name: マイグレーション実行
run: |
./mvnw flyway:migrate \
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
-Dflyway.user=testuser \
-Dflyway.password=testpass
本番デプロイ前検証スクリプト
#!/bin/bash
# scripts/pre-deploy-flyway-check.sh
set -e
echo "本番データベースに対してFlyway検証を実行中..."
./mvnw flyway:validate \
-Dflyway.url="${PROD_DB_URL}" \
-Dflyway.user="${PROD_DB_USER}" \
-Dflyway.password="${PROD_DB_PASSWORD}" \
-Dflyway.cleanDisabled=true
echo "Flyway検証が完了しました。デプロイ可能です。"
クイズ:Flyway知識チェック
Q1. flyway_schema_historyテーブルはどのような役割を担っていますか?
答え: マイグレーション実行履歴を追跡するメタデータテーブルです。
解説: flyway_schema_historyテーブルは、適用された各マイグレーションスクリプトのバージョン、ファイル名、チェックサム(ファイル変更検出用)、実行日時、成功・失敗状態を保存します。Flywayはこのテーブルを基に、どのマイグレーションが実行済みかを把握し、新たに追加されたスクリプトのみを順番に実行します。チェックサムを利用して、適用済みスクリプトへの不正な変更も検出します。
Q2. baseline-on-migrateオプションはどのような状況で使用すべきですか?
答え: すでに稼働中の既存データベースにFlywayを初めて導入する際に使用します。
解説: flyway_schema_historyテーブルが存在しない既存DBにFlywayを初めて適用するとエラーが発生することがあります。baseline-on-migrate: trueを設定することで、初回実行時に現在のDB状態をベースラインとして設定し、以降のマイグレーションのみを適用します。ただし、すでにFlywayで管理されているDBには使用しないことを推奨します。
Q3. リピータブルマイグレーション(R__プレフィックス)はどのような場合に使用しますか?
答え: ビュー、ストアドプロシージャ、ファンクションなど、内容が変更されるたびに再実行が必要なSQLオブジェクトの管理に使用します。
解説: バージョン付きマイグレーションは一度しか実行されませんが、リピータブルマイグレーションはファイルのチェックサムが変更されるたびに再実行されます。そのためR__create_views.sqlにビュー定義を記述しておけば、ビューの内容を変更したい時にファイルを修正するだけで自動的に再適用されます。リピータブルマイグレーションは常にバージョン付きマイグレーションが全て適用された後に実行されます。
Q4. 本番環境で大規模テーブルにインデックスを追加する際、Flywayのトランザクション処理はどうすべきですか?
答え: canExecuteInTransaction()メソッドをfalseにオーバーライドしたJavaマイグレーションを使用し、PostgreSQLのCREATE INDEX CONCURRENTLYを活用します。
解説: PostgreSQLのCREATE INDEX CONCURRENTLYはテーブルロックなしでインデックスを作成できますが、トランザクション内では使用できません。JavaマイグレーションでcanExecuteInTransaction()をfalseにすると、Flywayはそのマイグレーションをトランザクション外で実行します。これにより、本番サービスを止めることなく安全にインデックスを作成できます。
Q5. 適用済みのマイグレーションファイルを変更した場合に発生する「checksum mismatch」エラーはどう解決しますか?
答え: flyway repairコマンドを実行するか、修正内容を含む新しいバージョンのマイグレーションファイルを追加します。
解説: Flywayは既に適用されたマイグレーションファイルが変更されるとチェックサム不一致エラーを発生させます。これはDBの一貫性を保護するための安全機能です。解決方法は2つあります。まずflyway repairで履歴テーブルのチェックサムを現在のファイルと同期させます(開発環境でのみ推奨)。次に、絶対に既存ファイルを変更せず、修正内容を含む新しいバージョンのマイグレーションファイルを追加します(本番環境では必須)。