---
title: 'Şema Göçleri'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani/sema-gocleri'
course: 'Veri Erişim Katmanı ve İş Mantığı'
language: tr
updated: '2026-08-17T18:06:50+00:00'
license: 'CC BY-SA 4.0'
---

# Şema Göçleri

Şema değişikliğinin sürümlenmiş adımlara bölünmesi: ileri ve geri göç betikleri, sürüm tablosu, başarısız adımın atomik geri alınması ve bir sütunun genişlet–yaz–daralt düzeniyle hizmet kesilmeden değiştirilmesi.

Bağlantı havuzu, bütün bağlantıların aynı şemayı gördüğü varsayımıyla çalışır. Varsayım
şema değişene kadar doğrudur. Sütun eklendiğinde, kaldırıldığında ya da adı değiştiğinde
uygulamanın çalışan kopyalarıyla veritabanının durumu ayrışır: hangi kopyanın hangi şemayı
gördüğü, kopyaların ne zaman yenilendiğine bağlı kalır.

SQL Temelleri kursunda şema değiştirme, tek başına çalıştırılan bir komut olarak tanıtıldı.
Uygulama katmanında aynı komut bir **göç** (migration) adımına dönüşür: sürümlenmiş,
sırayla uygulanan, geri alınabilir ve hangi ortamda hangisinin uygulandığı kayıtlı bir
birim. Bu ders göç yürütücüsünü kurar, iki yönde çalıştırır ve bir sütunu hizmet kesilmeden
değiştirir.

## Adımlar ve Sürüm Tablosu

Göç adımları dosyadır. Her adımın ileri ve geri yönü ayrı dosyada durur; ad, sıra numarası
ile başlar.

```sh
mkdir -p goc
cat > goc/001-odunc-tablosu.up.sql <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
SQL
cat > goc/001-odunc-tablosu.down.sql <<'SQL'
DROP TABLE odunc;
DROP TABLE uye;
SQL
cat > goc/002-gecikme-ucreti.up.sql <<'SQL'
ALTER TABLE odunc ADD COLUMN gecikme_ucreti REAL NOT NULL DEFAULT 0;
SQL
cat > goc/002-gecikme-ucreti.down.sql <<'SQL'
ALTER TABLE odunc DROP COLUMN gecikme_ucreti;
SQL
cat > goc/003-acik-odunc-dizini.up.sql <<'SQL'
CREATE INDEX odunc_acik_dizini ON odunc (uye_id) WHERE iade_tarihi IS NULL;
SQL
cat > goc/003-acik-odunc-dizini.down.sql <<'SQL'
DROP INDEX odunc_acik_dizini;
SQL
```

Hangi adımın uygulandığı bilgisi veritabanının kendisinde durur. Dosya sisteminde ya da
yapılandırma dosyasında tutulsaydı, veritabanı kopyalandığında bilgi kopyayla birlikte
gitmezdi.

