← Kursa Dön
📄 Text · 20 min

Flyway ile Database Migration

Giriş — Database'ini Kim Yönetiyor?

Kodunu Git'te versiyonluyorsun. Her değişikliğin commit'i var, geçmişe dönebiliyorsun, branch açıp deneyebiliyorsun. Peki ya veritabanı şemanı? users tablosuna yeni bir kolon eklediğinde, bunu nasıl takip ediyorsun? Takım arkadaşın aynı değişikliği nasıl alıyor?

Eğer cevabın "elle SQL çalıştırıyorum" veya "Hibernate'in ddl-auto=update'ine güveniyorum" ise, seni bir felaket bekliyor. Bu derste veritabanı şemanı için versiyon kontrol sistemi olan Flyway'i öğreneceksin. Ve neden ddl-auto=update'in production'da asla kullanılmaması gerektiğini gerçek felaket senaryolarıyla göreceksin.


1. ddl-auto=update Neden Production'da Tehlikeli?

Spring Boot + JPA kullanırken application.properties'te şu satırı görmüşsündür:

spring.jpa.hibernate.ddl-auto=update

Bu, Hibernate'e "entity sınıflarıma bak, veritabanında eksik olan tabloları/kolonları oluştur" demektir. Development'ta çok pratik görünür. Ama production'da ticking time bomb'dur.

Felaket Senaryosu 1: Kolon İsmi Değiştirme

// Önceki entity
@Entity
public class User {
    private String userName; // DB'de user_name kolonu
}

// Geliştirici isim değiştirdi
@Entity
public class User {
    private String username; // DB'de username kolonu oluşur
}

ddl-auto=update ne yapar?

  • Yeni username kolonu oluşturur

  • Eski user_name kolonunu silmez (Hibernate kolon silmez)

  • Eski kolondaki tüm veriler orada kalır, yeni kolon boş

  • Sonuç: Tüm kullanıcıların username'i null — kullanıcılar giriş yapamaz!

Felaket Senaryosu 2: Tablo İsmi Değiştirme

// Önceki
@Entity
@Table(name = "user_addresses")
public class UserAddress { ... }

// Geliştirici refactor yaptı
@Entity
@Table(name = "addresses")
public class Address { ... }

ddl-auto=update:

  • Yeni addresses tablosu oluşturur (boş)

  • Eski user_addresses tablosu durur (tüm verilerle)

  • Sonuç: Tüm adres verileri kaybolmuş gibi görünür. 50.000 müşterinin adresi yok!

Felaket Senaryosu 3: İndeks Kaybı

// Entity'den @Index annotation'ı kaldırıldı (yanlışlıkla)
@Entity
@Table(name = "orders")
// @Table(name = "orders", indexes = @Index(columnList = "customer_id")) → silindi
public class Order { ... }

ddl-auto=update:

  • İndeksi silmez (bu sefer şanslıyız)

  • Ama yeni bir indeks eklediğinde, ismini kendisi belirler — kontrol sende değil

  • Farklı ortamlarda (dev, staging, prod) farklı indeks isimleri oluşur

Felaket Senaryosu 4: Enum Değişikliği

// Önceki
public enum OrderStatus { PENDING, CONFIRMED, SHIPPED }

// Yeni — sıralama değişti
public enum OrderStatus { PENDING, PROCESSING, CONFIRMED, SHIPPED }

Eğer @Enumerated(EnumType.ORDINAL) kullanıyorsan (sayısal değer):

  • CONFIRMED eskiden 1'di, şimdi 2 oldu

  • Tüm onaylanmış siparişler artık PROCESSING olarak görünüyor

  • Sonuç: Sipariş durumları karıştı — müşteriler yanlış durum görüyor

ddl-auto Seçenekleri ve Kullanım Yerleri

DeğerNe yapar?Nerede kullan?
noneHiçbir şey yapma✅ Production
validateŞemayı kontrol et, uyuşmuyorsa hata ver✅ Production (güvenli)
updateEksik tablo/kolon oluştur⚠️ Sadece development
createHer başlangıçta tabloları sil ve yeniden oluştur❌ Test ortamı
create-dropBaşlangıçta oluştur, kapanışta sil❌ Unit test

⚠️ Dikkat: Production'da her zaman ddl-auto=none veya ddl-auto=validate kullan. Veritabanı şeması değişiklikleri migration tool (Flyway veya Liquibase) ile yönetilmeli.


