# 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.

- Yayın: 2015-09-06
- Kategori: Framework & Kütüphane
- Etiketler: Laravel
- Okuma süresi: 3 dk okuma
- Kaynak: https://www.muhammetsafak.com.tr/blog/eloquent-iliskileri-hasmany-belongsto-ve-eager-loading/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
[Eloquent ORM ile veritabanı işlemleri kolaylaşıyor](/blog/eloquent-orm-ile-veritabani-islemlerini-sadestirmek) 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:

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

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

```php
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:

```php
$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:

```php
$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:

```php
$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:

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

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

```php
$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:

```php
$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:

```php
$kullanici = User::find(1);

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

Birden fazla ilişki için:

```php
$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:

```php
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.