```js
// goc.mjs — surum tablosuna bakarak goc adimlarini ileri ve geri calistirir
import { DatabaseSync } from "node:sqlite";
import { readdirSync, readFileSync } from "node:fs";

const db = new DatabaseSync(process.env.VT ?? "kutuphane.db");
db.exec(`CREATE TABLE IF NOT EXISTS sema_surumu (
           surum INTEGER PRIMARY KEY, ad TEXT NOT NULL, uygulanma TEXT NOT NULL)`);

const adimlar = [...new Set(readdirSync("goc").map((d) => d.replace(/\.(up|down)\.sql$/, "")))]
  .sort()
  .map((ad) => ({ surum: Number(ad.slice(0, 3)), ad }));

const uygulanan = () => db.prepare("SELECT surum FROM sema_surumu ORDER BY surum")
                          .all().map((r) => r.surum);

function calistir(adim, yon) {
  const sql = readFileSync(`goc/${adim.ad}.${yon}.sql`, "utf8");
  db.exec("BEGIN");
  try {
    db.exec(sql);
    if (yon === "up") {
      db.prepare("INSERT INTO sema_surumu VALUES (?,?,datetime('now'))").run(adim.surum, adim.ad);
    } else {
      db.prepare("DELETE FROM sema_surumu WHERE surum = ?").run(adim.surum);
    }
    db.exec("COMMIT");
    console.log(`${yon === "up" ? "ileri" : "geri "}  ${adim.ad}`);
  } catch (h) {
    db.exec("ROLLBACK");
    console.log(`HATA  ${adim.ad}: ${h.message}`);
    process.exit(1);
  }
}

const [komut, hedefArg] = process.argv.slice(2);
const hedef = hedefArg === undefined ? null : Number(hedefArg);

if (komut === "durum") {
  const u = new Set(uygulanan());
  for (const a of adimlar) console.log(`${u.has(a.surum) ? "[x]" : "[ ]"} ${a.ad}`);
  console.log("gecerli surum:", Math.max(0, ...u));
} else if (komut === "yukselt") {
  const u = new Set(uygulanan());
  for (const a of adimlar) {
    if (!u.has(a.surum) && (hedef === null || a.surum <= hedef)) calistir(a, "up");
  }
} else if (komut === "alcalt") {
  const u = uygulanan().sort((x, y) => y - x);
  for (const s of u) {
    if (s > hedef) calistir(adimlar.find((a) => a.surum === s), "down");
  }
} else {
  console.log("kullanim: node goc.mjs durum | yukselt [surum] | alcalt <surum>");
}
```

Yürütücünün üç kararı vardır. Adım ile sürüm kaydı **aynı işlem içinde** yazılır; adımlar
dosya adına göre sıralanır, dolayısıyla numaralandırma sıralamayı belirler; geri yönde
sıra tersine döner.

## İleri Yürütme

```sh
node goc.mjs durum
node goc.mjs yukselt 3
node goc.mjs durum
```

```
[ ] 001-odunc-tablosu
[ ] 002-gecikme-ucreti
[ ] 003-acik-odunc-dizini
gecerli surum: 0
ileri  001-odunc-tablosu
ileri  002-gecikme-ucreti
ileri  003-acik-odunc-dizini
[x] 001-odunc-tablosu
[x] 002-gecikme-ucreti
[x] 003-acik-odunc-dizini
gecerli surum: 3
```

Üç adım sırayla uygulandı ve sürüm tablosuna yazıldı. İkinci `durum` çağrısı bilgiyi
dosyalardan değil veritabanından okuyor; aynı göç dizini başka bir veritabanına karşı
çalıştırılsaydı orada bütün kutular boş görünürdü.

## Başarısız Adımın Atomikliği

Bir adımın ortasında hata çıkarsa, o adımın yarısı uygulanmış olarak kalmamalıdır.
Aşağıdaki adım ikinci satırında kasıtlı olarak hata üretir.

```sh
cat > goc/004-bozuk-ornek.up.sql <<'SQL'
ALTER TABLE odunc ADD COLUMN sube_id INTEGER;
ALTER TABLE odunc ADD COLUMN sube_id INTEGER;
SQL
cat > goc/004-bozuk-ornek.down.sql <<'SQL'
ALTER TABLE odunc DROP COLUMN sube_id;
SQL
node goc.mjs yukselt 4
echo "cikis kodu: $?"
node goc.mjs durum
sqlite3 kutuphane.db "PRAGMA table_info(odunc);" | grep -c sube_id
```

```
HATA  004-bozuk-ornek: duplicate column name: sube_id
cikis kodu: 1
[x] 001-odunc-tablosu
[x] 002-gecikme-ucreti
[x] 003-acik-odunc-dizini
[ ] 004-bozuk-ornek
gecerli surum: 3
0
```

Üç sonuç birden görünüyor: yürütücü sıfırdan farklı bir çıkış kodu verdi, sürüm tablosunda
adım işaretlenmedi ve son satırdaki sayı `sube_id` sütununun tabloda bulunmadığını
gösteriyor. İlk `ALTER TABLE` komutu başarılı olmuştu; işlem geri alındığında o da geri
alındı.