2. Database Migration Nedir?

Database migration, veritabanı şemasının versiyonlanmış değişiklik dosyalarıyla yönetilmesidir. Tıpkı Git commit'leri gibi:

Git Commits                     Database Migrations
───────────                     ───────────────────
commit 1: Initial project       V1: Create users table
commit 2: Add login feature     V2: Add email column to users
commit 3: Add orders            V3: Create orders table
commit 4: Fix user schema       V4: Add index on users.email

Migration Tool'un Sağladıkları

  1. Versiyon kontrolü: Her şema değişikliği numaralandırılmış bir dosyada

  2. Tekrarlanabilirlik: Yeni bir ortam kurduğunda tüm migration'ları sırayla çalıştır → aynı şema

  3. Takım çalışması: Herkes migration dosyası yazar, Git'te merge edilir

  4. Geri dönülebilirlik: Hangi migration'ın ne zaman çalıştığını bilirsin

  5. Güvenlik: Migration dosyası değiştirilemez — checksum koruması

Flyway Nedir?

Flyway, Java dünyasının en popüler database migration aracıdır. Basit, SQL-tabanlı, convention-over-configuration felsefesiyle çalışır.

Temel felsefesi: SQL dosyalarını belirli bir isimlendirme kuralıyla yaz, Flyway gerisini halleder.


3. Flyway Kurulumu

Maven Dependency

<!-- pom.xml -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

<!-- MySQL kullanıyorsan ek dependency -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
</dependency>

<!-- PostgreSQL kullanıyorsan ek dependency gerekmez (core yeterli) -->

Spring Boot Starter'ı kullanıyorsan ve flyway-core classpath'te ise, Flyway otomatik olarak yapılandırılır.

Gradle

// build.gradle
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql' // MySQL için

Application Properties

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: myuser
    password: mypassword
  
  jpa:
    hibernate:
      ddl-auto: validate  # Flyway kullanıyorsan validate veya none
  
  flyway:
    enabled: true           # Varsayılan: true (classpath'te varsa)
    locations: classpath:db/migration  # Migration dosyalarının yeri
    baseline-on-migrate: false  # Mevcut DB'ye ekleme (aşağıda anlatılacak)
# application.properties alternatifi
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.jpa.hibernate.ddl-auto=validate

Proje Yapısı

src/
├── main/
│   ├── java/
│   │   └── com/example/
│   │       ├── entity/
│   │       │   └── User.java
│   │       ├── repository/
│   │       │   └── UserRepository.java
│   │       └── MyApplication.java
│   └── resources/
│       ├── db/
│       │   └── migration/           ← Migration dosyaları BURAYA
│       │       ├── V1__create_users_table.sql
│       │       ├── V2__add_email_to_users.sql
│       │       └── V3__create_orders_table.sql
│       └── application.yml

4. Migration Dosya Convention

Flyway'in en güçlü yanı basit isimlendirme kuralıdır. Dosya adı her şeyi anlatır:

Versioned Migration Format

V{versiyon}__{açıklama}.sql

V  → "Versioned" migration olduğunu belirtir
{versiyon} → Sıra numarası (1, 2, 3... veya 1.1, 1.2, 2.0...)
__ → İKİ alt çizgi (tek değil!)
{açıklama} → Ne yaptığını anlatan açıklama (boşluk yerine _ kullan)
.sql → SQL dosyası

Örnekler

✅ Doğru İsimlendirme:
V1__create_users_table.sql
V2__add_email_column_to_users.sql
V3__create_orders_table.sql
V4__add_index_on_users_email.sql
V5__create_payments_table.sql
V1.1__add_phone_to_users.sql      (ondalıklı versiyon da olur)
V20240101__initial_schema.sql      (tarih bazlı versiyon)

❌ Yanlış İsimlendirme:
v1_create_users.sql       → küçük 'v', tek alt çizgi
V1_create_users.sql       → tek alt çizgi (iki olmalı: __)
V1-create-users.sql       → tire kullanılmaz, alt çizgi kullan
create_users.sql          → V prefix yok
V1__create users.sql      → boşluk kullanılmaz

⚠️ Dikkat: İki alt çizgi (__) çok önemli. Tek alt çizgi koyarsan Flyway dosyayı tanımaz ve migration çalışmaz. Bu çok yaygın bir hatadır!

Versiyon Sıralaması

