Laravel: Sıfırdan İleri Seviye

Ders 8/17 1 saat 40 dk

Eloquent İlişkileri (Relationships)

One-to-One, One-to-Many, Many-to-Many, Polymorphic ilişkiler ve eager loading.

Eloquent İlişkileri (Relationships)

İlişkiler, veritabanı tablolarınız arasındaki bağlantıları tanımlar. Laravel, tüm yaygın ilişki türlerini destekler.

🎯 Önemli: İlişkileri doğru anlamak, N+1 query probleminden kaçınmak ve verimli sorgular yazmak için kritiktir.

One-to-One (Bire-Bir)

Bir kullanıcının bir profili olması gibi.

// Migration: profiles tablosu
Schema::create('profiles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
    $table->string('bio')->nullable();
    $table->string('avatar')->nullable();
    $table->date('birth_date')->nullable();
    $table->timestamps();
});

// User Model
class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
        // users.id = profiles.user_id
    }
}

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

// Kullanım
$user = User::find(1);
$profile = $user->profile;          // Profile instance
$bio = $user->profile->bio;

$profile = Profile::find(1);
$user = $profile->user;             // User instance

// Oluşturma
$user->profile()->create([
    'bio' => 'Merhaba dünya!',
    'avatar' => 'avatar.jpg',
]);

One-to-Many (Bire-Çok)

Bir yazarın birden fazla blog yazısı olması gibi.

// Migration: posts tablosu
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->text('content');
    $table->timestamps();
});

// User Model
class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
        // users.id = posts.user_id
    }
}

// Post Model
class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    // Alternatif isim (author)
    public function author()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

// Kullanım
$user = User::find(1);
$posts = $user->posts;              // Collection of Posts
$postCount = $user->posts()->count();

$post = Post::find(1);
$author = $post->user;              // User instance
$authorName = $post->user->name;

// Oluşturma
$user->posts()->create([
    'title' => 'Yeni Yazı',
    'content' => 'İçerik...',
]);

// Birden fazla
$user->posts()->createMany([
    ['title' => 'Yazı 1', 'content' => '...'],
    ['title' => 'Yazı 2', 'content' => '...'],
]);

Many-to-Many (Çoka-Çok)

Bir yazının birden fazla etiketi, bir etiketin birden fazla yazısı olması gibi.

// Migration: tags tablosu
Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

// Migration: pivot tablosu (post_tag)
// İsimlendirme: alfabetik sıra, tekil (post_tag, not posts_tags)
Schema::create('post_tag', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
    
    $table->unique(['post_id', 'tag_id']);
});

// Post Model
class Post extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
        // Otomatik pivot: post_tag tablosu
    }
    
    // Pivot tablosu özelleştirme
    public function tagsWithTimestamps()
    {
        return $this->belongsToMany(Tag::class)
                    ->withTimestamps()
                    ->withPivot('order', 'is_primary');
    }
}

// Tag Model
class Tag extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}

// Kullanım
$post = Post::find(1);
$tags = $post->tags;                // Collection of Tags

$tag = Tag::find(1);
$posts = $tag->posts;               // Collection of Posts

// İlişki yönetimi
$post->tags()->attach(1);           // Tek tag ekle
$post->tags()->attach([1, 2, 3]);   // Çoklu ekle

$post->tags()->detach(1);           // Tek tag çıkar
$post->tags()->detach([1, 2]);      // Çoklu çıkar
$post->tags()->detach();            // Tümünü çıkar

$post->tags()->sync([1, 2, 3]);     // Sadece bunlar kalsın
$post->tags()->syncWithoutDetaching([4, 5]); // Ekle, silme

$post->tags()->toggle([1, 2]);      // Varsa çıkar, yoksa ekle

// Pivot verileri ile
$post->tags()->attach(1, ['is_primary' => true]);
$post->tags()->sync([
    1 => ['is_primary' => true],
    2 => ['is_primary' => false],
]);

// Pivot verilerine erişim
foreach ($post->tags as $tag) {
    echo $tag->pivot->created_at;
    echo $tag->pivot->is_primary;
}

Has Many Through

Ara tablo üzerinden ilişki. Örneğin: Ülke -> Kullanıcılar -> Yazılar

// Country Model
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            Post::class,    // Final model
            User::class,    // Ara model
            'country_id',   // users tablosundaki foreign key
            'user_id',      // posts tablosundaki foreign key
            'id',           // countries tablosundaki local key
            'id'            // users tablosundaki local key
        );
    }
}

// Kullanım
$country = Country::find(1);
$posts = $country->posts;  // Türkiye'deki tüm kullanıcıların yazıları

Polymorphic İlişkiler

Bir model, birden fazla farklı modele ait olabilir. Örneğin: Yorumlar hem yazılara hem de videolara yapılabilir.

// Migration: comments tablosu
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable');  // commentable_type ve commentable_id
    $table->timestamps();
});

// Comment Model
class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

// Post Model
class Post extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// Video Model
class Video extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// Kullanım
$post = Post::find(1);
$comments = $post->comments;

$video = Video::find(1);
$comments = $video->comments;

$comment = Comment::find(1);
$parent = $comment->commentable;  // Post veya Video instance

// Oluşturma
$post->comments()->create(['body' => 'Harika yazı!']);
$video->comments()->create(['body' => 'Güzel video!']);

Eager Loading (N+1 Problemi Çözümü)

⚠️ N+1 Query Problemi: En yaygın performans sorunlarından biri. Her ilişki için ayrı sorgu çalıştırılması.
// ❌ N+1 Problemi
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->user->name;  // Her post için ayrı sorgu!
}
// 1 (posts) + N (users) = N+1 sorgu

// ✅ Eager Loading ile çözüm
$posts = Post::with('user')->get();
foreach ($posts as $post) {
    echo $post->user->name;  // Sorgu yok, zaten yüklü
}
// Sadece 2 sorgu: posts ve users

// Birden fazla ilişki
$posts = Post::with(['user', 'tags', 'category'])->get();

// İç içe ilişkiler
$posts = Post::with('user.profile')->get();
$posts = Post::with(['user.profile', 'comments.user'])->get();

// Koşullu eager loading
$posts = Post::with(['comments' => function ($query) {
    $query->where('is_approved', true)
          ->orderBy('created_at', 'desc');
}])->get();

// Lazy eager loading (sonradan yükleme)
$posts = Post::all();
$posts->load('user');
$posts->load(['tags', 'category']);

// Sayım ile
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
    echo $post->comments_count;
}

Önemli Noktalar

  • hasOne/belongsTo: One-to-One ilişki
  • hasMany/belongsTo: One-to-Many ilişki
  • belongsToMany: Many-to-Many (pivot tablo gerekir)
  • morphTo/morphMany: Polymorphic ilişkiler
  • with() ile eager loading N+1 problemini çözer