Bu davranış motora bağlıdır. Şema değiştirme komutlarının işlem içinde geri alınabilmesi
her ilişkisel veritabanında yoktur; geri alınamayan motorlarda her adım tek bir komut
içerecek şekilde küçültülür, çünkü adımın yarısında kalmak elle onarım gerektirir.

## Geri Yürütme

Geri yön, uygulanmış adımları tersten çalıştırır.

```sh
node goc.mjs alcalt 1
node goc.mjs durum
```

```
geri   003-acik-odunc-dizini
geri   002-gecikme-ucreti
[x] 001-odunc-tablosu
[ ] 002-gecikme-ucreti
[ ] 003-acik-odunc-dizini
[ ] 004-bozuk-ornek
gecerli surum: 1
```

Önce dizin düştü, sonra sütun kaldırıldı. Sıra ters olsaydı sütunu kullanan bir dizin
ortada kalırdı. Geri yönün yazılması ihmal edilirse göç tek yönlü olur ve hatalı bir
sürümden dönüş yolu kalmaz.

## Sıfır Kesintili Sütun Değişimi

Asıl zorluk, uygulamanın çalışmaya devam ettiği bir değişikliktir. `uye` bağıntısındaki
`eposta` sütununun adı `e_posta` olacak. Tek adımda yeniden adlandırma **kırıcı
değişikliktir**: adı değiştiren komut çalıştığı anda, henüz yenilenmemiş uygulama kopyaları
hata almaya başlar.

Çözüm değişikliği üç adıma bölmektir. İlişkisel Veritabanı Yönetimi kursunda bu düzen
**genişlet–daralt göçü** adıyla geçti; arada bir yazma evresi bulunur.

1. **Genişlet:** yeni sütun eklenir ve var olan değerler kopyalanır. Eski sütun durur.
2. **Yaz:** uygulamanın yeni sürümü iki sütuna birden yazar, yeni sütundan okur. Bu
   evredeki yazma davranışına **çift yazma** denir. Eski sürüm de çalışmaya devam eder.
3. **Daralt:** bütün kopyalar yenilendikten sonra eski sütun kaldırılır.

Adımlar üç göç dosyasıdır.

```sh
cat > goc/005-eposta-genislet.up.sql <<'SQL'
ALTER TABLE uye ADD COLUMN e_posta TEXT;
UPDATE uye SET e_posta = eposta WHERE e_posta IS NULL;
SQL
cat > goc/005-eposta-genislet.down.sql <<'SQL'
ALTER TABLE uye DROP COLUMN e_posta;
SQL
cat > goc/006-eposta-dolgu.up.sql <<'SQL'
UPDATE uye SET e_posta = eposta WHERE e_posta IS NULL AND eposta IS NOT NULL;
SQL
cat > goc/006-eposta-dolgu.down.sql <<'SQL'
SELECT 1;
SQL
cat > goc/007-eposta-daralt.up.sql <<'SQL'
ALTER TABLE uye DROP COLUMN eposta;
SQL
cat > goc/007-eposta-daralt.down.sql <<'SQL'
ALTER TABLE uye ADD COLUMN eposta TEXT;
UPDATE uye SET eposta = e_posta;
SQL
```

İki kod sürümü, uygulamanın eski ve yeni kopyalarını temsil eder.

```js
// eski-kod.mjs — calisan eski surum: yalniz eposta sutununu bilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
try {
  db.prepare("INSERT INTO uye (ad, soyad, eposta, kayit_tarihi) VALUES (?,?,?,?)")
    .run("Eski", "Kayit", "eski@ornek.test", "2025-07-20");
  const n = db.prepare("SELECT count(*) AS n FROM uye WHERE eposta IS NOT NULL").get().n;
  console.log(`eski kod: calisti, eposta dolu satir = ${n}`);
} catch (h) {
  console.log(`eski kod: HATA -> ${h.message}`);
}
```