Flyway migration'ları versiyon numarasına göre sıralar:

V1__first.sql        → 1. çalışır
V2__second.sql       → 2. çalışır
V3__third.sql        → 3. çalışır
V10__tenth.sql       → 10. çalışır (string değil, numara sırası)
V1.1__one_one.sql    → V1 ile V2 arasında çalışır

5. İlk Migration'ları Yazalım

V1: Users Tablosu

-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100),
    last_name VARCHAR(100),
    active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- İndeks
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_active ON users(active);

-- Yorum: Her migration dosyası bir iş birimi olmalı
-- Bu dosya: users tablosunu oluşturur

V2: Email Kolonu Ekleme

-- V2__add_email_to_users.sql
ALTER TABLE users ADD COLUMN email VARCHAR(255);

-- Mevcut kullanıcılar için varsayılan değer ata
UPDATE users SET email = CONCAT(username, '@example.com') WHERE email IS NULL;

-- Artık NOT NULL yapabiliriz
ALTER TABLE users ALTER COLUMN email SET NOT NULL;

-- Unique constraint
ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email);

-- İndeks
CREATE INDEX idx_users_email ON users(email);

V3: Orders Tablosu

-- V3__create_orders_table.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    shipping_address TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id),
    CONSTRAINT chk_orders_status CHECK (status IN ('PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED')),
    CONSTRAINT chk_orders_amount CHECK (total_amount >= 0)
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);

V4: Order Items Tablosu

-- V4__create_order_items_table.sql
CREATE TABLE order_items (
    id BIGSERIAL PRIMARY KEY,
    order_id BIGINT NOT NULL,
    product_name VARCHAR(255) NOT NULL,
    quantity INT NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,
    
    CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) 
        REFERENCES orders(id) ON DELETE CASCADE,
    CONSTRAINT chk_order_items_quantity CHECK (quantity > 0),
    CONSTRAINT chk_order_items_price CHECK (unit_price >= 0)
);

CREATE INDEX idx_order_items_order_id ON order_items(order_id);

Entity ile Eşleşme

Migration'ları yazdıktan sonra entity'lerin bu şemayla uyumlu olması gerekir:

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    @Column(name = "password_hash", nullable = false)
    private String passwordHash;
    
    @Column(name = "first_name", length = 100)
    private String firstName;
    
    @Column(name = "last_name", length = 100)
    private String lastName;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(nullable = false)
    private Boolean active = true;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // getters, setters
}

@Entity
@Table(name = "orders")
public class Order {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @Column(name = "total_amount", nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Column(nullable = false, length = 20)
    @Enumerated(EnumType.STRING)
    private OrderStatus status = OrderStatus.PENDING;
    
    @Column(name = "shipping_address", columnDefinition = "TEXT")
    private String shippingAddress;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // getters, setters
}

💡 İpucu: ddl-auto=validate kullanarak Flyway migration'larıyla entity'lerin uyumlu olduğunu doğrulayabilirsin. Uyumsuzluk varsa uygulama başlamaz ve hemen fark edersin.


6. Spring Boot Auto-Migration

Spring Boot'ta Flyway çok basit çalışır:

  1. flyway-core dependency'si classpath'te

  2. spring.datasource.* yapılandırılmış

  3. src/main/resources/db/migration/ dizininde migration dosyaları var

Uygulama başladığında:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

INFO  - Flyway Community Edition 10.x.x
INFO  - Database: jdbc:postgresql://localhost:5432/mydb (PostgreSQL 16.x)
INFO  - Successfully validated 4 migrations
INFO  - Creating Schema History table: "public"."flyway_schema_history"
INFO  - Current version of schema "public": << Empty Schema >>
INFO  - Migrating schema "public" to version "1 - create users table"
INFO  - Migrating schema "public" to version "2 - add email to users"
INFO  - Migrating schema "public" to version "3 - create orders table"
INFO  - Migrating schema "public" to version "4 - create order items table"
INFO  - Successfully applied 4 migrations to schema "public"

İkinci Çalıştırma

Uygulama tekrar başlatıldığında:

INFO  - Successfully validated 4 migrations
INFO  - Current version of schema "public": 4
INFO  - Schema "public" is up to date. No migration necessary.

Flyway zaten çalıştırılmış migration'ları tekrar çalıştırmaz. Hangisinin çalıştığını flyway_schema_history tablosunda tutar.

Yeni Migration Ekleme

V5 migration dosyası oluşturuyorsun:

-- V5__add_products_table.sql
CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Uygulama tekrar başlatıldığında:

INFO  - Successfully validated 5 migrations
INFO  - Current version of schema "public": 4
INFO  - Migrating schema "public" to version "5 - add products table"
INFO  - Successfully applied 1 migration to schema "public"

Sadece V5 çalışır. V1-V4 zaten çalıştırılmış.


7. flyway_schema_history Tablosu

Flyway, hangi migration'ların çalıştığını flyway_schema_history tablosunda tutar:

SELECT * FROM flyway_schema_history;
installed_rank | version | description           | type | script                            | checksum    | installed_by | installed_on        | execution_time | success
───────────────┼─────────┼───────────────────────┼──────┼───────────────────────────────────┼─────────────┼──────────────┼─────────────────────┼────────────────┼────────
1              | 1       | create users table    | SQL  | V1__create_users_table.sql        | -1234567890 | myuser       | 2024-01-15 10:00:00 | 45             | true
2              | 2       | add email to users    | SQL  | V2__add_email_to_users.sql        | 987654321   | myuser       | 2024-01-15 10:00:01 | 23             | true
3              | 3       | create orders table   | SQL  | V3__create_orders_table.sql       | -456789123  | myuser       | 2024-01-20 14:30:00 | 38             | true
4              | 4       | create order items    | SQL  | V4__create_order_items_table.sql  | 123456789   | myuser       | 2024-01-20 14:30:01 | 15             | true

Önemli Alanlar

  • version: Migration versiyon numarası

  • checksum: Migration dosyasının hash'i — dosya değişirse uyumsuzluk hatası alırsın

  • installed_on: Ne zaman çalıştırıldığı

  • execution_time: Kaç milisaniye sürdüğü

  • success: Başarılı mı?

Checksum Mekanizması

Flyway her migration dosyasının hash'ini hesaplar ve flyway_schema_history tablosunda saklar. Uygulama başlarken:

  1. Dosyanın güncel hash'ini hesaplar

  2. Tablodaki hash ile karşılaştırır

  3. Farklıysa → Migration validation error!

Flyway validate failed: 
Detected applied migration not resolved locally: 2
Migration checksum mismatch for migration version 2
-> Applied to database : -1234567890
-> Resolved locally    : 987654321

Bu mekanizma, zaten çalıştırılmış bir migration dosyasını değiştirmenin tehlikeli olduğunu garanti eder.


8. Baseline — Mevcut Veritabanına Flyway Ekleme

Projen zaten canlı ve veritabanında tablolar var. Flyway'i sonradan eklemek istiyorsun. Ne yapacaksın?

Adım 1: Mevcut Şemayı Dışa Aktar

# PostgreSQL
pg_dump --schema-only -d mydb > baseline_schema.sql

# MySQL
mysqldump --no-data mydb > baseline_schema.sql

Adım 2: Baseline Migration Oluştur

-- V1__baseline.sql
-- Bu dosya mevcut veritabanı şemasını temsil eder
-- Zaten canlı olan veritabanında çalıştırılmayacak (baseline)

CREATE TABLE IF NOT EXISTS users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    -- ... mevcut yapı
);

CREATE TABLE IF NOT EXISTS orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    -- ... mevcut yapı
);

Adım 3: Baseline Konfigürasyonu

# application.yml
spring:
  flyway:
    baseline-on-migrate: true    # İlk çalıştırmada baseline oluştur
    baseline-version: 1          # V1'i baseline olarak işaretle
    baseline-description: "Baseline - existing schema"

Ne Olur?

  • Mevcut (canlı) veritabanı: V1 "baseline" olarak işaretlenir (çalıştırılmaz). V2'den itibaren migration'lar çalışır.

  • Yeni (boş) veritabanı: V1 dahil tüm migration'lar sırayla çalışır.

# Mevcut DB'de ilk çalıştırma:
INFO  - Successfully baselined schema with version: 1
INFO  - Current version of schema: 1
INFO  - Migrating schema to version "2 - add phone to users"
INFO  - Successfully applied 1 migration

# Yeni DB'de ilk çalıştırma:
INFO  - Current version: << Empty Schema >>
INFO  - Migrating schema to version "1 - baseline"
INFO  - Migrating schema to version "2 - add phone to users"
INFO  - Successfully applied 2 migrations

💡 İpucu: Baseline yaparken V1__baseline.sql dosyasında CREATE TABLE IF NOT EXISTS kullanmak iyi bir pratik. Böylece mevcut DB'de yanlışlıkla çalıştırılsa bile hata vermez.


9. Rollback — Forward-Only Migration

Flyway Community'de Rollback Yok

Flyway'in ücretsiz (Community) sürümünde rollback mekanizması yoktur. Bu bilinçli bir tasarım kararıdır:

Neden?

  • Veri kaybı geri alınamaz: DROP COLUMN sonrası verileri geri getiremezsin

  • INSERT ile eklenen veriler rollback'te silinmeli mi? Belirsiz

  • Complex migration'ların tam tersini yazmak çok zor ve hata-prone

Forward-Only Migration Stratejisi

Hata yaptıysan → düzeltme için yeni bir migration yaz:

-- V5__add_phone_to_users.sql (Hatalı migration)
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL;
-- Hata: NOT NULL koydum ama mevcut kayıtlarda phone yok → migration fail!

Düzeltme:

-- V6__fix_phone_column.sql (Düzeltme migration)
-- V5 başarısız olduysa, önce kolonu temizle
ALTER TABLE users DROP COLUMN IF EXISTS phone;

-- Doğru şekilde ekle (nullable)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

Gerçek Dünyada Rollback Stratejisi

-- V7__add_status_column.sql
-- Bu migration geri alınabilecek şekilde yazılmış

-- İleri yön (forward)
ALTER TABLE orders ADD COLUMN priority VARCHAR(10) DEFAULT 'NORMAL';

-- Eğer geri almak gerekirse → yeni migration yaz:
-- V8__remove_priority_column.sql
-- ALTER TABLE orders DROP COLUMN priority;

⚠️ Dikkat: Flyway Teams (ücretli) sürümünde U (Undo) migration'ları var. Ama Community sürümünde forward-only çalışırsın. Bu aslında daha güvenli bir yaklaşımdır — geri almanın ne yapacağını açıkça yeni bir migration'da belirtirsin.


10. Spring Boot Konfigürasyon — Tüm Flyway Properties

# application.yml — Production yapılandırması
spring:
  flyway:
    # Temel ayarlar
    enabled: true                                    # Flyway'i aktif et
    locations: classpath:db/migration                # Migration dosyalarının yeri
    
    # Versiyon kontrolü
    baseline-on-migrate: false                       # Baseline (mevcut DB için true yap)
    baseline-version: 1                              # Baseline versiyonu
    
    # Doğrulama
    validate-on-migrate: true                        # Her başlangıçta validate et
    validate-migration-naming: true                  # İsimlendirme kuralını kontrol et
    
    # Sıralama
    out-of-order: false                              # Sıra dışı migration'lara izin ver?
    
    # Şema
    default-schema: public                           # Varsayılan şema
    schemas: public                                  # Yönetilecek şema(lar)
    table: flyway_schema_history                     # History tablosu adı
    
    # Bağlantı
    url: ${spring.datasource.url}                    # Ayrı bağlantı (opsiyonel)
    user: ${spring.datasource.username}
    password: ${spring.datasource.password}
    connect-retries: 3                               # Bağlantı tekrar deneme sayısı
    
    # Gelişmiş
    encoding: UTF-8                                  # Dosya encoding
    placeholder-replacement: true                    # Placeholder kullan
    placeholders:
      schema_name: public                            # ${schema_name} olarak kullan
      default_role: USER

Environment-Specific Properties

# application-dev.yml
spring:
  flyway:
    clean-disabled: false  # Dev'de clean komutuna izin ver

# application-prod.yml
spring:
  flyway:
    clean-disabled: true   # Production'da clean KESİNLİKLE kapalı!

Placeholder Kullanımı

Migration dosyalarında dinamik değerler kullanabilirsin:

-- V10__create_admin_role.sql
INSERT INTO roles (name, description) 
VALUES ('${default_role}', 'Default user role');

-- Spring ayarlarından ${default_role} = 'USER' gelir

11. Hands-on: Sıfırdan Proje

Adım adım bir proje kuralım:

Adım 1: Spring Initializr

Dependencies:

  • Spring Web

  • Spring Data JPA

  • PostgreSQL Driver

  • Flyway Migration

