Ders 10 / 21
İlişkiler
Bire bir, bire çok ve çoktan çoğa ilişkiler küçük bir modelde kurulur; tembel yüklemede ilişki sorgusunun erişime kadar ertelendiği, istekli yüklemede listeyle birlikte koştuğu ve hiç erişilmeyen ilişkinin ertelenmiş kaldığı ölçülür.
Yayın Güncelleme
Bu derste
- Bire bir, bire çok ve çoktan çoğa ilişkileri model yöntemleri olarak kurmak
- Tembel yüklemede ilişki sorgusunun erişene kadar ertelendiğini ve N+1 sorununu ölçmek
- İstekli yüklemede ilişki sorgusunun listeyle birlikte koştuğunu göstermek
- Hiç erişilmeyen ilişkinin ertelendi sınıfında kaldığını, kaydı olmayan ilişkinin ise bağlanmadığını ayırt etmek
İçindekiler
Model kaydedilirken hangi parçanın koştuğunu gördük. Bir yazının yazarı ise başka bir tabloda durur ve ona ulaşan sorgunun ne zaman çalıştığı modelin yazılışından okunmaz. Soru şudur: yazılar listelendiğinde yazarlarını getiren sorgu o anda mı çalışır, yoksa yazara ilk erişildiğinde mi?
Bu dersin ölçü ekseni, ilişki sorgusunun bu istekte hangi koşulla koştuğudur. Karar sırası sinifla() fonksiyonunun kendisidir: koştu, ertelendi, atlandı, bağlanmadı. Ağırlıklı sınıf ertelendi‘dir: parça isteğin dışına bırakılmıştır, erişilene kadar çalışmaz. Sorgu sayısı da basılacak, ama bu M10/K03’ten ayrı bir şeydir: orada koşum sayısı ölçülmüştü; burada sayı sınıfı belirlemez. Sınıf, ilişkinin neden ve hangi koşulda çalıştığını söyleyen sinifla() çıktısından gelir; sayı yalnızca yanına konan kanıttır.
GV12. Tablolar PHP dizileridir ve her Depo::sec() çağrısı bir sorgu sayılır; sorgu sayacı veri katmanının kendisinde tutulur. GV13. Tembel yüklemede Gunluk::ertele() kaydı, nesne oluşturulurken yazılır; yani ilişki, nesne var olduğu andan itibaren ertelenmiş sayılır. GV14. On yazı üç yazar arasında dağıtılmıştır ve sorgu önbelleği yoktur. GV15. İstekli yüklemede ilişki sorgusu, listenin sorgusunun hemen ardından tek sorguyla çalışır.
Ölçüm düzeneği değişmeden gelir.
<?php // kostu.php — beklenen bir parça bu istekte koştu mu? declare(strict_types=1); final class Gunluk { public static array $yol = []; // isteğin yoluna konan parçalar public static array $kostu = []; // istek içinde çağrılanlar public static array $ertelenen = []; // isteğin dışına bırakılanlar public static function yeniIstek(): void { self::$yol = self::$kostu = self::$ertelenen = []; } public static function yola(string $parca): void { self::$yol[] = $parca; } public static function kostu(string $parca): void { self::$kostu[] = $parca; } public static function ertele(string $parca): void { self::$ertelenen[] = $parca; } } function sinifla(string $parca): string { return match (true) { in_array($parca, Gunluk::$kostu, true) => 'koştu', in_array($parca, Gunluk::$ertelenen, true) => 'ertelendi', in_array($parca, Gunluk::$yol, true) => 'atlandı', default => 'bağlanmadı', }; }
İlişkileri Kurmak
İlişki, bir modelden başka bir modele giden yöntemdir. Bire bir ilişkide bir yazarın tek bir profili vardır. Bire çok ilişkide bir yazarın birçok yazısı vardır ve her yazının tek bir yazarı vardır. Çoktan çoğa ilişkide bir yazının birçok etiketi, bir etiketin birçok yazısı vardır ve ilişki bir ara tabloda (pivot) tutulur. Tembel yükleme (lazy loading) ilişkiyi ilk erişimde sorgular; istekli yükleme (eager loading) ilişkiyi liste sorgusuyla birlikte, tek ek sorguyla getirir.
Aşağıdaki dosya üç ilişki türünü ve iki yükleme biçimini kurar. Yazi nesnesi, yazarı önceden yüklenmemişse oluşurken iliski:yazar kaydını ertelenmiş olarak yazar; yazara erişildiğinde sorgu çalışır ve kayıt koştu olur. Önceden yüklendiyse liste yükleyicisi kaydı hemen koştu olarak yazar.
<?php // iliski.php — ilişki yöntemleri, tembel ve istekli yükleme declare(strict_types=1); require_once 'kostu.php'; final class Depo { public static int $sorgu = 0; public static array $tablolar = [ 'yazarlar' => [['id' => 1, 'ad' => 'Ayse'], ['id' => 2, 'ad' => 'Mert'], ['id' => 3, 'ad' => 'Deniz']], 'profiller' => [['yazar_id' => 1, 'site' => 'ayse.test'], ['yazar_id' => 2, 'site' => 'mert.test']], 'etiketler' => [['id' => 1, 'ad' => 'php'], ['id' => 2, 'ad' => 'web']], 'yazi_etiket' => [['yazi_id' => 1, 'etiket_id' => 1], ['yazi_id' => 1, 'etiket_id' => 2], ['yazi_id' => 2, 'etiket_id' => 1]], 'yazilar' => [], ]; public static function sec(string $tablo, callable $kosul): array { self::$sorgu++; return array_values(array_filter(self::$tablolar[$tablo], $kosul)); } } Depo::$tablolar['yazilar'] = array_map( fn($i) => ['id' => $i, 'baslik' => "Yazi $i", 'yazar_id' => ($i % 3) + 1], range(1, 10) ); final class Yazar { public function __construct(public array $satir) {} public function profil(): ?array { // bire bir return Depo::sec('profiller', fn($p) => $p['yazar_id'] === $this->satir['id'])[0] ?? null; } public function yazilar(): array { // bire çok return Depo::sec('yazilar', fn($y) => $y['yazar_id'] === $this->satir['id']); } } final class Yazi { private array $yuklu = []; public function __construct(public array $satir, ?array $yazar = null) { if ($yazar === null) Gunluk::ertele('iliski:yazar'); else $this->yuklu['yazar'] = $yazar; } public function yazar(): array { // yazının tek yazarı if (!isset($this->yuklu['yazar'])) { Gunluk::kostu('iliski:yazar'); $this->yuklu['yazar'] = Depo::sec('yazarlar', fn($y) => $y['id'] === $this->satir['yazar_id'])[0]; } return $this->yuklu['yazar']; } public function etiketler(): array { // çoktan çoğa, ara tablo üzerinden $idler = array_column(Depo::sec('yazi_etiket', fn($r) => $r['yazi_id'] === $this->satir['id']), 'etiket_id'); return Depo::sec('etiketler', fn($e) => in_array($e['id'], $idler, true)); } public static function liste(array $yukle = []): array { $satirlar = Depo::sec('yazilar', fn() => true); $yazarlar = null; if (in_array('yazar', $yukle, true)) { Gunluk::kostu('iliski:yazar'); $idler = array_unique(array_column($satirlar, 'yazar_id')); $yazarlar = array_column(Depo::sec('yazarlar', fn($y) => in_array($y['id'], $idler, true)), null, 'id'); } return array_map(fn($s) => new self($s, $yazarlar[$s['yazar_id']] ?? null), $satirlar); } } function yeniIstek(): void { Gunluk::yeniIstek(); Depo::$sorgu = 0; } function raporla(array $parcalar): void { foreach ($parcalar as $parca) printf(" %-16s %s\n", $parca, sinifla($parca)); echo " sorgu=" . Depo::$sorgu . "\n"; }
Önce üç ilişki türünün sonuçlarına bakalım; sınıf yok, yalnızca ilişkilerin çalıştığının kanıtı.
<?php // turler.php — üç ilişki türü require_once 'iliski.php'; $yazarlar = array_map(fn($s) => new Yazar($s), Depo::$tablolar['yazarlar']); echo "profil: " . $yazarlar[0]->profil()['site'] . "\n"; echo "yazar 1 yazi sayisi: " . count($yazarlar[0]->yazilar()) . "\n"; $yazi = Yazi::liste()[0]; echo "yazi 1 etiketleri: " . implode(',', array_column($yazi->etiketler(), 'ad')) . "\n";
profil: ayse.test yazar 1 yazi sayisi: 3 yazi 1 etiketleri: php,web
Tembel Yükleme
Ölçüm on yazıyı listeler ve yazarlarına henüz bakmaz. Beklenen: liste sorgusu çalışmış, ilişki ertelendi ve sorgu sayısı 1. Sonra döngü her yazının yazarına erişir. Beklenen: ilişki koştu ve sorgu sayısı 1 + 10.
<?php // tembel.php — tembel yükleme ve N+1 sorunu require_once 'iliski.php'; yeniIstek(); $yazilar = Yazi::liste(); echo "listelendi: " . count($yazilar) . " yazi\n"; raporla(['iliski:yazar']); foreach ($yazilar as $yazi) $yazi->yazar(); echo "yazarlara erisildi\n"; raporla(['iliski:yazar']);
listelendi: 10 yazi iliski:yazar ertelendi sorgu=1 yazarlara erisildi iliski:yazar koştu sorgu=11
Liste çalıştığında ilişki sorgusu henüz yazılmamıştı: ertelendi, sorgu sayısı bir. Döngü her yazıda ayrı bir yazar sorgusu çalıştırdı; N+1 sorunu (N+1 problem) budur: bir liste sorgusu ve listedeki her satır için bir sorgu daha. Aynı yazar birden çok kez sorgulanır, çünkü önbellek yoktur. Sayı sorunu gösterir; sınıf ise sorunun nedenini: ilişki erişime kadar ertelenmişti ve erişim bir döngünün içindeydi.
Karşıt Ölçüm: İstekli Yükleme
Aynı liste, yazar ilişkisi istenerek yüklenir. Beklenen: ilişki listeyle birlikte koştu, erişimden önce bile ve sorgu sayısı 2.
<?php // istekli.php — istekli yüklemede ilişki listeyle birlikte koşar require_once 'iliski.php'; yeniIstek(); $yazilar = Yazi::liste(['yazar']); echo "listelendi: " . count($yazilar) . " yazi\n"; raporla(['iliski:yazar']); foreach ($yazilar as $yazi) $yazi->yazar(); echo "yazarlara erisildi\n"; raporla(['iliski:yazar']);
listelendi: 10 yazi iliski:yazar koştu sorgu=2 yazarlara erisildi iliski:yazar koştu sorgu=2
İlişki, hiçbir yazının yazarına bakılmadan koştu; döngü sonrası sorgu sayısı değişmedi. Bedeli, yazarlara hiç bakılmasa da sorgunun çalışmasıdır. Tembel yükleme ile istekli yükleme arasındaki seçim, ilişkiye erişileceğinin bilinip bilinmemesine bağlıdır.
Sınırlayıcı Ölçüm
Erişilmeyen bir ilişki ertelenmiş kalır mı yoksa bağlanmaz mı? Cevap, kaydın nerede yazıldığına bağlıdır. Burada Gunluk::ertele() nesne oluşturulurken yazılır, yani ilişki var olan nesnenin parçası sayılır. Hiçbir yazının yazarına erişilmeyen istekte iliski:yazar yine ertelendi kalır. Aynı istekte, adı hiçbir yere yazılmayan bir ilişki, örneğin iliski:profil, hiçbir kayıt bırakmaz ve bağlanmadı sınıfına düşer.
<?php // erisilmeyen.php — erişilmeyen ilişki ertelenmiş kalır, kaydı olmayan bağlanmaz require_once 'iliski.php'; yeniIstek(); $yazilar = Yazi::liste(); echo "yazarlara hic erisilmedi\n"; raporla(['iliski:yazar', 'iliski:profil']);
yazarlara hic erisilmedi iliski:yazar ertelendi iliski:profil bağlanmadı sorgu=1
iliski:yazar ertelendi, iliski:profil bağlanmadı. İkisi de hiç çalışmadı, ama aralarındaki fark büyüktür: ertelenen, ilişkinin bir erişimle çalışabileceğini bilir; bağlanmayanın böyle bir yolu hiç tanımlanmamıştır. Kaydı nesne oluşturulurken değil erişim sırasında yazan bir tasarımda iliski:yazar de bu istekte bağlanmadı olurdu. Yani sınıf, ilişkinin kendisinin değil, kayıt noktasının seçimine bağlı bir bilgidir; bu seçim dersin başında açıkça verilmiştir.
Özet
- Bire bir, bire çok ve çoktan çoğa ilişkiler birer model yöntemidir; çoktan çoğa ilişki ara tabloyu kullanır.
- Bu dersin ölçü sınıfı
ertelendi’dir: tembel yüklemede ilişki sorgusu, liste sorgusundan sonra bile erişilene kadar çalışmadı. - Döngüde erişim N+1 sorununu doğurdu; sorgu sayısı sorunu gösterdi, sınıf ise ertelemenin nedenini.
- İstekli yüklemede ilişki listeyle birlikte
koştu; bedeli, erişilmese de sorgunun çalışmasıdır. - Hiç erişilmeyen ilişki
ertelendikalır; kaydı olmayan ilişki isebağlanmadıolur.
Sonraki Adım
İlişkiler tablolara dayanır ve tablolar bir yerden gelir. Şemayı kuran göç ikinci kez çalıştırıldığında ne olur? Bir sonraki ders göçleri ve tohumlamayı ölçer ve ikinci çalışmada göçün atlandı sınıfına düştüğünü gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.