```js
// yeni-kod.mjs — yeni surum: e_posta sutununu okur; CIFT_YAZMA=1 iken eskiye de yazar
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const cift = process.env.CIFT_YAZMA === "1";
try {
  if (cift) {
    db.prepare("INSERT INTO uye (ad, soyad, e_posta, eposta, kayit_tarihi) VALUES (?,?,?,?,?)")
      .run("Yeni", "Kayit", "yeni@ornek.test", "yeni@ornek.test", "2025-07-20");
  } else {
    db.prepare("INSERT INTO uye (ad, soyad, e_posta, kayit_tarihi) VALUES (?,?,?,?)")
      .run("Yeni", "Kayit", "yeni@ornek.test", "2025-07-20");
  }
  const n = db.prepare("SELECT count(*) AS n FROM uye WHERE e_posta IS NOT NULL").get().n;
  console.log(`yeni kod: calisti (cift yazma=${cift ? "acik" : "kapali"}), e_posta dolu satir = ${n}`);
} catch (h) {
  console.log(`yeni kod: HATA -> ${h.message}`);
}
```

Aşağıdaki betik veritabanını sıfırdan kurar, her adımdan sonra iki kod sürümünü de
çalıştırır. Bozuk örnek adımı, sonraki adımların önünü tıkamaması için siliniyor.

```sh
# sifir-kesinti.sh — uc adimli sutun degisiminde iki kod surumunun durumu
rm -f kutuphane.db goc/004-bozuk-ornek.up.sql goc/004-bozuk-ornek.down.sql
node goc.mjs yukselt 3 >/dev/null
sqlite3 kutuphane.db "INSERT INTO uye (ad,soyad,eposta,kayit_tarihi)
  VALUES ('Ayşe','Demir','ayse@ornek.test','2023-02-14'),
         ('Zeynep','Arslan',NULL,'2024-01-09');"

echo "== surum 3: yalniz eski sutun var =="
node eski-kod.mjs
node yeni-kod.mjs

echo "== surum 5 (genislet): yeni sutun eklendi ve dolduruldu =="
node goc.mjs yukselt 5
node eski-kod.mjs
CIFT_YAZMA=1 node yeni-kod.mjs

echo "== surum 6 (dolgu): eski kodun yazdiklari yeni sutuna tasindi =="
node goc.mjs yukselt 6
node eski-kod.mjs
CIFT_YAZMA=1 node yeni-kod.mjs

echo "== surum 7 (daralt): eski sutun kaldirildi =="
node goc.mjs yukselt 7
node eski-kod.mjs
node yeni-kod.mjs
echo "-- adresi bos kalan satirlar --"
sqlite3 -header -column kutuphane.db "SELECT uye_id, ad, soyad FROM uye WHERE e_posta IS NULL;"
```

```sh
sh sifir-kesinti.sh
```

```
== surum 3: yalniz eski sutun var ==
eski kod: calisti, eposta dolu satir = 2
yeni kod: HATA -> table uye has no column named e_posta
== surum 5 (genislet): yeni sutun eklendi ve dolduruldu ==
ileri  005-eposta-genislet
eski kod: calisti, eposta dolu satir = 3
yeni kod: calisti (cift yazma=acik), e_posta dolu satir = 3
== surum 6 (dolgu): eski kodun yazdiklari yeni sutuna tasindi ==
ileri  006-eposta-dolgu
eski kod: calisti, eposta dolu satir = 5
yeni kod: calisti (cift yazma=acik), e_posta dolu satir = 5
== surum 7 (daralt): eski sutun kaldirildi ==
ileri  007-eposta-daralt
eski kod: HATA -> table uye has no column named eposta
yeni kod: calisti (cift yazma=kapali), e_posta dolu satir = 6
-- adresi bos kalan satirlar --
uye_id  ad      soyad 
------  ------  ------
2       Zeynep  Arslan
6       Eski    Kayit 
```

