İçeriğe geç
Muhammet Şafak
Framework & Kütüphane 3 dk okuma

Eloquent ilişkileri: hasMany, belongsTo ve eager loading

Laravel Eloquent'te hasMany ve belongsTo ilişkilerini kurmayı ve N+1 sorgusunu eager loading ile çözmeyi anlattım.


Eloquent ORM ile veritabanı işlemleri kolaylaşıyor ama ilişkili verileri çekerken bir tuzak var: N+1 sorgu problemi. Bu yazıda en sık kullandığım iki ilişki türünü — hasMany ve belongsTo — anlatacağım ve ardından eager loading ile bu problemin üstesinden nasıl gelindiğini göstereceğim.

hasMany ve belongsTo ilişkileri

Bir blog uygulamasında User (Kullanıcı) ve Post (Gönderi) modelleri olduğunu varsayalım. Bir kullanıcının birden fazla gönderisi olabilir. Bu bir bir-çok (one-to-many) ilişkisidir.

User modeli, gönderiler için hasMany (çoğuna sahip) tanımlar:

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Post modeli ise kullanıcıya geri döner — belongsTo (ait olmak):

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Eloquent bu ilişkiyi kurarken varsayılan olarak posts tablosundaki user_id sütununu kullanır. Farklı bir sütun adı varsa hasMany(Post::class, 'yazar_id') şeklinde belirtebilirsiniz.

Kullanımı şöyle:

$kullanici = User::find(1);
$gonderiler = $kullanici->posts; // Tüm gönderiler

$gonderi = Post::find(5);
$yazar = $gonderi->user; // Gönderinin yazarı

İlişki metodunu özellik (property) gibi çağırdığınızda Eloquent sorguyu çalıştırır ve sonucu döndürür. Bunu dinamik özellik (dynamic property) deniyor.

N+1 sorgu problemi

İlişkiler bu kadar basitken neden dikkat gerekiyor?

Şu kodu inceleyin:

$kullanicilar = User::all();

foreach ($kullanicilar as $kullanici) {
    echo $kullanici->name . ': ' . $kullanici->posts->count() . ' gönderi';
}

Bu kod çalışır. Ama arka planda ne oluyor?

  1. User::all() — 1 sorgu (tüm kullanıcıları getir)
  2. Döngüde her kullanıcı için $kullanici->posts — her kullanıcıya 1 sorgu

10 kullanıcı varsa 11 sorgu çalışıyor. 100 kullanıcı varsa 101 sorgu. Kullanıcı sayısı arttıkça sorgu sayısı da artıyor. Buna N+1 sorgu problemi deniyor.

Bu sorunu fark etmem biraz zaman aldı. Geliştirme ortamında küçük veri kümeleriyle her şey hızlıydı; ama bir istemci projesinde kullanıcı sayısı birkaç yüze çıkınca sayfa yüklenme süresi belirgin biçimde uzadı. Sorgu sayısına bakınca 340 kullanıcı için 341 sorgu çalıştığını gördüm. Hata kod’da değil, alışkanlıktaydı.

Eager loading ile çözüm

Eager loading, ilişkili verileri ana sorguyla birlikte yükler. Bunu with() metodu ile yapıyorsunuz:

$kullanicilar = User::with('posts')->get();

foreach ($kullanicilar as $kullanici) {
    echo $kullanici->name . ': ' . $kullanici->posts->count() . ' gönderi';
}

Şimdi yalnızca 2 sorgu çalışıyor:

  1. SELECT * FROM users
  2. SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)

Kullanıcı sayısı 100 olsa bile hâlâ 2 sorgu. Bu farkı özellikle veri miktarı büyüdüğünde çok belirgin hissediyorsunuz.

Birden fazla ilişkiyi yüklemek

with() birden fazla ilişkiyi aynı anda yükleyebiliyor:

$gonderiler = Post::with(['user', 'comments'])->get();

İç içe ilişkiler için nokta sözdizimi:

$kullanicilar = User::with('posts.comments')->get();

Bu, kullanıcıları, onların gönderilerini ve her gönderinin yorumlarını tek seferde getiriyor — üç sorgu toplamda, binlerce olası N+1 yerine.

Koşullu eager loading

Yüklenen ilişkiye koşul eklemek de mümkün:

$kullanicilar = User::with([
    'posts' => function ($sorgu) {
        $sorgu->where('yayinlandi', true)->orderBy('created_at', 'desc');
    }
])->get();

Yalnızca yayımlanmış gönderiler, tarihe göre sıralı olarak yükleniyor.

Burada dikkat edilmesi gereken bir nokta var: eager loading koşulu uygulanmış ilişki, o kullanıcıya ait olmayan gönderileri değil, yalnızca koşulu geçemeyenleri dışarıda bırakıyor. Yani posts boş gelse de kullanıcı nesnesi hâlâ döner; null gelmez. Bu fark özellikle görünüm katmanında koşullu render yazarken yanıltıcı olabiliyor.

Lazy eager loading

Modeli zaten çektiniz ama sonradan ilişkiye ihtiyaç duydunuz. load() metodu bunu çözüyor:

$kullanici = User::find(1);

// Sonradan ilişkiyi yükle
$kullanici->load('posts');

Birden fazla ilişki için:

$kullanicilar = User::all();
$kullanicilar->load('posts', 'profile');

Ne zaman hangi yöntem?

Veriyi çekerken ilişkiye kesinlikle ihtiyacınız varsa with() kullanın. Koşula bağlı olarak bazen ilişkiye ihtiyaç duyacaksanız load() daha uygun olabilir.

N+1 problemini tespit etmenin pratik yolu, Laravel’in sorgu günlüğünü açmak:

DB::enableQueryLog();
// ... kod ...
dd(DB::getQueryLog());

Döngü içinde tekrarlayan sorgular görünce with() eklemenin zamanı gelmiştir.

İlk başta eager loading’i atlayıp sonradan sayfa yükleme sürelerinin uzadığını fark etmek bir uyarı olmaya başladı benim için. Artık ilişki kullanacağım her yerde with() gerekip gerekmediğini sormayı alışkanlık edindim.

Etiketler: #Laravel
Paylaş:

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi