Flyway İleri Kullanım & Liquibase
Giriş — Flyway'i Gerçek Dünyada Kullanmak
Flyway'in temellerini öğrendin: migration dosyası yaz, uygulamayı başlat, tablo oluşsun. Ama gerçek dünyada işler bu kadar basit değil.
Seed data nasıl yönetilir? Her ortamda farklı migration çalıştırmak istersen? Flyway'de bir şeyler ters giderse nasıl düzeltirsin? Ve herkesin sorduğu soru: Liquibase mi, Flyway mi?
Bu derste Flyway'in ileri özelliklerini, Liquibase alternatifini ve production'da migration yönetiminin inceliklerini öğreneceksin.
1. Repeatable Migrations — Tekrarlanabilir Migration'lar
Versioned migration'lar (V1__, V2__) bir kez çalışır. Ama bazı şeyler her değişiklikte tekrar çalıştırılmalı:
Seed data: Varsayılan roller, kategoriler, ayarlar
View'lar: Her değişiklikte yeniden oluşturulmalı
Stored Procedure'lar: Güncellenmiş versiyonu her seferinde çalışmalı
Function'lar: İş mantığı değişikliklerinde güncellenmeli
Repeatable Migration Formatı
R__{açıklama}.sql
R → "Repeatable" migration olduğunu belirtir
__ → İki alt çizgi
{açıklama} → Ne yaptığını anlatan açıklama
.sql → SQL dosyasıVersiyon numarası yok — çünkü her değişiklikte tekrar çalışır.
Çalışma Mantığı
Flyway dosyanın checksum'ını hesaplar
flyway_schema_history'deki checksum ile karşılaştırırChecksum değiştiyse → dosyayı tekrar çalıştırır
Aynıysa → atlar
Seed Data Örneği
-- R__insert_default_roles.sql
-- Bu dosya her değişiklikte tekrar çalışır
-- Mevcut rolleri temizle ve yeniden yükle
DELETE FROM roles WHERE is_system = true;
INSERT INTO roles (name, description, is_system) VALUES
('ADMIN', 'Sistem yöneticisi', true),
('USER', 'Standart kullanıcı', true),
('MODERATOR', 'İçerik moderatörü', true),
('EDITOR', 'İçerik editörü', true);Yeni bir rol eklemek istediğinde dosyayı düzenlersin:
-- R__insert_default_roles.sql (güncellenmiş)
DELETE FROM roles WHERE is_system = true;
INSERT INTO roles (name, description, is_system) VALUES
('ADMIN', 'Sistem yöneticisi', true),
('USER', 'Standart kullanıcı', true),
('MODERATOR', 'İçerik moderatörü', true),
('EDITOR', 'İçerik editörü', true),
('SUPPORT', 'Destek ekibi', true); -- Yeni eklendiFlyway bir sonraki çalıştırmada checksum değişikliğini algılar ve dosyayı tekrar çalıştırır.
View Örneği
-- R__create_order_summary_view.sql
CREATE OR REPLACE VIEW order_summary AS
SELECT
o.id AS order_id,
u.username,
u.email,
o.total_amount,
o.status,
o.created_at,
COUNT(oi.id) AS item_count,
SUM(oi.quantity) AS total_items
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, u.username, u.email, o.total_amount, o.status, o.created_at;View değişikliğinde dosyayı düzenlersin, Flyway otomatik olarak CREATE OR REPLACE ile günceller.
Stored Procedure Örneği
-- R__update_order_statistics_procedure.sql
CREATE OR REPLACE FUNCTION update_order_statistics()
RETURNS void AS $$
BEGIN
-- Günlük istatistikleri hesapla
INSERT INTO order_statistics (date, total_orders, total_revenue, avg_order_value)
SELECT
CURRENT_DATE,
COUNT(*),
COALESCE(SUM(total_amount), 0),
COALESCE(AVG(total_amount), 0)
FROM orders
WHERE created_at >= CURRENT_DATE
AND status != 'CANCELLED'
ON CONFLICT (date) DO UPDATE SET
total_orders = EXCLUDED.total_orders,
total_revenue = EXCLUDED.total_revenue,
avg_order_value = EXCLUDED.avg_order_value;
END;
$$ LANGUAGE plpgsql;Çalışma Sırası
Flyway migration'ları şu sırada çalıştırır:
1. Versioned migrations (V1, V2, V3...) — versiyon sırasıyla
2. Repeatable migrations (R__...) — dosya adı alfabetik sırasıylaRepeatable migration'lar her zaman versioned migration'lardan sonra çalışır. Bu mantıklı çünkü view veya procedure, tabloların var olmasına bağlıdır.
💡 İpucu: Repeatable migration'larda
DELETE + INSERTyerineUPSERT(ON CONFLICT) veyaCREATE OR REPLACEkullan. Böylece dosya her çalıştığında var olan veriyi bozmaz, sadece günceller.
Repeatable Migration'lar için UPSERT Pattern
-- R__insert_app_settings.sql
-- PostgreSQL UPSERT
INSERT INTO app_settings (key, value, description) VALUES
('max_login_attempts', '5', 'Maksimum giriş denemesi'),
('session_timeout_minutes', '30', 'Oturum zaman aşımı (dakika)'),
('maintenance_mode', 'false', 'Bakım modu')
ON CONFLICT (key) DO UPDATE SET
value = EXCLUDED.value,
description = EXCLUDED.description;2. Undo Migrations (Flyway Teams)
Flyway'in ücretli sürümü (Teams/Enterprise) undo migration desteği sunar:
Format
U{versiyon}__{açıklama}.sql
U → "Undo" migration
{versiyon} → Geri alınacak migration'ın versiyonu
__ → İki alt çizgiÖrnek
-- V5__add_tags_table.sql (İleri yön)
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
color VARCHAR(7) DEFAULT '#000000'
);
CREATE TABLE product_tags (
product_id BIGINT NOT NULL REFERENCES products(id),
tag_id BIGINT NOT NULL REFERENCES tags(id),
PRIMARY KEY (product_id, tag_id)
);-- U5__undo_add_tags_table.sql (Geri yön)
DROP TABLE IF EXISTS product_tags;
DROP TABLE IF EXISTS tags;Kullanım
# V5'i geri al
flyway undo -target=5
# Son migration'ı geri al
flyway undo⚠️ Dikkat: Undo migration'lar sadece Flyway Teams (ücretli) sürümünde var. Community sürümünde bu dosyalar yoksayılır. Ücretsiz sürümde forward-only yaklaşımla çalışmalısın — geri almak için yeni bir versioned migration yaz.
Community'de Manuel Undo Stratejisi
-- V5__add_tags_table.sql (çalıştı, ama geri almak istiyoruz)
-- V6__remove_tags_table.sql (undo yerine yeni migration)
DROP TABLE IF EXISTS product_tags;
DROP TABLE IF EXISTS tags;Bu yaklaşım aslında daha güvenlidir çünkü:
Değişiklik geçmişi korunur (V5 ekledi, V6 sildi)
Herkes ne olduğunu görebilir
Audit trail bozulmaz
3. Flyway Callbacks — Migration Öncesi/Sonrası Aksiyonlar
Flyway belirli olaylarda otomatik çalıştırılacak callback'ler tanımanı sağlar.
SQL Callbacks
Callback dosyaları migration dizinine konur:
src/main/resources/db/migration/
├── V1__create_users.sql
├── V2__create_orders.sql
├── beforeMigrate.sql ← Her migration öncesi çalışır
├── afterMigrate.sql ← Her migration sonrası çalışır
├── beforeEachMigrate.sql ← Her bir migration dosyası öncesi
├── afterEachMigrate.sql ← Her bir migration dosyası sonrası
└── afterMigrateError.sql ← Migration hatası sonrası-- beforeMigrate.sql
-- Her migration başlamadan önce çalışır
-- Örnek: Şema kontrolleri, lock alma
-- Eğer başka bir Flyway instance'ı çalışıyorsa bekle
SELECT pg_advisory_lock(12345);-- afterMigrate.sql
-- Tüm migration'lar başarıyla tamamlandıktan sonra çalışır
-- Örnek: Cache temizleme, istatistik güncelleme
-- View'ları yenile
REFRESH MATERIALIZED VIEW IF EXISTS order_statistics;
-- Lock'u serbest bırak
SELECT pg_advisory_unlock(12345);-- afterMigrateError.sql
-- Migration hatası sonrası çalışır
-- Örnek: Alert gönder, log yaz
INSERT INTO migration_errors (error_time, message)
VALUES (NOW(), 'Migration failed! Check logs.');Java Callbacks
Daha karmaşık işlemler için Java callback'leri kullanabilirsin:
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
@Component
public class FlywayCallbackHandler implements Callback {
private final SlackNotifier slackNotifier;
public FlywayCallbackHandler(SlackNotifier slackNotifier) {
this.slackNotifier = slackNotifier;
}
@Override
public boolean supports(Event event, Context context) {
// Hangi event'lerde çalışacağını belirle
return event == Event.AFTER_MIGRATE
|| event == Event.AFTER_MIGRATE_ERROR;
}
@Override
public boolean canHandleInTransaction(Event event, Context context) {
return true;
}
@Override
public void handle(Event event, Context context) {
if (event == Event.AFTER_MIGRATE) {
slackNotifier.send(
"#deployments",
"✅ Database migration başarıyla tamamlandı! " +
"Version: " + context.getMigrationInfo().getVersion()
);
} else if (event == Event.AFTER_MIGRATE_ERROR) {
slackNotifier.send(
"#alerts",
"❌ Database migration BAŞARISIZ! Acil müdahale gerekli!"
);
}
}
@Override
public String getCallbackName() {
return "SlackNotificationCallback";
}
}Callback Konfigürasyonu
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy flywayMigrationStrategy(
FlywayCallbackHandler callbackHandler) {
return flyway -> {
Flyway configuredFlyway = Flyway.configure()
.configuration(flyway.getConfiguration())
.callbacks(callbackHandler)
.load();
configuredFlyway.migrate();
};
}
}Tüm Callback Event'leri
| Event | Ne zaman? |
|---|---|
beforeMigrate | Tüm migration süreci başlamadan önce |
beforeEachMigrate | Her bir migration dosyası çalışmadan önce |
afterEachMigrate | Her bir migration dosyası çalıştıktan sonra |
afterMigrate | Tüm migration'lar başarıyla tamamlandıktan sonra |
afterMigrateError | Migration hatası sonrası |
beforeValidate | Validation başlamadan önce |
afterValidate | Validation tamamlandıktan sonra |
beforeClean | Clean komutu öncesi |
afterClean | Clean komutu sonrası |
beforeRepair | Repair komutu öncesi |
afterRepair | Repair komutu sonrası |
4. Multi-Tenant Migration Stratejileri
Birden fazla kiracının (tenant) aynı uygulamayı kullandığı SaaS yapılarda migration stratejisi kritik önem taşır.
Strateji 1: Ayrı Şema per Tenant
@Configuration
public class MultiTenantFlywayConfig {
@Bean
public FlywayMigrationStrategy multiTenantMigrationStrategy(
DataSource dataSource, TenantService tenantService) {
return flyway -> {
// Önce shared şemayı migrate et
Flyway.configure()
.dataSource(dataSource)
.schemas("shared")
.locations("classpath:db/migration/shared")
.load()
.migrate();
// Sonra her tenant'ın şemasını migrate et
for (Tenant tenant : tenantService.getAllTenants()) {
Flyway.configure()
.dataSource(dataSource)
.schemas(tenant.getSchemaName()) // tenant_acme, tenant_globex...
.locations("classpath:db/migration/tenant")
.load()
.migrate();
log.info("Migrated tenant: {}", tenant.getName());
}
};
}
}Dizin Yapısı
src/main/resources/db/migration/
├── shared/ ← Ortak tablolar (tenant listesi vs.)
│ ├── V1__create_tenants_table.sql
│ └── V2__create_shared_config.sql
└── tenant/ ← Her tenant şemasına uygulanır
├── V1__create_users_table.sql
├── V2__create_orders_table.sql
└── V3__create_products_table.sqlStrateji 2: Ayrı Veritabanı per Tenant
@Bean
public FlywayMigrationStrategy separateDbStrategy(
TenantDataSourceProvider dataSourceProvider) {
return flyway -> {
for (TenantInfo tenant : dataSourceProvider.getAllTenants()) {
DataSource tenantDs = dataSourceProvider.getDataSource(tenant.getId());
Flyway.configure()
.dataSource(tenantDs)
.locations("classpath:db/migration")
.load()
.migrate();
log.info("Migrated DB for tenant: {}", tenant.getName());
}
};
}Strateji 3: Tek Şema, Tenant ID Kolonu
En basit yaklaşım — migration açısından özel bir şey yok:
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL, -- Her satır bir tenant'a ait
username VARCHAR(50) NOT NULL,
-- ...
CONSTRAINT uk_users_tenant_username UNIQUE (tenant_id, username)
);
CREATE INDEX idx_users_tenant_id ON users(tenant_id);💡 İpucu: Multi-tenant migration'larda ayrı şema yaklaşımı en yaygın olanıdır. Her tenant kendi versiyon geçmişine sahip olur ve bağımsız migrate edilebilir. Ama yeni tenant eklerken tüm migration'ların baştan çalışması gerekir.
5. Flyway CLI Kullanımı
Spring Boot dışında veya doğrudan komut satırından Flyway kullanabilirsin:
Kurulum
# macOS
brew install flyway
# Linux
wget -qO- https://download.red-gate.com/maven/release/com/redgate/flyway/flyway-commandline/10.8.1/flyway-commandline-10.8.1-linux-x64.tar.gz | tar -xvz
# Docker
docker run --rm flyway/flyway -url=jdbc:postgresql://host:5432/mydb migrateTemel Komutlar
migrate — Migration'ları Çalıştır
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=postgres \
-password=secret \
-locations=filesystem:./sql \
migrateinfo — Migration Durumu
flyway info+-----------+---------+---------------------+----------+---------------------+----------+----------+
| Category | Version | Description | Type | Installed On | State | Undoable |
+-----------+---------+---------------------+----------+---------------------+----------+----------+
| Versioned | 1 | create users table | SQL | 2024-01-15 10:00:00 | Success | No |
| Versioned | 2 | add email to users | SQL | 2024-01-15 10:00:01 | Success | No |
| Versioned | 3 | create orders table | SQL | 2024-01-20 14:30:00 | Success | No |
| Versioned | 4 | add products table | SQL | | Pending | No |
+-----------+---------+---------------------+----------+---------------------+----------+----------+validate — Doğrulama
flyway validateMigration dosyalarının veritabanıyla uyumlu olup olmadığını kontrol eder. Checksum uyumsuzluğu, eksik dosya gibi sorunları algılar.
repair — Onarım
flyway repairNe yapar?
Failed migration kayıtlarını
flyway_schema_history'den silerChecksum uyumsuzluklarını düzeltir (tablodaki checksum'ı dosyadakiyle günceller)
Tabloyu temiz bir duruma getirir
# Senaryo: V5 migration'ı yarıda kaldı (MySQL'de)
# 1. Manuel olarak yarım kalan değişiklikleri geri al
# 2. repair çalıştır → failed kayıt silinir
flyway repair
# 3. Migration dosyasını düzelt
# 4. tekrar migrate
flyway migrate⚠️ Dikkat:
repairkomutu sadeceflyway_schema_historytablosunu düzeltir. Veritabanındaki şema değişikliklerini geri almaz! Yarım kalan DDL komutlarını manuel geri alman gerekir.
clean — Temizle (TEHLİKELİ!)
flyway cleanVeritabanındaki TÜM tabloları, view'ları, procedure'ları siler. Development'ta kullanışlı, production'da ASLA çalıştırma.
# Production'da clean'i engelle!
spring:
flyway:
clean-disabled: true # MUTLAKA true olsun# Flyway 10+ sürümde clean varsayılan olarak disabled
# Açmak için:
flyway -cleanDisabled=false cleanFlyway Configuration File
# flyway.conf (proje root'unda)
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=postgres
flyway.password=secret
flyway.locations=filesystem:./sql/migration
flyway.schemas=public
flyway.cleanDisabled=true
flyway.validateOnMigrate=true
flyway.baselineOnMigrate=false
flyway.outOfOrder=false6. Environment-Specific Migration
Farklı ortamlarda farklı migration'lar çalıştırmak isteyebilirsin: test ortamında seed data yükle, production'da yükleme.
Spring Profiles ile
# application-dev.yml
spring:
flyway:
locations:
- classpath:db/migration # Ortak migration'lar
- classpath:db/migration/dev # Dev-only migration'lar (seed data)
# application-test.yml
spring:
flyway:
locations:
- classpath:db/migration # Ortak migration'lar
- classpath:db/migration/test # Test-only migration'lar
# application-prod.yml
spring:
flyway:
locations:
- classpath:db/migration # Sadece ortak migration'lar
clean-disabled: trueDizin Yapısı
src/main/resources/db/
├── migration/ ← Her ortamda çalışır
│ ├── V1__create_users.sql
│ ├── V2__create_orders.sql
│ └── V3__create_products.sql
├── migration/dev/ ← Sadece dev'de çalışır
│ ├── V100__insert_test_users.sql
│ └── V101__insert_test_products.sql
├── migration/test/ ← Sadece test'te çalışır
│ └── V200__insert_test_fixtures.sql-- db/migration/dev/V100__insert_test_users.sql
INSERT INTO users (username, email, password_hash, active)
VALUES
('admin', 'admin@dev.local', '$2a$10$...', true),
('testuser', 'test@dev.local', '$2a$10$...', true),
('demo', 'demo@dev.local', '$2a$10$...', true);💡 İpucu: Dev ve test seed data'ları için yüksek versiyon numaraları kullan (V100, V200...). Böylece ortak migration'larla çakışmaz.
Programmatic Yaklaşım
@Configuration
public class FlywayConfig {
@Value("${spring.profiles.active:default}")
private String activeProfile;
@Bean
public FlywayMigrationStrategy conditionalMigration() {
return flyway -> {
List<String> locations = new ArrayList<>();
locations.add("classpath:db/migration");
if ("dev".equals(activeProfile) || "test".equals(activeProfile)) {
locations.add("classpath:db/seed");
}
Flyway.configure()
.configuration(flyway.getConfiguration())
.locations(locations.toArray(String[]::new))
.load()
.migrate();
};
}
}7. Liquibase Alternatifi
Flyway tek seçenek değil. Java dünyasının diğer büyük migration aracı Liquibase'dir. İkisi farklı felsefelerle çalışır.
Liquibase Nedir?
Liquibase, veritabanı değişikliklerini changelog dosyalarıyla yönetir. Flyway'den farklı olarak SQL yerine XML, YAML, JSON veya SQL formatında changeset'ler yazarsın.
Liquibase Kurulumu
<!-- pom.xml -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency># application.yml
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yamlChangelog Formatları
XML Format (En Yaygın)
<!-- db/changelog/db.changelog-master.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-4.24.xsd">
<include file="db/changelog/changes/001-create-users.xml"/>
<include file="db/changelog/changes/002-create-orders.xml"/>
<include file="db/changelog/changes/003-add-email-to-users.xml"/>
</databaseChangeLog><!-- db/changelog/changes/001-create-users.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-4.24.xsd">
<changeSet id="001-1" author="tolga">
<createTable tableName="users">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="username" type="VARCHAR(50)">
<constraints nullable="false" unique="true"/>
</column>
<column name="email" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="password_hash" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
<createIndex tableName="users" indexName="idx_users_username">
<column name="username"/>
</createIndex>
<!-- Rollback tanımı — Liquibase'in en güçlü yanı -->
<rollback>
<dropTable tableName="users"/>
</rollback>
</changeSet>
</databaseChangeLog>YAML Format
# db/changelog/changes/001-create-users.yaml
databaseChangeLog:
- changeSet:
id: 001-1
author: tolga
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: username
type: VARCHAR(50)
constraints:
nullable: false
unique: true
- column:
name: email
type: VARCHAR(255)
constraints:
nullable: false
unique: true
rollback:
- dropTable:
tableName: usersJSON Format
{
"databaseChangeLog": [
{
"changeSet": {
"id": "001-1",
"author": "tolga",
"changes": [
{
"createTable": {
"tableName": "users",
"columns": [
{
"column": {
"name": "id",
"type": "BIGINT",
"autoIncrement": true,
"constraints": {
"primaryKey": true,
"nullable": false
}
}
},
{
"column": {
"name": "username",
"type": "VARCHAR(50)",
"constraints": {
"nullable": false,
"unique": true
}
}
}
]
}
}
],
"rollback": [
{
"dropTable": {
"tableName": "users"
}
}
]
}
}
]
}SQL Format (Flyway'e Benzer)
-- db/changelog/changes/001-create-users.sql
-- liquibase formatted sql
-- changeset tolga:001-1
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- rollback DROP TABLE users;Liquibase Temel Kavramları
ChangeLog — Ana Dosya
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/changes/001-create-users.yaml
- include:
file: db/changelog/changes/002-create-orders.yaml
- includeAll:
path: db/changelog/changes/v2/ # Dizindeki tüm dosyaları yükleChangeSet — Değişiklik Birimi
Her changeset benzersiz bir id + author kombinasyonuna sahiptir:
- changeSet:
id: "002-add-phone"
author: "tolga"
comment: "Kullanıcılara telefon alanı eklendi"
changes:
- addColumn:
tableName: users
columns:
- column:
name: phone
type: VARCHAR(20)
rollback:
- dropColumn:
tableName: users
columnName: phoneRollback Desteği
Liquibase'in en güçlü özelliklerinden biri built-in rollback desteğidir:
# Son 2 changeset'i geri al
liquibase rollbackCount 2
# Belirli bir tag'e geri dön
liquibase rollback v1.0
# Belirli bir tarihe geri dön
liquibase rollbackToDate 2024-01-15# Tag koyma
- changeSet:
id: "tag-v1.0"
author: "tolga"
changes:
- tagDatabase:
tag: "v1.0"Preconditions — Ön Koşullar
- changeSet:
id: "003-add-index"
author: "tolga"
preConditions:
- onFail: MARK_RAN # Koşul sağlanmazsa çalışmış say
- not:
- indexExists:
tableName: users
indexName: idx_users_email
changes:
- createIndex:
tableName: users
indexName: idx_users_email
columns:
- column:
name: emailContext ve Labels
- changeSet:
id: "100-insert-test-data"
author: "tolga"
context: "dev, test" # Sadece dev ve test ortamlarında çalışır
labels: "seed-data"
changes:
- insert:
tableName: users
columns:
- column: { name: username, value: "testuser" }
- column: { name: email, value: "test@example.com" }# application.yml
spring:
liquibase:
contexts: dev # Sadece "dev" context'li changeset'ler çalışır
labels: ""Liquibase History Tablosu
Liquibase değişiklik geçmişini DATABASECHANGELOG tablosunda tutar:
SELECT id, author, filename, dateexecuted, orderexecuted, md5sum, tag
FROM DATABASECHANGELOG;id | author | filename | dateexecuted | md5sum
────────────┼────────┼───────────────────────────────────┼─────────────────────┼──────────────
001-1 | tolga | db/changelog/changes/001-...yaml | 2024-01-15 10:00:00 | 8:a1b2c3d4...
002-1 | tolga | db/changelog/changes/002-...yaml | 2024-01-15 10:00:01 | 8:e5f6g7h8...8. Flyway vs Liquibase — Karar Matrisi
Karşılaştırma Tablosu
| Özellik | Flyway | Liquibase |
|---|---|---|
| Öğrenme eğrisi | ⚡ Çok kolay | 📚 Orta-Zor |
| Migration formatı | SQL (+ Java) | XML, YAML, JSON, SQL |
| Veritabanı bağımsızlığı | ❌ SQL'e bağımlı | ✅ Soyut changeset'ler |
| Rollback (Community) | ❌ Yok | ✅ Var |
| Rollback (Ücretli) | ✅ Undo migration | ✅ Built-in |
| Repeatable migration | ✅ R__ dosyaları | ✅ runOnChange |
| Preconditions | ❌ Yok | ✅ Gelişmiş |
| Context/Labels | ❌ Sınırlı | ✅ Gelişmiş |
| Diff (şema karşılaştırma) | ❌ Yok | ✅ Var |
| Callback'ler | ✅ SQL + Java | ✅ Sınırlı |
| Spring Boot entegrasyonu | ✅ Mükemmel | ✅ Mükemmel |
| Topluluk büyüklüğü | 🏆 Daha büyük | 📊 Büyük |
| Dokümantasyon | ✅ Basit, net | ✅ Kapsamlı |
| Multi-DB desteği | SQL'i her DB için yaz | Tek changeset, her DB'de çalışır |
| Performans | ⚡ Hızlı | 🔧 Orta |
Ne Zaman Flyway?
✅ Flyway'i tercih et eğer:
├── SQL yazmaktan rahatsız değilsen
├── Tek veritabanı kullanıyorsan (sadece PostgreSQL veya sadece MySQL)
├── Basitlik ve hız öncelikliyse
├── Takım SQL'i iyi biliyorsa
├── Spring Boot monolith projesiyse
└── Rollback'e ihtiyacın yoksa (forward-only çalışıyorsan)Ne Zaman Liquibase?
✅ Liquibase'i tercih et eğer:
├── Birden fazla veritabanı desteklemen gerekiyorsa (MySQL + PostgreSQL + Oracle)
├── Rollback desteği kritikse
├── Changeset'leri XML/YAML ile yazmak istiyorsan
├── Precondition ve context/label özellikleri gerekiyorsa
├── Enterprise ortamda çalışıyorsan (compliance, audit)
├── Schema diff (karşılaştırma) aracı istiyorsan
└── DBA ekibi SQL yerine deklaratif format tercih ediyorsaBenim Tavsiyem
%80 proje için → Flyway
├── Basit, hızlı, SQL-native
├── Spring Boot ile mükemmel entegrasyon
└── "İşini gör, yoldan çekil" felsefesi
%20 proje için → Liquibase
├── Multi-database desteği gerektiren enterprise projeler
├── Rollback'in kritik olduğu finansal sistemler
└── Non-SQL DBA'ların dahil olduğu büyük ekipler💡 İpucu: İkisini de bilmen büyük avantaj. Mülakatlarda "Flyway mı Liquibase mi?" sorusuna "İkisini de biliyorum, proje ihtiyacına göre seçerim" demek güçlü bir cevaptır. Trade-off'ları açıklayabilmen önemli.
9. Production Migration Checklist
Production'a migration deploy etmeden önce bu listeyi kontrol et:
Deployment Öncesi
□ Migration dosyası lokal ortamda test edildi
□ Migration dosyası staging ortamında çalıştırıldı
□ Büyük tablolarda ALTER TABLE süresi ölçüldü (10M+ satır dikkat!)
□ NOT NULL kolon ekleme → önce nullable ekle, data migrasyon yap, sonra NOT NULL
□ Foreign key ekleme → referans edilen tabloda data var mı kontrol et
□ İndeks ekleme → CONCURRENTLY kullan (PostgreSQL) — tablo lock'lanmasın
□ Migration idempotent mi? (IF NOT EXISTS, IF EXISTS kullanıldı mı?)
□ Rollback planı hazır (yeni migration ile geri alma)
□ Backup alındı (veya point-in-time recovery aktif)
□ Deployment window planlandı (gerekiyorsa maintenance mode)Deployment Sırası
1. Veritabanı backup al
2. Uygulamayı maintenance mode'a al (gerekiyorsa)
3. Migration'ı çalıştır (veya yeni versiyonu deploy et — auto-migrate)
4. Migration loglarını kontrol et
5. flyway info ile durumu doğrula
6. Uygulamanın sağlıklı başladığını kontrol et
7. Smoke test çalıştır
8. Maintenance mode'u kapatDeployment Sonrası
□ Migration başarıyla tamamlandı (flyway_schema_history kontrol)
□ Uygulama hatasız başladı (health check)
□ API endpoint'leri çalışıyor (smoke test)
□ Performans metrikleri normal (slow query yok)
□ Error rate artmadı (monitoring)
□ Yeni feature çalışıyor (QA onay)Büyük Migration'lar için Ekstra Önlemler
-- ❌ TEHLİKELİ: 10M satırlı tabloda normal indeks oluşturma
CREATE INDEX idx_orders_customer ON orders(customer_id);
-- Tablo lock'lanır → downtime!
-- ✅ GÜVENLİ: CONCURRENTLY ile indeks oluşturma (PostgreSQL)
CREATE INDEX CONCURRENTLY idx_orders_customer ON orders(customer_id);
-- Lock yok, arka planda oluşturulur
-- Not: CONCURRENTLY bir transaction içinde çalışmaz
-- Flyway'de kullanmak için:
-- spring.flyway.postgresql.transactional.lock=false# Büyük migration'lar için Flyway ayarları
spring:
flyway:
# PostgreSQL: DDL'leri transaction dışında çalıştırmaya izin ver
# (CONCURRENTLY indeks oluşturma için gerekli)
mixed: true # Flyway Teams featureZero-Downtime Migration Stratejisi
Aşama 1: Backward-compatible migration deploy et
├── Yeni kolon ekle (nullable)
├── Yeni tablo oluştur
└── Eski yapıyı BOZMA
Aşama 2: Yeni uygulama versiyonunu deploy et
├── Yeni kodu deploy et
├── Yeni kolon/tabloyu kullanmaya başla
└── Eski ve yeni yapıyı paralel destekle
Aşama 3: Temizlik migration'ı deploy et
├── Eski kolonu/tabloyu sil
├── Constraint'leri ekle (NOT NULL vs.)
└── Bu aşama isteğe bağlı ve güvenli zamanda yapılırÖrnek:
-- Aşama 1: V10__add_full_name_column.sql (Backward-compatible)
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);
-- Eski kod hâlâ first_name + last_name kullanıyor — sorun yok
-- Aşama 2: Yeni uygulama deploy edilir
-- Yeni kod full_name'i doldurmaya başlar
-- Eski kayıtları da günceller:
UPDATE users SET full_name = first_name || ' ' || last_name
WHERE full_name IS NULL;
-- Aşama 3: V11__cleanup_name_columns.sql (Haftalarca sonra)
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;10. İleri Flyway Konfigürasyonu
Custom Migration (Java-based)
SQL yetmediğinde Java ile migration yazabilirsin:
package db.migration; // Bu paket adı önemli!
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
// Dosya adı convention: V6__EncryptPasswords.java
public class V6__EncryptPasswords extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (var stmt = context.getConnection().createStatement()) {
var rs = stmt.executeQuery(
"SELECT id, password_hash FROM users WHERE password_hash NOT LIKE '$2a$%'");
try (var updateStmt = context.getConnection().prepareStatement(
"UPDATE users SET password_hash = ? WHERE id = ?")) {
while (rs.next()) {
long id = rs.getLong("id");
String oldHash = rs.getString("password_hash");
String newHash = BCrypt.hashpw(oldHash, BCrypt.gensalt());
updateStmt.setString(1, newHash);
updateStmt.setLong(2, id);
updateStmt.addBatch();
}
updateStmt.executeBatch();
}
}
}
}⚠️ Dikkat: Java migration'larda Spring bean'lerini inject edemezsin — Flyway Spring context'inden bağımsız çalışır. JDBC connection'ı doğrudan kullanman gerekir.
Placeholder'lar ile Dinamik SQL
# application.yml
spring:
flyway:
placeholders:
table_prefix: app_
default_schema: public
admin_email: admin@example.com-- V7__create_config_table.sql
CREATE TABLE ${table_prefix}config (
id BIGSERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE,
value TEXT
);
INSERT INTO ${table_prefix}config (key, value) VALUES
('admin_email', '${admin_email}'),
('version', '1.0.0');Bu migration çalıştığında:
CREATE TABLE app_config (...);
INSERT INTO app_config (key, value) VALUES ('admin_email', 'admin@example.com', ...);Flyway + Testcontainers
Production'a en yakın test ortamı:
@SpringBootTest
@Testcontainers
class FlywayMigrationIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(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 Flyway flyway;
@Test
void allMigrationsShouldRunSuccessfully() {
// Context başarıyla yüklendiyse migration'lar çalışmıştır
MigrationInfo[] applied = flyway.info().applied();
assertThat(applied).isNotEmpty();
for (MigrationInfo info : applied) {
assertThat(info.getState())
.as("Migration %s should be successful", info.getVersion())
.isEqualTo(MigrationState.SUCCESS);
}
}
@Test
void shouldHaveCorrectSchemaVersion() {
MigrationInfo current = flyway.info().current();
assertThat(current).isNotNull();
assertThat(current.getVersion().toString())
.as("Latest migration version")
.isNotEmpty();
}
}Özet
Repeatable migration'lar (
R__...) seed data, view ve stored procedure'lar için kullanılır. Dosya değiştiğinde otomatik tekrar çalışır — checksum tabanlı algılama ile.Flyway CLI (
migrate,info,validate,repair,clean) production dışında debug ve onarım için çok kullanışlıdır.cleankomutu tüm veritabanını siler — production'dacleanDisabled=trueolmalı!Liquibase XML/YAML/JSON changeset formatı, built-in rollback, precondition ve multi-database desteği sunar. Enterprise ve multi-DB projeler için güçlü alternatif.
Flyway basitlik ve hız odaklıdır: SQL yaz, çalıştır, bitti. Liquibase esneklik ve kontrol odaklıdır: deklaratif changeset'ler, rollback, context/label. Projenin ihtiyacına göre seç.
Production migration checklist: Backup al, staging'de test et, büyük tabloları dikkatli yönet (
CONCURRENTLY), zero-downtime stratejisi uygula, smoke test çalıştır.Environment-specific migration için Spring profiles ve
locationskullan. Dev'de seed data yükle, production'da yükleme. Java-based migration'lar karmaşık veri dönüşümleri için kullanışlıdır.
AI Asistan
Sorularını yanıtlamaya hazır