Ortadaki iki evre kesintisizliğin kanıtıdır: sürüm 5 ve 6'da hem eski hem yeni kod hatasız
çalıştı. Değişikliğin kırıcı olduğu tek an sürüm 7'dir ve o an, bütün kopyalar yenilendikten
sonraya bırakılabilir.

## Daralt Adımının Koşulu

Çıktının son bölümü bir uyarı taşıyor. `e_posta` sütunu boş kalan iki satır var. Bunlardan
`Zeynep` başlangıçtan beri adressizdi; `Eski Kayit` ise sorunludur. O satırı eski kod, dolgu
adımından **sonra** yazdı: eski sütuna yazdı, yeni sütun boş kaldı ve daralt adımı eski
sütunu kaldırdığında değer kayboldu.

Daralt adımının koşulu buradan çıkar: **eski sütuna yazan hiçbir kopya kalmamalıdır.**
Koşul, dolgu adımını tekrarlayarak değil, eski kopyaların tamamen çekildiğini doğrulayarak
sağlanır. Uygulamada bu doğrulama iki yolla yapılır: yenileme sürecinin bittiğinin
onaylanması ve daralt adımından hemen önce boş kalan satır sayısının sıfır olduğunun
sınanması.

## Geri Göçün Sınırı

Daralt adımından dönmek mümkündür, ama dönüş şemayı geri getirir; veriyi getirmez.

```sh
node goc.mjs alcalt 6
sqlite3 -header -column kutuphane.db "SELECT uye_id, ad, eposta, e_posta FROM uye WHERE uye_id IN (1,2,6);"
```

```
geri   007-eposta-daralt
uye_id  ad      eposta           e_posta        
------  ------  ---------------  ---------------
1       Ayşe    ayse@ornek.test  ayse@ornek.test
2       Zeynep                                  
6       Eski                                    
```

`eposta` sütunu geri geldi ve `e_posta` değerlerinden dolduruldu. Altıncı satırın adresi
yine boş: daralt adımı onu sildiğinde bilgi tamamen kayboldu. Geri göç bir **geri dönüş
planıdır**, bir yedek değildir. Veri kaybettiren adımların geri yönü ancak silinen veriyi
başka bir yerde saklamışsa anlamlıdır; aksi hâlde geri dönüş yalnız şemayı onarır.

## Özet

- Göç adımı sürümlenmiş, iki yönlü ve sırayla uygulanan bir birimdir; hangi adımın
  uygulandığı bilgisi veritabanının kendi içindeki sürüm tablosunda durur.
- Adımın kendisi ile sürüm kaydı aynı işlemde yazılır; hata çıkan adımda ikisi birlikte
  geri alındı ve sürüm tablosu değişmedi.
- Bir sütunun kesintisiz değiştirilmesi üç evre ister: genişlet, çift yazmayla yaz, daralt.
  Ortadaki iki evrede eski ve yeni kod sürümleri birlikte çalıştı.
- Daralt adımı, eski sütuna yazan kopya kalmadan uygulanamaz; kurala uyulmadığında dolgudan
  sonra yazılan satırın değeri kayboldu.
- Geri göç şemayı geri alır, silinen veriyi geri getirmez; geri dönüş planı yedeğin yerini
  tutmaz.

## Sonraki Adım

Göç yürütücüsü her adımı `BEGIN` ile açıp `COMMIT` ile kapattı ve hata durumunda
`ROLLBACK` çağırdı; adım ile sürüm kaydının birlikte yazılmasını sağlayan da buydu. Aynı
soru uygulamanın normal çalışması için de geçerlidir: bir ödünç verme isteği hangi
yazmaları kapsar, bu yazmaların hepsi birlikte mi kesinleşmelidir, işlemi açan ve kapatan
kod hangi katmanda durur? Sonraki konu bu soruyla açılır ve ilk ders işlem sınırını iş
biriminin tanımı üzerinden kurar: sınır fazla dar çizilirse yarım kalan durumlar, fazla
geniş çizilirse uzun süre tutulan kilitler üretir.