Adım 2: application.yml

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/flyway_demo
    username: postgres
    password: postgres
  
  jpa:
    hibernate:
      ddl-auto: validate  # Flyway yönetsin, Hibernate sadece doğrulasın
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: false

Adım 3: İlk Migration

-- src/main/resources/db/migration/V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT uk_users_username UNIQUE (username),
    CONSTRAINT uk_users_email UNIQUE (email)
);

CREATE INDEX idx_users_active ON users(active);

Adım 4: Entity

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(name = "password_hash", nullable = false)
    private String passwordHash;
    
    @Column(nullable = false)
    private Boolean active = true;
    
    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();
    
    // Constructor, getters, setters
}

Adım 5: Çalıştır

./mvnw spring-boot:run

Log'da şunu görmelisin:

INFO  - Flyway Community Edition 10.x.x
INFO  - Migrating schema "public" to version "1 - create users table"
INFO  - Successfully applied 1 migration

Adım 6: Yeni Feature — Profil Fotoğrafı Ekle

-- src/main/resources/db/migration/V2__add_profile_to_users.sql
ALTER TABLE users ADD COLUMN profile_photo_url VARCHAR(500);
ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

Entity'yi güncelle:

@Entity
@Table(name = "users")
public class User {
    // ... mevcut alanlar
    
    @Column(name = "profile_photo_url", length = 500)
    private String profilePhotoUrl;
    
    @Column(columnDefinition = "TEXT")
    private String bio;
    
    @Column(length = 20)
    private String phone;
}

Tekrar çalıştır:

INFO  - Current version: 1
INFO  - Migrating schema to version "2 - add profile to users"
INFO  - Successfully applied 1 migration

Adım 7: Ürünler Tablosu

-- V3__create_products_table.sql
CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    category VARCHAR(100),
    active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_products_price CHECK (price >= 0),
    CONSTRAINT chk_products_stock CHECK (stock >= 0)
);

CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_active ON products(active);
-- V4__create_orders_and_items.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_orders_status CHECK (
        status IN ('PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED')
    )
);

CREATE TABLE order_items (
    id BIGSERIAL PRIMARY KEY,
    order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id BIGINT NOT NULL REFERENCES products(id),
    quantity INT NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,
    
    CONSTRAINT chk_items_quantity CHECK (quantity > 0)
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);

12. Yaygın Hatalar ve Çözümleri

Hata 1: Migration Dosyasını Düzenleme

Flyway validate failed:
Migration checksum mismatch for migration version 2
-> Applied to database : -1234567890
-> Resolved locally    : 567890123

Sebep: Zaten çalıştırılmış bir migration dosyasını değiştirdin.

Çözüm:

# Development'ta: repair komutu ile checksum'ı güncelle
./mvnw flyway:repair

# Veya Spring Boot'ta
spring.flyway.repair-on-migrate=true  # Dikkatli kullan!

# Production'da: ASLA migration dosyasını düzenleme!
# Yeni bir migration yaz.

Hata 2: Out-of-Order Migration

Takım çalışmasında sık olur:

Geliştirici A → V5__add_category.sql (PR merge edildi, çalıştı)
Geliştirici B → V4__add_tags.sql (PR sonra merge edildi)

Hata: V4 found but V5 already applied!

Çözüm:

# Out-of-order'a izin ver (dikkatli!)
spring:
  flyway:
    out-of-order: true

Veya daha iyi çözüm: tarih bazlı versiyon numaraları kullan:

V20240115_001__add_category.sql    (Geliştirici A - 15 Ocak)
V20240116_001__add_tags.sql        (Geliştirici B - 16 Ocak)

Hata 3: Failed Migration — Yarıda Kalan Migration

-- V6__complex_migration.sql
CREATE TABLE new_table (...);     -- ✅ Çalıştı
ALTER TABLE users ADD COLUMN ...;  -- ✅ Çalıştı
INSERT INTO new_table SELECT ...;  -- ❌ Hata verdi!

PostgreSQL'de: Tüm migration rollback olur (transactional DDL desteği var). Dosyayı düzelt ve tekrar çalıştır.

MySQL'de: İlk iki komut çalıştı ve kalıcı oldu (DDL transactional değil). Yarım kalmış durumu manuel temizlemen gerekir.

