Uygulama kodunuz Git'te, sürümlü, gözden geçirilmiş ve her ortama aynı biçimde dağıtılıyor. Peki veritabanı şemanız? Çoğu ekipte cevap şu oluyor: birinin makinesindeki bir .sql dosyası, bir Slack mesajındaki ALTER TABLE komutu ve "canlıya da uyguladın mı" diye sorulan bir soru. Veritabanı migration yönetimi tam olarak bu boşluğu kapatır — şema değişikliklerini kod gibi sürümler, sırayla uygular ve hangi ortamda hangi değişikliğin uygulandığını veritabanının kendisinde kaydeder.
Bu rehberde bu işi yapan iki köklü aracı, Flyway ve Liquibase'i, gerçek dosya örnekleri ve komutlarla ele alacağım. Sürümleme kurallarını, checksum doğrulamasının neden hayat kurtardığını, geri alma stratejilerini, sıfır kesintili şema değişikliği için kullanılan expand/contract desenini ve bu araçları CI/CD boru hattına nasıl yerleştireceğinizi anlatacağım. Sonunda hangi aracın sizin ekibinize daha uygun olduğuna dair net bir cevabınız olacak.
Migration Aracı Olmadan Ne Oluyor#
Aracı olmayan bir ekipte şema değişikliği şöyle ilerler: geliştirici kendi makinesinde ALTER TABLE çalıştırır, kodu yazar, test ortamına dağıtım yaparken "bu arada şu SQL'i de çalıştır" der. Test ortamı çalışır. Canlıya çıkarken birileri o SQL'i unutur ve uygulama Unknown column hatasıyla düşer.
Daha sinsi olan senaryo ise ortamların sessizce ayrışmasıdır. Test veritabanında bir indeks vardır, canlıda yoktur; canlıda bir kolonun tipi varchar(100), testte varchar(255)'tir. Bu farklar hata olarak ortaya çıkmaz, performans farkı ve zaman zaman "sadece canlıda olan" tuhaf davranışlar olarak görünür. Kimse ne zaman ayrıştığını bilemez, çünkü hiçbir yerde kayıt yoktur.
Migration araçlarının çözdüğü asıl problem budur: veritabanının içinde, uygulanmış her değişikliği sırasıyla listeleyen bir tablo tutarlar. Böylece "bu veritabanı hangi şema sürümünde" sorusunun tek ve kesin bir cevabı olur.
| Yaklaşım | Sürüm takibi | Tekrarlanabilirlik | Geri alma | Ekip ölçeği |
|---|---|---|---|---|
| Elle SQL çalıştırma | Yok | Yok | Yedekten dönmek | 1 kişi, kısa süre |
| Depoda sql klasörü | İnsan hafızası | Kısmi | Elle | Küçük ekip |
| Flyway | Otomatik tablo | Tam | Sınırlı | Her ölçek |
| Liquibase | Otomatik tablo | Tam | Yerleşik | Her ölçek |
Flyway: SQL Öncelikli ve Yalın#
Flyway'in felsefesi basittir: migration'lar düz SQL dosyalarıdır, dosya adı sürüm bilgisini taşır ve araç bunları sırayla uygular. Öğrenme eğrisi neredeyse yoktur; SQL biliyorsanız Flyway'i biliyorsunuzdur.
Dosya adlandırma kuralı üç tipten oluşur:
# Sürümlü migration: bir kez uygulanır, sırayla
db/migration/V1__ilk_semayi_olustur.sql
db/migration/V2__siparis_tablosu_ekle.sql
db/migration/V2_1__siparis_indeksleri.sql
# Tekrarlanabilir migration: içeriği her değiştiğinde yeniden uygulanır
db/migration/R__gorunum_siparis_ozeti.sql
# Geri alma dosyası (ticari sürümde)
db/migration/U2__siparis_tablosu_geri_al.sql
Ayırıcı çift alt çizgidir (V2__ad.sql); tek alt çizgi kullanırsanız Flyway dosyayı tanımaz. Sürümlü dosyalar bir kez uygulanır ve flyway_schema_history tablosuna yazılır; tekrarlanabilir dosyalar ise checksum'ı her değiştiğinde yeniden çalıştırılır ki bu, görünüm (view), saklı yordam ve tetikleyici tanımları için idealdir.
Örnek bir migration dosyası:
-- V2__siparis_tablosu_ekle.sql
CREATE TABLE siparisler (
id bigserial PRIMARY KEY,
musteri_id bigint NOT NULL REFERENCES musteriler(id),
tutar numeric(12,2) NOT NULL CHECK (tutar >= 0),
durum varchar(20) NOT NULL DEFAULT 'bekliyor',
olusturuldu timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_siparis_musteri ON siparisler (musteri_id, olusturuldu DESC);
Yapılandırma bir flyway.conf dosyasıyla ya da ortam değişkenleriyle yapılır:
# flyway.conf — parolayı dosyaya yazmak yerine ortam değişkeni tercih edin
flyway.url=jdbc:postgresql://127.0.0.1:5432/uygulama
flyway.user=uygulama_migrator
flyway.locations=filesystem:db/migration
flyway.baselineOnMigrate=true
flyway.validateOnMigrate=true
flyway.cleanDisabled=true
Günlük kullanımda ihtiyacınız olan komutlar birkaç tanedir:
# Hangi migration uygulanmış, hangisi bekliyor
flyway info
# Bekleyen migration'ları sırayla uygula
flyway migrate
# Dosyalarla veritabanı geçmişi uyuşuyor mu (checksum kontrolü)
flyway validate
# Var olan bir veritabanını Flyway yönetimine al
flyway baseline -baselineVersion=1
flyway.cleanDisabled=true satırı tesadüf değil: flyway clean komutu şemadaki her şeyi siler. Bu komut geliştirme ortamında kullanışlıdır ve üretimde felakettir. Yapılandırmada kapatın, sonra da unutun.
Liquibase: Değişiklik Kümeleri ve Yerleşik Geri Alma#
Liquibase farklı bir yol izler: migration'ları veritabanından bağımsız bir "changelog" dosyasında tanımlarsınız. Bu dosya XML, YAML, JSON ya da SQL biçiminde olabilir. Her değişiklik bir changeset'tir ve id ile author alanlarının birleşimiyle benzersiz olarak tanımlanır.
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- changeSet:
id: 2026-08-25-siparis-tablosu
author: ekip
changes:
- createTable:
tableName: siparisler
columns:
- column: {name: id, type: bigint, autoIncrement: true,
constraints: {primaryKey: true, nullable: false}}
- column: {name: musteri_id, type: bigint,
constraints: {nullable: false}}
- column: {name: tutar, type: "numeric(12,2)",
constraints: {nullable: false}}
rollback:
- dropTable:
tableName: siparisler
- changeSet:
id: 2026-08-25-siparis-indeks
author: ekip
changes:
- createIndex:
tableName: siparisler
indexName: idx_siparis_musteri
columns:
- column: {name: musteri_id}
Liquibase'in en belirgin farkı geri alma (rollback) desteğinin yerleşik olmasıdır. Her changeset için bir rollback bloğu tanımlarsınız; createTable gibi bazı değişikliklerde Liquibase geri almayı kendisi türetebilir. İkinci farkı ise soyutlamadır: createTable gibi tanımlar veritabanından bağımsızdır, aynı changelog PostgreSQL'de ve MySQL'de farklı SQL üretir.
# Uygulanmamış changeset'leri listele
liquibase status --verbose
# Uygula
liquibase update
# SQL'i çalıştırmadan üret ve gözden geçir (üretim öncesi altın kural)
liquibase updateSQL > /tmp/uygulanacak.sql
# Son iki changeset'i geri al
liquibase rollbackCount 2
# Etiketlenmiş bir noktaya dön
liquibase rollback --tag=surum-2026-08-01
Liquibase iki tablo oluşturur: uygulanan changeset'leri tutan DATABASECHANGELOG ve eşzamanlı çalıştırmayı engelleyen DATABASECHANGELOGLOCK. Ayrıca precondition desteği vardır; bir changeset'i yalnızca belirli bir koşul sağlanıyorsa çalıştırabilirsiniz. Context ve label özellikleri sayesinde de aynı changelog'dan yalnızca test ortamına ait değişiklikleri uygulayabilirsiniz — bu, test verisi yüklemeyi ayırmak için çok kullanışlıdır. Test verisi stratejilerinin tamamı için test verisi üretme ve seed yazısına bakabilirsiniz.
Flyway mı Liquibase mi#
İki araç da olgun ve güvenilir; seçim büyük ölçüde ekibinizin alışkanlıklarına ve ihtiyaç duyduğunuz özelliklere bağlıdır.
| Kriter | Flyway | Liquibase |
|---|---|---|
| Migration biçimi | Düz SQL (Java da mümkün) | XML/YAML/JSON/SQL |
| Öğrenme eğrisi | Çok düşük | Orta |
| Geri alma | Ücretli sürümde, elle | Ücretsiz, yerleşik |
| Veritabanı bağımsızlığı | Düşük (SQL yazarsınız) | Yüksek (soyut tanımlar) |
| Koşullu çalıştırma | Sınırlı | Precondition, context, label |
| Mevcut şemadan üretme | Yok | generateChangeLog var |
| Gözden geçirme kolaylığı | Yüksek (SQL okunur) | Orta (soyutlama var) |
Pratik tavsiyem şudur: tek bir veritabanı motoru kullanıyorsanız ve ekibiniz SQL'e hâkimse Flyway'i seçin — daha az kavram, daha az sürpriz, gözden geçirmesi daha kolay. Birden fazla motoru desteklemeniz gerekiyorsa, geri alma zorunluysa ya da ortama göre farklı değişiklik kümeleri çalıştıracaksanız Liquibase'in ek yetenekleri karşılığını verir.
Hangisini seçerseniz seçin, iki kural değişmez: migration'lar depoya kodla birlikte girer ve gözden geçirmeden geçer; üretime uygulamadan önce üretilen SQL insan gözüyle okunur.
Sıfır Kesintili Şema Değişikliği: Expand ve Contract#
Migration aracı size sürümleme verir ama kesintiyi tek başına engellemez. Büyük bir tabloda ALTER TABLE çalıştırmak, motora ve işleme göre tabloyu dakikalarca kilitleyebilir. Üstelik dağıtım sırasında eski ve yeni uygulama sürümü kısa bir süre aynı anda çalışır; şemanız her ikisiyle de uyumlu olmak zorundadır.
Bu problemin standart çözümü expand/contract desenidir ve üç dağıtım turunda tamamlanır:
- Expand (genişlet): Yeni kolonu
NULLkabul eden biçimde ekleyin. Eski kod bu kolonu görmez ve etkilenmez. - Çift yazma: Yeni uygulama sürümü hem eski hem yeni kolona yazar; okumada hâlâ eskiyi kullanır.
- Geri doldurma: Mevcut satırları parçalar hâlinde yeni kolona kopyalayın.
- Okumayı çevirme: Yeni sürüm artık yeni kolondan okur.
- Contract (daralt): Eski kolonu ve çift yazma kodunu kaldırın.
-- 1. tur: genişlet. NOT NULL ve DEFAULT vermeyin, tablo yeniden yazılabilir.
ALTER TABLE musteriler ADD COLUMN eposta_normalize varchar(255);
-- 3. tur: geri doldurmayı parçalara bölün, tek dev UPDATE tabloyu kilitler
UPDATE musteriler
SET eposta_normalize = lower(trim(eposta))
WHERE eposta_normalize IS NULL
AND id BETWEEN 1 AND 10000;
-- 5. tur: daralt
ALTER TABLE musteriler DROP COLUMN eposta;
Bu desende dikkat edilecek iki nokta var. Birincisi, geri doldurmayı tek bir UPDATE ile yapmak tabloyu ve replikaları uzun süre meşgul eder; binlik parçalara bölün ve aralarına kısa beklemeler koyun. İkincisi, indeks oluştururken kilitlemeyen biçimi kullanın: PostgreSQL'de CREATE INDEX CONCURRENTLY, MySQL 8'de ALGORITHM=INPLACE, LOCK=NONE.
-- PostgreSQL: tabloyu kilitlemeden indeks oluştur (transaction dışında çalışır)
CREATE INDEX CONCURRENTLY idx_musteri_eposta_norm
ON musteriler (eposta_normalize);
-- MySQL 8: yerinde ve kilitsiz indeks ekleme dener, mümkün değilse hata verir
ALTER TABLE musteriler
ADD INDEX idx_musteri_eposta_norm (eposta_normalize),
ALGORITHM=INPLACE, LOCK=NONE;
CREATE INDEX CONCURRENTLY ifadesi transaction içinde çalışamaz; Flyway'de bu migration için transaction'ı kapatmanız, Liquibase'de ise runInTransaction: false işaretlemeniz gerekir. Bu ayrıntı, "neden bu migration hata veriyor" sorusunun en sık cevabıdır.
CI/CD Boru Hattına Yerleştirme#
Migration'ı dağıtım sürecine bağlamanın birden fazla yolu var ve seçiminiz risk toleransınıza bağlı. En yaygın üç desen şunlar:
Dağıtımdan önce ayrı adım: Boru hattında uygulama dağıtılmadan önce migration çalışır. En yaygın ve en öngörülebilir yöntemdir; migration başarısız olursa dağıtım hiç başlamaz.
Uygulama açılışında: Uygulama başlarken migration'ı kendisi çalıştırır. Kolaydır ama birden fazla örnek aynı anda açılırsa yarış oluşur — her iki araç da kilit tablosu kullandığı için veri bozulmaz, ancak açılış süreleri uzar.
Elle onaylı adım: Üretim migration'ı, üretilen SQL gözden geçirildikten sonra elle onaylanır. Kritik sistemler için en güvenli olanıdır.
# Örnek CI adımı: önce doğrula, sonra SQL üret, sonra uygula
migration:
script:
- flyway -url=$DB_URL -user=$DB_USER -password=$DB_PASS validate
- flyway -url=$DB_URL -user=$DB_USER -password=$DB_PASS info
- flyway -url=$DB_URL -user=$DB_USER -password=$DB_PASS migrate
Hangi deseni seçerseniz seçin, üretim migration'ından hemen önce yedek almak pazarlık konusu değildir. Migration aracı geri alma sunsa bile, veri kaybına yol açan bir değişikliği (kolon silme, tip daraltma) geri almak yedeğe ihtiyaç duyar. MySQL tarafında mysqldump ile veritabanı yedekleme, PostgreSQL tarafında pg_dump ile yedekleme yazıları bu adımı hızlıca kurmanızı sağlar; yedeklerin nasıl dağıtılacağı konusunda ise 3-2-1 yedekleme stratejisi yazısına bakın.
Migration çalıştıran veritabanı kullanıcısının yetkilerini de ayırın. Uygulamanın günlük kullandığı kullanıcı CREATE/DROP yetkisine sahip olmamalı; migration için ayrı ve daha yetkili bir kullanıcı tanımlayın. cPanel ortamında kullanıcı ve yetki yönetimi için MySQL kullanıcı yetkileri yazısı yol gösterir.
Sık Yapılan Hatalar#
Uygulanmış bir migration dosyasını düzenlemek en klasik hatadır. Flyway her dosyanın checksum'ını saklar; dosyayı değiştirdiğinizde validate komutu hata verir ve migrate çalışmaz. Doğru çözüm dosyayı düzeltmek değil, düzeltmeyi yapan yeni bir migration eklemektir. Aynı kural Liquibase changeset'leri için de geçerlidir.
Migration içine veri taşıma mantığı gömmek ikinci hatadır. Milyonlarca satırı güncelleyen bir UPDATE'i migration olarak çalıştırmak, dağıtımı dakikalarca bekletir ve zaman aşımına uğrayabilir. Şema değişikliği ile veri taşıma işini ayırın; veri taşımayı ayrı, parçalı ve yeniden başlatılabilir bir iş olarak yürütün.
Geri dönülemez değişiklikleri hafife almak üçüncüsüdür. DROP COLUMN ve DROP TABLE geri alınamaz; rollback bloğunuz olsa bile veriyi geri getiremez. Bu tür değişiklikleri expand/contract deseninin son adımına bırakın ve mümkünse önce kolonu bir süre kullanılmadan bekletin.
Ortamlar arası sıra farkı yaratmak dördüncüsüdür. İki geliştirici aynı anda V5__ numarasını kullanırsa, sıralama ortamdan ortama değişebilir. Sürüm numarası yerine zaman damgası kullanmak (V20260825103000__ad.sql) bu çakışmayı büyük ölçüde önler.
Sıkça Sorulan Sorular#
Flyway ücretsiz mi#
Flyway'in açık kaynaklı topluluk sürümü ücretsizdir ve sürümlü migration, tekrarlanabilir migration, doğrulama ve baseline gibi günlük ihtiyaçların tamamını karşılar. Ücretli sürümlerde bulunan başlıca özellikler otomatik geri alma (undo) dosyaları, kuru çalıştırma ve bazı kurumsal veritabanı desteğidir. Çoğu ekip için topluluk sürümü fazlasıyla yeterlidir; geri alma ihtiyacınız güçlüyse ücretsiz alternatif olarak Liquibase'i değerlendirebilirsiniz.
Migration'ı geri almak gerçekten mümkün mü#
Kısmen. Kolon eklemek, indeks oluşturmak gibi değişiklikleri geri almak teknik olarak sorunsuzdur. Ancak kolon silmek, tip daraltmak ya da veri dönüştürmek gibi işlemler bilgi kaybettirir; bunları "geri almak" ancak yedekten dönmekle mümkündür. Bu yüzden geri alma özelliğine güvenmek yerine, ileri yönlü düzeltme yapmayı alışkanlık hâline getirin: hatalı bir migration'ı geri almak yerine, hatayı düzelten yeni bir migration yazın.
Var olan bir veritabanını Flyway yönetimine nasıl alırım#
flyway baseline komutu bu iş içindir. Mevcut şemayı bir başlangıç noktası olarak kabul eder ve geçmiş tablosuna bir taban sürüm kaydı yazar; bu sürümden küçük numaralı migration'lar çalıştırılmaz. Pratikte önce mevcut şemayı V1__baslangic.sql olarak dışa aktarır, sonra flyway baseline -baselineVersion=1 çalıştırır ve yeni değişiklikleri V2__ ile devam ettirirsiniz. Liquibase'de karşılığı generateChangeLog ile mevcut şemadan changelog üretip changelogSync ile senkronlamaktır.
Aynı anda birden fazla sunucu migration çalıştırırsa ne olur#
Her iki araç da bunu bir kilit mekanizmasıyla yönetir. Flyway veritabanı düzeyinde bir kilit alır, Liquibase ise DATABASECHANGELOGLOCK tablosunu kullanır. İlk gelen kilidi alır ve migration'ı çalıştırır, diğerleri bekler. Veri bozulmaz ancak bekleyen örneklerin açılışı uzar. Süreç kilidi bırakmadan çökerse kilit takılı kalabilir; Liquibase'de releaseLocks komutu bu durumu temizler.
Migration için hangi veritabanı kullanıcısını kullanmalıyım#
Uygulamanın günlük çalıştığı kullanıcıdan ayrı, şema değiştirme yetkisine sahip özel bir kullanıcı kullanın. Uygulama kullanıcısının yalnızca SELECT, INSERT, UPDATE, DELETE yetkisi olmalı; CREATE, ALTER ve DROP yetkileri yalnızca migration kullanıcısında bulunmalıdır. Bu ayrım, uygulamada bir SQL enjeksiyon açığı bulunması hâlinde saldırganın tablo silmesini ya da yeni kullanıcı oluşturmasını engeller ve denetim kayıtlarında kimin ne yaptığını netleştirir.
Büyük tabloda ALTER TABLE ne kadar sürer#
Süre, motora, işlemin tipine ve tablo boyutuna göre saniyelerden saatlere kadar değişir. Modern PostgreSQL'de varsayılan değeri olmayan bir kolon eklemek anlıktır; MySQL 8'de birçok işlem yerinde ve kilitsiz yapılabilir. Buna karşılık kolon tipi değiştirmek genellikle tablonun tamamının yeniden yazılmasını gerektirir ve bu, boyutla doğru orantılı sürer. Riskli bir değişiklik yapmadan önce mutlaka üretim boyutuna yakın bir kopya üzerinde süreyi ölçün; tahminle üretime çıkmak, planlanmamış bakım kesintisinin en sık sebebidir.
Kapanış#
Veritabanı migration yönetimi, "şema kod gibi yönetilir" ilkesinin pratik karşılığıdır. Aklınızda kalması gereken dört alışkanlık şunlar: her şema değişikliği depoya migration dosyası olarak girsin ve gözden geçirmeden geçsin; uygulanmış bir migration'ı asla düzenlemeyin, düzeltmeyi yeni bir migration olarak ekleyin; büyük tablolarda expand/contract desenini kullanın ve geri doldurmayı parçalara bölün; üretim migration'ından hemen önce yedek alın ve üretilen SQL'i insan gözüyle okuyun.
Bu süreci çalıştıracağınız ortamın da tutarlı olması gerekir; geliştirme, test ve üretim veritabanlarının aynı motor ve sürümde olması sürprizleri belirgin biçimde azaltır. İzole test ve staging veritabanları için VDS veya bulut sunucu paketleri uygun bir zemin sunar; migration öncesi yedeklerin düzenli ve dışarıda saklanması için yedekleme hizmetimize, kurulum ve bakım yükünü devretmek içinse sunucu yönetimi hizmetimize göz atabilirsiniz.