Ders 07 / 15
Nesne–Tablo Bağlamasının Sorguları
Bir liste basılırken gönderilen sorgular bir sayaçla sayılır; tembel yüklemede sorgu sayısının satırla büyüdüğü, istekli yüklemede ikide kaldığı ve sorgu metninin ise gönderilen veriye göre değişebildiği ölçülür.
Yayın Güncelleme
Bu derste
- PDO çağrılarını saran bir sayaçla bir liste için gönderilen sorguları saymak
- Tembel yüklemede N+1 sorununu sorgu sayısıyla göstermek
- İstekli yüklemenin sorgu sayısını sabit tuttuğunu ve bunun sahibini ölçmek
- Sorgu metninin sorgu sayısından farklı bir sonuç olduğunu ayırt etmek
İçindekiler
Şema artık kayıt altında. Üstüne bir nesne–tablo bağlaması (object-relational mapping) kurulduğunda uygulama kodu satır değil nesne görür ve sorguları yazmaz. Sorgular yine gider; yalnızca nerede ve kaç tane gittiği koddan okunmaz hale gelir. Soru şudur: bir liste basılırken veritabanına kaç sorgu gidiyor?
Bu derste model kurulmaz. Çerçeve kursundaki Nesne ve Tablo Bağlaması ile İlişkiler dersleri modeli ve ilişkinin yüklenme zamanını işledi. Daha önceki bir kursta sorgu sayısı da ölçülmüştü; o ölçümde sayı yalnızca bir koşum sayısıydı. Burada sayı bir sonuçtur ve sorulan şey sahibidir: sayıyı istemci mi, ortam mı, yoksa sunucu mu belirliyor? Karar sırası aynıdır: istemci, üçüncü taraf, ortam; hiçbiri değiştiremiyorsa sunucu. Bu dersin taraf hücresi sunucudur ve çekirdek aşağıdadır.
<?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; }
VE13. Bağlamanın yaptığı iş, PDO çağrılarını saran küçük bir sınıfla gösterilir; her sec() çağrısı bir sorgu sayılır ve sorgu metni de kaydedilir. VE14. Üç yazar ve altı yazı vardır; yazıların yazarları 1 ve 2’dir, 3 numaralı yazarın henüz yazısı yoktur. Sorgu önbelleği yoktur. VE15. İstemci sapması bir yazı daha eklemesidir; sapmanın değeri, eklenen yazının yazar numarasıdır. VE16. Sonuç, bir liste basılırken gönderilen sorgu sayısıdır.
Sayaç ve İki Yükleyici
Aşağıdaki dosya sayacı ve veriyi kurar, ardından listeyi iki biçimde yükler. Tembel yükleme (lazy loading) listeyi bir sorguyla alır, sonra her yazı için yazarını ayrı bir sorguyla ister. İstekli yükleme (eager loading) listeyi bir sorguyla alır, ardından gereken bütün yazarları IN (...) listesiyle tek ek sorguda getirir. Tembel biçimdeki bu davranış N+1 sorunu diye adlandırılır: bir liste sorgusu, artı satır başına bir sorgu.
<?php // sayac.php — PDO çağrılarını sayan sarmalayıcı ve iki liste yükleyicisi declare(strict_types=1); final class SayacliVeritabani { public int $sorgu = 0; public array $metinler = []; public function __construct(private PDO $pdo) {} public function sec(string $sql, array $degerler = []): array { $this->sorgu++; $this->metinler[] = $sql; $hazir = $this->pdo->prepare($sql); $hazir->execute($degerler); return $hazir->fetchAll(PDO::FETCH_ASSOC); } } function veriKur(?int $ekYazar = null): SayacliVeritabani { $pdo = new PDO('sqlite::memory:'); $pdo->exec('CREATE TABLE yazarlar (id INTEGER PRIMARY KEY, ad TEXT)'); $pdo->exec('CREATE TABLE yazilar (id INTEGER PRIMARY KEY, yazar_id INTEGER, baslik TEXT)'); $pdo->exec("INSERT INTO yazarlar VALUES (1, 'ayse'), (2, 'mehmet'), (3, 'zeynep')"); $ekle = $pdo->prepare('INSERT INTO yazilar (yazar_id, baslik) VALUES (?, ?)'); for ($i = 1; $i <= 6; $i++) $ekle->execute([($i % 2) + 1, "yazı $i"]); if ($ekYazar !== null) $ekle->execute([$ekYazar, 'yazı 7']); return new SayacliVeritabani($pdo); } function listeTembel(SayacliVeritabani $db): array { $satirlar = []; foreach ($db->sec('SELECT yazar_id, baslik FROM yazilar ORDER BY id') as $yazi) { $yazar = $db->sec('SELECT ad FROM yazarlar WHERE id = ?', [$yazi['yazar_id']]); $satirlar[] = $yazi['baslik'] . ' — ' . $yazar[0]['ad']; } return $satirlar; } function listeIstekli(SayacliVeritabani $db): array { $yazilar = $db->sec('SELECT yazar_id, baslik FROM yazilar ORDER BY id'); $kimlikler = array_values(array_unique(array_column($yazilar, 'yazar_id'))); sort($kimlikler); $yerTutucular = implode(', ', array_fill(0, count($kimlikler), '?')); $adlar = array_column($db->sec("SELECT id, ad FROM yazarlar WHERE id IN ($yerTutucular)", $kimlikler), 'ad', 'id'); return array_map(fn(array $y) => $y['baslik'] . ' — ' . $adlar[$y['yazar_id']], $yazilar); }
İki yükleyiciyi aynı veriyle çalıştıralım. Liste aynı olmalı; fark yalnız sorgu sayısında çıkar.
<?php // liste.php — iki yükleyici, aynı liste, farklı sorgu sayısı declare(strict_types=1); require 'sayac.php'; $tembel = veriKur(); $tembelListe = listeTembel($tembel); $istekli = veriKur(); $istekliListe = listeIstekli($istekli); echo 'ilk satır: ', $tembelListe[0], "\n"; echo 'aynı liste: ', var_export($tembelListe === $istekliListe, true), "\n"; echo 'tembel: ', $tembel->sorgu, " sorgu\n"; echo 'istekli: ', $istekli->sorgu, " sorgu\n";
ilk satır: yazı 1 — mehmet aynı liste: true tembel: 7 sorgu istekli: 2 sorgu
İki yükleyici de aynı altı satırı üretti. Tembel yükleme yedi sorgu gönderdi: biri liste için, altısı yazarlar için. İstekli yükleme ikisini gönderdi. Bağlama aracı kod tarafında ikisini de aynı görünüme sokar; fark yalnız veritabanına giden trafikte kalır.
Sayı Kime Ait
Şimdi sayıyı bir sonuç olarak ele alalım. İstemci sapması, listeye bir yazı daha eklenmesidir ve yazının yazarı 3 numaralıdır. Tembel yüklemede her yazı bir yazar sorgusu getirdiği için sayı yediden sekize çıkar. İstekli yüklemede yazar sorgusu tek kalır; yeni yazı sorgu sayısına katılmaz, yalnızca IN listesine bir değer ekler.
<?php // sorgusayisi.php — sorgu sayısını kim değiştirebiliyor? declare(strict_types=1); require 'taraf.php'; require 'sayac.php'; function sorguSayisi(string $yukleme, ?int $ekYazar): int { $db = veriKur($ekYazar); $yukleme === 'tembel' ? listeTembel($db) : listeIstekli($db); return $db->sorgu; } tarafRaporu('tembel yükleme', fn(array $s) => sorguSayisi('tembel', $s['istemci'] ?? null), ['istemci' => 3]); tarafRaporu('istekli yükleme', fn(array $s) => sorguSayisi('istekli', $s['istemci'] ?? null), ['istemci' => 3]);
tembel yükleme → istemci
istemci değiştirdi
istekli yükleme → sunucu
istemci değiştiremedi
Tembel yüklemenin sorgu sayısı satır sayısıyla büyüdüğü için istemci değiştirebildi: her yeni yazı sayıyı bir artırdı. İstekli yüklemede sayı ikide kaldı; sapma etkisiz olduğu için sonuç sunucuya geçti. Savunma düzeneği, yani istekli yükleme, sonucu istemciden sunucuya taşıdı. Bu, yükleme biçiminin kodda seçilmesi demektir; bu seçimi yazılan kod verir, gelen istek vermez.
Yazar Hiç Okunmazsa
İstekli yükleme her zaman daha az sorgu göndermez. Liste yalnızca başlıkları basıyorsa yazara hiç erişilmez; tembel yükleme bu durumda ilişki sorgusunu hiç göndermez, istekli yükleme ise sonucu kullanılmayacak bir sorguyu yine de gönderir. İki yükleme biçimi arasındaki fark, ilişkinin erişilip erişilmediğine bağlıdır. Aşağıdaki dosya aynı listeyi yalnız başlıkları basacak biçimde iki yükleyiciyle yeniden kurar.
<?php // baslik.php — ilişkiye hiç erişilmeyen liste declare(strict_types=1); require 'sayac.php'; $tembel = veriKur(); $basliklar = array_column($tembel->sec('SELECT baslik FROM yazilar ORDER BY id'), 'baslik'); echo 'tembel, yalnız başlık: ', $tembel->sorgu, " sorgu\n"; $istekli = veriKur(); listeIstekli($istekli); echo 'istekli, yazarlar boşa getirildi: ', $istekli->sorgu, " sorgu\n"; $basliklar2 = array_column($istekli->sec('SELECT baslik FROM yazilar ORDER BY id'), 'baslik'); echo 'başlıklar aynı: ', var_export($basliklar === $basliklar2, true), "\n";
tembel, yalnız başlık: 1 sorgu istekli, yazarlar boşa getirildi: 2 sorgu başlıklar aynı: true
Tembel yükleme yalnız başlıkları istediğinde tek sorgu yeterli oldu. İstekli yükleme yazarları gereksiz yere getirdi. Bu yüzden istekli yüklemenin sunucuya taşıdığı koruma, ilişkinin gerçekten kullanıldığı listeler için geçerlidir; kullanılmayan ilişkide aynı seçim fazladan bir sorgu gönderir.
Sınırlayıcı Ölçüm
İstekli yüklemenin sorgu sayısı sabit; peki sorgunun kendisi? IN listesindeki yer tutucu sayısı, listedeki farklı yazar sayısından kurulur. Sonuç olarak sorgu sayısı yerine ikinci sorgunun metni alınırsa soru değişir. İstemci sapmasının iki biçimini deneyelim: yeni yazı yeni bir yazara aitse (3) ve zaten listede olan bir yazara aitse (1).
<?php // sorgumetni.php — sorgu sayısı değil, sorgu metni declare(strict_types=1); require 'taraf.php'; require 'sayac.php'; function ikinciSorgu(?int $ekYazar): string { $db = veriKur($ekYazar); listeIstekli($db); return $db->metinler[1]; } tarafRaporu('IN listesi, yeni yazar', fn(array $s) => ikinciSorgu($s['istemci'] ?? null), ['istemci' => 3]); tarafRaporu('IN listesi, var olan yazar', fn(array $s) => ikinciSorgu($s['istemci'] ?? null), ['istemci' => 1]);
IN listesi, yeni yazar → istemci
istemci değiştirdi
IN listesi, var olan yazar → sunucu
istemci değiştiremedi
Sonuç, gönderilen veriye bağlı çıktı. Yeni yazar listeye girdiğinde yer tutucu sayısı arttı ve sorgu metni istemcinin eliyle değişti. Var olan yazarın yazısı eklendiğinde metin aynı kaldı. Sorgu sayısı sunucuya taşınmıştı; sorgu metni taşınmadı. Bir savunmanın hangi sonucu koruduğu, o sonucun neyi ölçtüğüne bağlıdır.
Özet
- Nesne–tablo bağlaması sorguları saklar; PDO çağrılarını saran bir sayaç kaç sorgu gittiğini görünür kılar.
- Tembel yüklemede sorgu sayısı satır sayısıyla büyür (N+1 sorunu); sayıyı istemci değiştirebilir.
- İstekli yüklemede sorgu sayısı
INlistesiyle ikide kalır; bu derste taraf sunucudur. - Aynı düzenekte sorgu metni, gönderilen veriye göre istemci tarafından değiştirilebilir.
- Bir savunmanın neyi koruduğu ölçülen sonuca bağlıdır: sayı korundu, metin korunmadı.
Sonraki Adım
Sorguların sayısı ve metni kontrol altındayken, sorguların döndürdüğü veri de bir sınırdan geçer. Uygulamanın dışındaki bir ortaktan gelen veri JSON, XML ya da CSV olarak gelir ve biçimi değişebilir. Sonraki ders, ortağın verisi biçim değiştirirse ne olduğunu ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.