# MySQL'de yarım kalan migration'ı düzelt:
# 1. Manuel olarak yarım kalan değişiklikleri geri al
# 2. flyway_schema_history'den failed kaydı sil
DELETE FROM flyway_schema_history WHERE version = '6' AND success = false;
# 3. Migration dosyasını düzelt
# 4. Tekrar çalıştır

⚠️ Dikkat: PostgreSQL transactional DDL destekler (CREATE TABLE, ALTER TABLE bir transaction içinde çalışır). MySQL desteklemez. Bu yüzden MySQL'de her migration dosyasını mümkün olduğunca küçük tut — bir dosyada bir iş yap.

Hata 4: NOT NULL Kolon Ekleme (Mevcut Verili Tabloya)

-- ❌ YANLIŞ: Mevcut kayıtlarda phone yok → hata!
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL;

-- ✅ DOĞRU: 3 adımda yap
-- Adım 1: Nullable olarak ekle
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Adım 2: Mevcut kayıtları güncelle
UPDATE users SET phone = 'N/A' WHERE phone IS NULL;

-- Adım 3: NOT NULL yap
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

Hata 5: Büyük Tabloda ALTER TABLE

-- ❌ Dikkat: 10 milyon kayıtlı tabloda ALTER TABLE uzun sürebilir
ALTER TABLE huge_table ADD COLUMN new_col VARCHAR(255);
-- PostgreSQL'de hızlı (metadata change), MySQL'de tablo kopyalanabilir!

-- ❌ Çok tehlikeli: Default değerle NOT NULL (MySQL)
ALTER TABLE huge_table ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE';
-- MySQL'de tüm satırlar güncellenir → tablo lock → downtime!

Çözüm: Büyük tablolarda migration'ları maintenance window'da çalıştır. Veya online schema migration araçları kullan (pt-online-schema-change, gh-ost).


13. Migration Best Practices

1. Her Migration Tek İş Yapsın

-- ❌ KÖTÜ: Her şey bir dosyada
-- V1__everything.sql
CREATE TABLE users (...);
CREATE TABLE orders (...);
CREATE TABLE products (...);
INSERT INTO users (...);

-- ✅ İYİ: Her migration tek bir iş
-- V1__create_users_table.sql
-- V2__create_products_table.sql
-- V3__create_orders_table.sql
-- V4__insert_default_users.sql

2. Migration Dosyaları Immutable

Bir migration çalıştırıldıktan sonra asla düzenleme. Yeni migration yaz.

3. Her Migration İdempotent Olsun (Mümkünse)

-- İdempotent migration
CREATE TABLE IF NOT EXISTS users (...);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

4. Geri Alma Planı Düşün

-- V10__add_feature_flag.sql
-- Forward: Yeni kolon ekle
ALTER TABLE users ADD COLUMN feature_x_enabled BOOLEAN DEFAULT false;

-- Geri alma gerekirse:
-- V11__remove_feature_flag.sql
-- ALTER TABLE users DROP COLUMN feature_x_enabled;

5. Test Ortamında Dene

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class MigrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @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);
    }

    @Test
    void shouldRunAllMigrationsSuccessfully() {
        // Uygulama context'i başarıyla yüklendiyse → tüm migration'lar çalıştı
        // Hata varsa → test fail olur
    }
}

Özet

  • `ddl-auto=update` production'da kullanma! Kolon ismi değişikliği veri kaybına, tablo değişikliği veri kaybolmasına yol açar. Production'da validate veya none kullan, şema değişikliklerini Flyway ile yönet.

  • Flyway SQL-tabanlı, convention-over-configuration migration aracıdır. Dosya adı formatı V{versiyon}__{açıklama}.sql — iki alt çizgi (__) kritik!

  • `flyway_schema_history` tablosu hangi migration'ların çalıştığını, checksum'larını ve zamanlamasını tutar. Bir migration dosyasını değiştirirsen checksum uyumsuzluğu alırsın.

  • Flyway Community forward-only'dir — rollback yok. Hata düzeltmek için yeni migration yaz. Bu daha güvenli bir yaklaşımdır.

  • Baseline mevcut veritabanına Flyway eklemek için kullanılır. baseline-on-migrate: true ile ilk çalıştırmada mevcut şema baseline olarak işaretlenir.

  • Her migration tek iş yapsın, immutable olsun, ve production'a gitmeden önce test ortamında denensin. NOT NULL kolon ekleme, büyük tablolarda ALTER TABLE gibi riskli işlemlere özellikle dikkat et.