Ders 05 / 15
İşlemler
İki hesap arasındaki aktarım işlemle ve işlemsiz çalıştırılır; ikinci yazma başarısız olduğunda bakiye toplamını kimin değiştirebildiği ölçülür ve geri alma çağrılmayan işlemin neyi korumadığı gösterilir.
Yayın Güncelleme
Bu derste
- beginTransaction, commit ve rollBack ile birden fazla yazmayı tek birim olarak çalıştırmak
- İşlemsiz aktarımda yarıda kalan yazmanın bakiye toplamını bozduğunu ölçmek
- İşlemin sonucu ortamın sapmasından sunucuya taşıdığını göstermek
- Geri alma çağrılmayan işlemin korumayı tamamlamadığını ayırt etmek
İçindekiler
Veri erişim katmanı bir sorguyu güvenle çalıştırıyordu. Ama bir iş çoğu zaman tek sorgudan oluşmaz: para bir hesaptan düşer, başka bir hesaba eklenir. İki yazmanın arasında bir şey ters giderse ilk yazma kalır, ikincisi kalmaz. Soru şudur: yarıda kalan yazma sonucu kimin elinde bırakır?
Veritabanları kursunda işlem ve ACID özellikleri anlatıldı; atomikliğin hangi bileşenden geldiği orada aranmıştı. Burada o soru yeniden sorulmaz. Sapma uygulanır ve sonuca bakılır: ikinci yazma başarısız olduğunda bakiye toplamı değişiyor mu, değişiyorsa kimin sapmasıyla? Karar sırası öncekiyle aynıdır: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu dersin taraf hücresi sunucudur: işlem açıldığında sapmanın sonucu değiştirmesi kapanır. Çekirdek dosya her derste olduğu gibi aynen verilir.
<?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; }
VE5. Bakiyeler tam sayıdır ve bakiye >= 0 kısıtı tabloda durur; iki hesap a ve b yüz ve elli bakiyeyle başlar, yani toplam her zaman yüz ellidir. VE6. Ortamın sapması, dağıtımı yapanın tabloya koyduğu bir kısıttır: b hesabına yazılan her güncelleme reddedilir. Bu, ikinci yazmayı her seferinde aynı biçimde başarısız kılar. VE7. İstemci yalnız aktarılacak tutarı gönderir; sapması kırk yerine yirmi beş göndermesidir. VE8. Sonuç, aktarım denemesinden sonra iki bakiyenin toplamıdır.
İşlemsiz Aktarım
İşlem (transaction), birden fazla yazmayı tek bir birim olarak çalıştıran bir sarmalayıcıdır. PDO’da bu üç çağrıdır: beginTransaction() işlemi açar, commit() yazılanları kesinleştirir, rollBack() işlemin açıldığı andan beri yazılanları geri alır. Önce işlem olmadan bakalım. Aşağıdaki dosya hesapları kurar ve aktarımı iki ayrı güncelleme olarak yazar; islemli bayrağı açıkken aynı iki güncelleme işlemin içine alınır.
<?php // hesaplar.php — iki hesap ve aktarım declare(strict_types=1); function hesapKur(?string $kisit = null): PDO { $pdo = new PDO('sqlite::memory:'); $pdo->exec('CREATE TABLE hesaplar (ad TEXT PRIMARY KEY, bakiye INTEGER CHECK (bakiye >= 0))'); $pdo->exec("INSERT INTO hesaplar VALUES ('a', 100), ('b', 50)"); if ($kisit !== null) $pdo->exec($kisit); return $pdo; } function toplam(PDO $pdo): int { return (int) $pdo->query('SELECT SUM(bakiye) FROM hesaplar')->fetchColumn(); } function guncelle(PDO $pdo, string $ad, int $fark): void { $pdo->prepare('UPDATE hesaplar SET bakiye = bakiye + :fark WHERE ad = :ad') ->execute(['fark' => $fark, 'ad' => $ad]); } function aktar(PDO $pdo, int $tutar, bool $islemli): void { if ($islemli) $pdo->beginTransaction(); try { guncelle($pdo, 'a', -$tutar); guncelle($pdo, 'b', $tutar); if ($islemli) $pdo->commit(); } catch (PDOException $e) { if ($islemli) $pdo->rollBack(); } }
Sapma ortamdan gelen kısıttır: b hesabına yazan her güncelleme RAISE(ABORT) ile reddedilir. İlk yazma a hesabından tutarı düşer, ikincisi reddedilir ve işlem yoksa ilk yazma kalır.
<?php // islemsiz.php — işlem olmadan aktarım declare(strict_types=1); require 'taraf.php'; require 'hesaplar.php'; const KISIT = "CREATE TRIGGER b_kilit BEFORE UPDATE ON hesaplar WHEN NEW.ad = 'b' BEGIN SELECT RAISE(ABORT, 'reddedildi'); END"; function aktarimSonucu(array $s, bool $islemli): int { $pdo = hesapKur($s['ortam'] ?? null); aktar($pdo, $s['istemci'] ?? 40, $islemli); return toplam($pdo); } tarafRaporu('toplam, işlemsiz', fn(array $s) => aktarimSonucu($s, false), ['istemci' => 25, 'ortam' => KISIT]);
toplam, işlemsiz → ortam
istemci değiştiremedi · ortam değiştirdi
Taban toplam yüz ellidir. İstemci tutarı değiştirdiğinde toplam yine yüz elli kalır; çünkü tutar iki hesap arasında yer değiştirir, toplamı etkilemez. Ortam kısıtı koyduğunda ise ilk yazma kalır, ikincisi gitmez ve toplam düşer. Sonuç ortamındır.
İşlemle Aktarım
Aynı sapma, aynı kısıt, aynı iki güncelleme; fark yalnız islemli bayrağıdır. İkinci yazma reddedildiğinde catch bloğu rollBack() çağırır ve işlemin başından beri yazılanlar geri alınır.
<?php // islemli.php — aynı aktarım işlemin içinde declare(strict_types=1); require 'taraf.php'; require 'hesaplar.php'; const KISIT = "CREATE TRIGGER b_kilit BEFORE UPDATE ON hesaplar WHEN NEW.ad = 'b' BEGIN SELECT RAISE(ABORT, 'reddedildi'); END"; function aktarimSonucu(array $s, bool $islemli): int { $pdo = hesapKur($s['ortam'] ?? null); aktar($pdo, $s['istemci'] ?? 40, $islemli); return toplam($pdo); } tarafRaporu('toplam, işlemli', fn(array $s) => aktarimSonucu($s, true), ['istemci' => 25, 'ortam' => KISIT]);
toplam, işlemli → sunucu
istemci değiştiremedi · ortam değiştiremedi
Ortam yine ikinci yazmayı başarısız kılar, ama artık sonuç değişmez: ilk yazma da geri alınmıştır ve toplam yüz ellidir. Hiçbir dış taraf sonucu değiştiremediği için sonuç sunucuya geçti. Savunma düzeneği, yani işlem, sonucu ortamdan sunucuya taşıdı. İşlemin gücü burada yazılan kodun kendisinden değil, kodun commit() ve rollBack() çağrılarını doğru yerlere koymasından gelir.
İşlemin Durumu
İşlemin açık olup olmadığı inTransaction() ile sorulur. PDO işlemi iç içe açmaz: açık bir işlemin içinde ikinci kez beginTransaction() çağırmak da, açık işlem yokken commit() ya da rollBack() çağırmak da bir PDOException fırlatır. İç içe bir bölümü geri alınabilir yapmak gerekirse bunu kayıt noktası (savepoint) üstlenir; kayıt noktası sorgu olarak yazılır ve ROLLBACK TO yalnız o noktadan sonrasını geri alır.
<?php // durum.php — işlemin durumu, çift açma ve kayıt noktası declare(strict_types=1); require 'hesaplar.php'; $pdo = hesapKur(); echo 'başlangıç açık: ', var_export($pdo->inTransaction(), true), "\n"; try { $pdo->commit(); } catch (PDOException $e) { echo 'işlemsiz commit: ', get_class($e), "\n"; } $pdo->beginTransaction(); try { $pdo->beginTransaction(); } catch (PDOException $e) { echo 'çift açma: ', get_class($e), "\n"; } guncelle($pdo, 'a', -10); $pdo->exec('SAVEPOINT yarim'); guncelle($pdo, 'a', -30); echo 'nokta öncesi a: ', $pdo->query("SELECT bakiye FROM hesaplar WHERE ad = 'a'")->fetchColumn(), "\n"; $pdo->exec('ROLLBACK TO yarim'); echo 'nokta sonrası a: ', $pdo->query("SELECT bakiye FROM hesaplar WHERE ad = 'a'")->fetchColumn(), "\n"; $pdo->commit(); echo 'kesinleşti, toplam: ', toplam($pdo), ', açık: ', var_export($pdo->inTransaction(), true), "\n";
başlangıç açık: false işlemsiz commit: PDOException çift açma: PDOException nokta öncesi a: 60 nokta sonrası a: 90 kesinleşti, toplam: 140, açık: false
Kayıt noktasından sonraki otuzluk düşüş geri alındı, ondan önceki onluk düşüş kaldı ve commit() yalnız onu kesinleştirdi. Toplam yüz kırktır; çünkü a hesabından düşen on, b hesabına eklenmedi. Bu blokta iki yazmalı bir aktarım yoktur; kayıt noktasının davranışını göstermek için tek yönlü bir düşüş yazılmıştır.
Sınırlayıcı Ölçüm
İşlemi açmak, korumanın tamamı değildir. Aynı aktarımı bu kez rollBack() çağrısı olmadan yazalım: catch bloğu istisnayı yutar ve işlem açık kalır. Bağlantı kapandığında işlemin ne olacağı bellekteki bir veritabanında gözlenemez; veritabanı bağlantıyla birlikte yok olur ve dosya veritabanı açılmaz. Gözlenebilen şey aynı bağlantıdan, işlem hâlâ açıkken okunan toplamdır.
<?php // yarim.php — işlem açık, rollBack çağrılmıyor declare(strict_types=1); require 'taraf.php'; require 'hesaplar.php'; const KISIT = "CREATE TRIGGER b_kilit BEFORE UPDATE ON hesaplar WHEN NEW.ad = 'b' BEGIN SELECT RAISE(ABORT, 'reddedildi'); END"; function yarimSonuc(array $s): int { $pdo = hesapKur($s['ortam'] ?? null); $pdo->beginTransaction(); try { guncelle($pdo, 'a', -($s['istemci'] ?? 40)); guncelle($pdo, 'b', $s['istemci'] ?? 40); $pdo->commit(); } catch (PDOException $e) { // istisna yakalandı ama geri alma unutuldu } return toplam($pdo); } tarafRaporu('toplam, geri alma yok', 'yarimSonuc', ['istemci' => 25, 'ortam' => KISIT]); $pdo = hesapKur(KISIT); $pdo->beginTransaction(); try { guncelle($pdo, 'a', -40); guncelle($pdo, 'b', 40); } catch (PDOException $e) {} echo 'işlem hâlâ açık: ', var_export($pdo->inTransaction(), true), "\n";
toplam, geri alma yok → ortam
istemci değiştiremedi · ortam değiştirdi
işlem hâlâ açık: true
İşlem açıkken bile ortamın sapması toplamı değiştirdi. rollBack() çağrılmadığında işlem, açık kaldığı sürece hiçbir şeyi geri almaz; yalnız kesinleştirmeyi erteler. PDO açık kalan işlemi betik bittiğinde ya da bağlantı kapanırken kendiliğinden geri alır, ama o ana kadar aynı bağlantıdan okunan toplam düşmüş durumdadır. Sonuç sunucuya taşınmadı, ortamda kaldı. İşlem açmak ile işlemi doğru kapatmak aynı şey değildir.
Özet
- İşlem, birden fazla yazmayı
beginTransaction(),commit()verollBack()ile tek birim olarak çalıştırır. - İşlemsiz aktarımda ikinci yazma başarısız olduğunda ilk yazma kalır ve bakiye toplamı değişir; bu sapma ortamdan gelir.
- İşlemle aynı aktarımda
rollBack()ilk yazmayı da geri alır ve toplam sapmadan etkilenmez; bu derste taraf sunucudur. rollBack()çağrılmayan işlem açık kalır ve sonuç ortamda kalır.- Bir yazmanın etkisini yarım bırakmak, sonucu dış taraflardan birine teslim etmektir.
Sonraki Adım
İşlem verinin tutarlılığını korudu, ama tabloların kendisi nereden geliyor? Şema da bir yazma sonucudur ve her ortamda aynı olmayabilir. Sonraki ders, şemayı kimin belirlediğini göç yönetimiyle ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.