← Kursa Dön
📄 Text · 20 min

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ığı

  1. Flyway dosyanın checksum'ını hesaplar

  2. flyway_schema_history'deki checksum ile karşılaştırır

  3. Checksum değiştiyse → dosyayı tekrar çalıştırır

  4. 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 eklendi

Flyway 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ıyla

Repeatable 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 + INSERT yerine UPSERT (ON CONFLICT) veya CREATE OR REPLACE kullan. 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

EventNe zaman?
beforeMigrateTüm migration süreci başlamadan önce
beforeEachMigrateHer bir migration dosyası çalışmadan önce
afterEachMigrateHer bir migration dosyası çalıştıktan sonra
afterMigrateTüm migration'lar başarıyla tamamlandıktan sonra
afterMigrateErrorMigration hatası sonrası
beforeValidateValidation başlamadan önce
afterValidateValidation tamamlandıktan sonra
beforeCleanClean komutu öncesi
afterCleanClean komutu sonrası
beforeRepairRepair komutu öncesi
afterRepairRepair 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.sql

Strateji 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 migrate

Temel Komutlar

migrate — Migration'ları Çalıştır

flyway -url=jdbc:postgresql://localhost:5432/mydb \
       -user=postgres \
       -password=secret \
       -locations=filesystem:./sql \
       migrate

info — 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 validate

Migration dosyalarının veritabanıyla uyumlu olup olmadığını kontrol eder. Checksum uyumsuzluğu, eksik dosya gibi sorunları algılar.

repair — Onarım

flyway repair

Ne yapar?

  • Failed migration kayıtlarını flyway_schema_history'den siler

  • Checksum 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: repair komutu sadece flyway_schema_history tablosunu 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 clean

Veritabanı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 clean

Flyway 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=false

6. 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: true

Dizin 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.yaml

Changelog 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: users

JSON 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ükle

ChangeSet — 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: phone

Rollback 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: email

Context 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

ÖzellikFlywayLiquibase
Öğ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ğiSQL'i her DB için yazTek 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 ediyorsa

Benim 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 kapat

Deployment 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 feature

Zero-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. clean komutu tüm veritabanını siler — production'da cleanDisabled=true olmalı!

  • 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 locations kullan. 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.