Ders 06 / 15
Göç Yönetimi
Göç dosyaları, göç kaydı tablosu, sıralı uygulama ve geri alma küçük bir düzenekte kurulur; bu çalıştırmada kaç göçün uygulandığını veritabanının durumunun, şema sürümünü ise kodun belirlediği ölçülür.
Yayın Güncelleme
Bu derste
- Göç dosyasını, göç kaydı tablosunu ve sıralı uygulamayı kurmak
- Son göçü geri almayı ve kaydın ona göre güncellendiğini göstermek
- Bu çalıştırmada uygulanan göç sayısını hangi tarafın değiştirebildiğini ölçmek
- Şema sürümünün her ortamda aynı kaldığını göstermek
İçindekiler
İşlem, yazmanın bütün olarak kalmasını sağladı. Ama yazmaların üzerinde durduğu tablolar da bir yerden gelmek zorundadır ve uygulama büyüdükçe bu tablolar değişir. Şemayı elle değiştirmek, hangi ortamda hangi değişikliğin yapıldığını kimsenin bilmediği bir duruma götürür. Soru şudur: şemayı hangi veritabanı belirliyor?
Çerçeve kursundaki Göçler ve Tohumlama dersi, bir göçün çalıştığını ölçmüştü. Bu derste göçün kendisi yeniden anlatılmaz; sorulan şey, bu çalıştırmada kaç göçün uygulandığını kimin değiştirebildiğidir. Veritabanları kursundaki şema göçleri dersi de kavramı zaten tanıttı; burada yalnız bir PHP düzeneği kurulur. Karar sırası değişmez: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu dersin ağırlıklı tarafı ortamdır. Çekirdek dosya aynen gelir.
<?php // taraf.php — bu sonucu kim değiştirebiliyor? declare(strict_types=1); // Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım. const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam']; /** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */ function sina(callable $sonuc, array $sapmalar): array { $taban = $sonuc([]); $degisti = []; foreach (DIS_TARAFLAR as $taraf) { if (array_key_exists($taraf, $sapmalar)) { $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban; } } return $degisti; } /** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */ function taraf(array $degisti): string { foreach (DIS_TARAFLAR as $t) { if ($degisti[$t] ?? false) return $t; } return 'sunucu'; } function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string { $degisti = sina($sonuc, $sapmalar); $satirlar = []; foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi'); $sonTaraf = taraf($degisti); echo "$ad → $sonTaraf\n ", implode(' · ', $satirlar), "\n"; return $sonTaraf; }
VE9. Göç dosyaları PHP dizisidir ve her göç bir ileri sorgusuyla bir geri sorgusu taşır; dosya sistemi okunmaz. VE10. Göç kaydı gocler tablosudur ve uygulanan her göçün adını tutar. VE11. Ortamın sapması veritabanının durumudur: boş bir veritabanı ya da göçleri önceden uygulanmış bir veritabanı. VE12. İstemci yalnız göçlerden sonra eklenecek üyenin adını gönderir; sapması ayse yerine mehmet göndermesidir.
Göç, Kayıt ve Sıra
Göç (migration), şemada tek bir değişikliği anlatan, adı sıralı bir dosyadır. Adın başındaki numara sırayı belirler: 001 önce, 003 sonra uygulanır. Göç kaydı, hangi göçlerin bu veritabanına uygulandığını tutan tablodur. Uygulayıcı kayıtta olmayan göçleri sırayla çalıştırır, her birini bir işlemin içinde uygular ve işlem bitince adını kayda ekler. Kayıt ve değişiklik aynı işlemde yazıldığı için biri olmadan öteki kalmaz.
Geri alma da aynı yapıyı tersine kullanır: kayıttaki son göçün geri sorgusu çalıştırılır ve adı kayıttan silinir. Aşağıdaki dosya üç göçü, kaydı ve iki uygulayıcıyı tanımlar. Çıktısı yoktur, yalnız diske yazılır.
<?php // gocler.php — göç dosyaları, göç kaydı ve sıralı uygulama declare(strict_types=1); const GOCLER = [ '001-uyeler' => ['CREATE TABLE uyeler (id INTEGER PRIMARY KEY, ad TEXT)', 'DROP TABLE uyeler'], '002-puan' => ['ALTER TABLE uyeler ADD COLUMN puan INTEGER DEFAULT 0', 'ALTER TABLE uyeler DROP COLUMN puan'], '003-indeks' => ['CREATE INDEX uyeler_ad ON uyeler (ad)', 'DROP INDEX uyeler_ad'], ]; function gocKaydiKur(PDO $pdo): void { $pdo->exec('CREATE TABLE IF NOT EXISTS gocler (ad TEXT PRIMARY KEY)'); } function uygulananlar(PDO $pdo): array { return $pdo->query('SELECT ad FROM gocler ORDER BY ad')->fetchAll(PDO::FETCH_COLUMN); } /** Kayıtta olmayan göçleri sırayla uygular; bu çalıştırmada uygulananların adını döndürür. */ function gocleriUygula(PDO $pdo): array { gocKaydiKur($pdo); $yapilan = uygulananlar($pdo); $buCalistirma = []; foreach (GOCLER as $ad => [$ileri, $geri]) { if (in_array($ad, $yapilan, true)) continue; $pdo->beginTransaction(); $pdo->exec($ileri); $pdo->prepare('INSERT INTO gocler (ad) VALUES (:ad)')->execute(['ad' => $ad]); $pdo->commit(); $buCalistirma[] = $ad; } return $buCalistirma; } /** Kayıttaki son göçü geri alır; geri aldığının adını döndürür. */ function sonGocuGeriAl(PDO $pdo): ?string { $yapilan = uygulananlar($pdo); $ad = end($yapilan); if ($ad === false) return null; $pdo->beginTransaction(); $pdo->exec(GOCLER[$ad][1]); $pdo->prepare('DELETE FROM gocler WHERE ad = :ad')->execute(['ad' => $ad]); $pdo->commit(); return $ad; } function sonSurum(PDO $pdo): ?string { $ad = $pdo->query('SELECT MAX(ad) FROM gocler')->fetchColumn(); return $ad === false || $ad === null ? null : $ad; }
Şimdi düzeneği boş bir veritabanında çalıştıralım. İkinci çalıştırmada kayıtta her göç bulunduğu için hiçbir göç uygulanmaz.
<?php // uygulama.php — uygulama, ikinci çalıştırma ve geri alma declare(strict_types=1); require 'gocler.php'; $pdo = new PDO('sqlite::memory:'); echo 'ilk çalıştırma: ', implode(', ', gocleriUygula($pdo)), "\n"; echo 'ikinci çalıştırma: ', count(gocleriUygula($pdo)), " göç\n"; echo 'geri alınan: ', sonGocuGeriAl($pdo), "\n"; echo 'kayıt: ', implode(', ', uygulananlar($pdo)), "\n"; echo 'yeniden uygulanan: ', implode(', ', gocleriUygula($pdo)), "\n"; echo 'sütunlar: ', implode(', ', $pdo->query("SELECT name FROM pragma_table_info('uyeler')")->fetchAll(PDO::FETCH_COLUMN)), "\n";
ilk çalıştırma: 001-uyeler, 002-puan, 003-indeks ikinci çalıştırma: 0 göç geri alınan: 003-indeks kayıt: 001-uyeler, 002-puan yeniden uygulanan: 003-indeks sütunlar: id, ad, puan
İkinci çalıştırma sıfır göç uyguladı; çünkü uygulanıp uygulanmayacağına göç dosyası değil kayıt karar verir. Geri alınan göç yalnız sonuncusudur ve kayıttan silindiği için bir sonraki çalıştırmada yeniden uygulanır. Dosyalar aynı kalır, değişen kayıttır.
Veri Taşıyan Göç
Şema göçü yalnız tablo yapısını değiştirir; ama bazı göçler sütun eklemekle kalmaz, ona değer de yazar. Geri sorgusu böyle bir göçün şemasını eski hâline döndürür, yazılan değerleri döndürmez. Aşağıdaki dosya üç göçü uygular, bir üye ekler, seviye sütununu ekleyip puandan doldurur ve ardından geri sorgusuyla sütunu kaldırır.
<?php // veritasi.php — sütun ekleyip dolduran göçün geri alınması declare(strict_types=1); require 'gocler.php'; $pdo = new PDO('sqlite::memory:'); gocleriUygula($pdo); $pdo->exec("INSERT INTO uyeler (ad, puan) VALUES ('ayse', 7), ('mehmet', 3)"); $pdo->exec('ALTER TABLE uyeler ADD COLUMN seviye TEXT'); $pdo->exec("UPDATE uyeler SET seviye = CASE WHEN puan > 5 THEN 'üst' ELSE 'alt' END"); echo 'göçten sonra: ', json_encode($pdo->query('SELECT ad, seviye FROM uyeler ORDER BY id')->fetchAll(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE), "\n"; $pdo->exec('ALTER TABLE uyeler DROP COLUMN seviye'); echo 'geri aldıktan sonra: ', json_encode($pdo->query('SELECT * FROM uyeler ORDER BY id')->fetchAll(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE), "\n";
göçten sonra: [{"ad":"ayse","seviye":"üst"},{"ad":"mehmet","seviye":"alt"}]
geri aldıktan sonra: [{"id":1,"ad":"ayse","puan":7},{"id":2,"ad":"mehmet","puan":3}]
Şema geri geldi, seviye değerleri gelmedi. Geri alma bir yapıyı eski hâline getirir; yazılmış veriyi geri getirmez. Bu yüzden veri taşıyan bir göçün geri sorgusu, yazmadan önce yedek almayı ya da geri alınamaz olarak işaretlenmeyi gerektirebilir. Bu karar göçü yazanındır ve kodda kayıtlı durur; hangi ortamda çalıştığına bakmaz.
Sonuç Kime Ait
Ölçülen sonuç, bu çalıştırmada uygulanan göç sayısıdır. Aynı kod, boş bir veritabanında üç göç uygular; göçleri önceden uygulanmış bir veritabanında hiçbirini uygulamaz. Hangi durumun geçerli olduğu kodun içinde değildir, uygulamanın bağlandığı veritabanındadır ve veritabanını hazırlayan taraf dağıtımı yapandır. İstemci ise göçler bittikten sonra bir üye ekler; bu, kayıtta hiçbir şeyi değiştirmez.
<?php // gocsonuc.php — bu çalıştırmada kaç göç uygulandı? declare(strict_types=1); require 'taraf.php'; require 'gocler.php'; function uygulananSayi(array $s): int { $pdo = new PDO('sqlite::memory:'); if (($s['ortam'] ?? 'bos') === 'guncel') gocleriUygula($pdo); $buCalistirma = gocleriUygula($pdo); $pdo->prepare('INSERT INTO uyeler (ad) VALUES (:ad)')->execute(['ad' => $s['istemci'] ?? 'ayse']); return count($buCalistirma); } echo 'boş veritabanı: ', uygulananSayi([]), "\n"; echo 'güncel veritabanı: ', uygulananSayi(['ortam' => 'guncel']), "\n"; tarafRaporu('uygulanan göç sayısı', 'uygulananSayi', ['istemci' => 'mehmet', 'ortam' => 'guncel']);
boş veritabanı: 3
güncel veritabanı: 0
uygulanan göç sayısı → ortam
istemci değiştiremedi · ortam değiştirdi
İstemci sayıyı değiştirmedi: üyenin adı göçlerin uygulanıp uygulanmadığına karışmaz. Ortam değiştirdi: veritabanı güncelken sayı üçten sıfıra indi. Sonuç ortama aittir. Göç düzeneğinin kendisi doğru çalışıyor; ama tek bir çalıştırmanın sonucu, düzeneğin değil, onu çalıştıran veritabanının durumunu yansıtır.
Sınırlayıcı Ölçüm
Aynı düzenekten bir başka sonuç alalım: göçler çalıştıktan sonraki şema sürümü, yani kayıttaki son göçün adı. Veritabanı boş da olsa güncel de olsa, göçler çalıştığında kayıtta aynı son ad durur. Bu sonuç veritabanının başlangıç durumuna bağlı değildir.
<?php // gocsinir.php — çalıştırmadan sonraki şema sürümü declare(strict_types=1); require 'taraf.php'; require 'gocler.php'; function sonrakiSurum(array $s): string { $pdo = new PDO('sqlite::memory:'); if (($s['ortam'] ?? 'bos') === 'guncel') gocleriUygula($pdo); gocleriUygula($pdo); $pdo->prepare('INSERT INTO uyeler (ad) VALUES (:ad)')->execute(['ad' => $s['istemci'] ?? 'ayse']); return sonSurum($pdo) ?? 'yok'; } tarafRaporu('çalıştırmadan sonraki sürüm', 'sonrakiSurum', ['istemci' => 'mehmet', 'ortam' => 'guncel']);
çalıştırmadan sonraki sürüm → sunucu
istemci değiştiremedi · ortam değiştiremedi
Veritabanının durumu değişse de sürüm değişmedi; sürümü dağıtımın başlangıç durumu değil, göç dosyalarının son adı belirliyor. Aynı düzenekten iki sonuç iki farklı sahiplik verdi: uygulanan sayı ortamın, çalıştırma sonrası sürüm sunucunun. Bir göç düzeneği hangi soruyu yanıtladığına göre okunmalıdır.
Özet
- Göç, adı sıralı bir dosyadır; göç kaydı tablosu hangilerinin uygulandığını tutar ve uygulayıcı yalnız kayıtta olmayanları sırayla çalıştırır.
- Her göç ve kaydı aynı işlemde yazılır; geri alma son göçün geri sorgusunu çalıştırıp adı kayıttan siler.
- Bu çalıştırmada uygulanan göç sayısı veritabanının durumuna bağlıdır; bu derste taraf ortamdır.
- Çalıştırmadan sonraki şema sürümü her ortamda aynıdır ve sonuç sunucuya aittir.
- Aynı düzenekten alınan iki sonuç farklı tarafa ait olabilir.
Sonraki Adım
Şema artık kayıt altında; peki bu şemanın üstüne kurulan nesne–tablo bağlaması hangi sorguları arka planda gönderiyor? Sonraki ders, bir listeyi basarken gidenleri sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.