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=updateBu, 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
usernamekolonu oluştururEski
user_namekolonunu silmez (Hibernate kolon silmez)Eski kolondaki tüm veriler orada kalır, yeni kolon boş
Sonuç: Tüm kullanıcıların
username'inull— 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
addressestablosu oluşturur (boş)Eski
user_addressestablosu 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):
CONFIRMEDeskiden 1'di, şimdi 2 olduTüm onaylanmış siparişler artık
PROCESSINGolarak görünüyorSonuç: Sipariş durumları karıştı — müşteriler yanlış durum görüyor
ddl-auto Seçenekleri ve Kullanım Yerleri
| Değer | Ne yapar? | Nerede kullan? |
|---|---|---|
none | Hiçbir şey yapma | ✅ Production |
validate | Şemayı kontrol et, uyuşmuyorsa hata ver | ✅ Production (güvenli) |
update | Eksik tablo/kolon oluştur | ⚠️ Sadece development |
create | Her başlangıçta tabloları sil ve yeniden oluştur | ❌ Test ortamı |
create-drop | Başlangıçta oluştur, kapanışta sil | ❌ Unit test |
⚠️ Dikkat: Production'da her zaman
ddl-auto=noneveyaddl-auto=validatekullan. 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.emailMigration Tool'un Sağladıkları
Versiyon kontrolü: Her şema değişikliği numaralandırılmış bir dosyada
Tekrarlanabilirlik: Yeni bir ortam kurduğunda tüm migration'ları sırayla çalıştır → aynı şema
Takım çalışması: Herkes migration dosyası yazar, Git'te merge edilir
Geri dönülebilirlik: Hangi migration'ın ne zaman çalıştığını bilirsin
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çinApplication 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=validateProje 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.yml4. 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ışır5. İ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ştururV2: 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=validatekullanarak 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:
flyway-coredependency'si classpath'tespring.datasource.*yapılandırılmış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:
Dosyanın güncel hash'ini hesaplar
Tablodaki hash ile karşılaştırır
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 : 987654321Bu 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.sqlAdı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.sqldosyasındaCREATE TABLE IF NOT EXISTSkullanmak 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 COLUMNsonrası verileri geri getiremezsinINSERTile eklenen veriler rollback'te silinmeli mi? BelirsizComplex 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: USEREnvironment-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' gelir11. 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: falseAdı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:runLog'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 migrationAdı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 migrationAdı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 : 567890123Sebep: 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: trueVeya 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.sql2. 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
validateveyanonekullan, ş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: trueile 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.
AI Asistan
Sorularını yanıtlamaya hazır