İçeriğe geç
academia.sh

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 ertelendi kalır; kaydı olmayan ilişki ise bağ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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat