Laravel Framework

Eloquent Relationships

20 min Lesson 8 of 45

Eloquent Relationships

Eloquent relationships are one of the most powerful features of Laravel. They allow you to define connections between database tables and work with related data using intuitive, object-oriented syntax. In this lesson, you'll master all major relationship types.

Why Use Relationships?

Relationships eliminate the need for complex JOIN queries and make your code more readable and maintainable. Instead of writing SQL, you access related data as object properties.

Tip: Think of relationships as connections between your models. A user has many posts, a post belongs to a user, and a post has many tags through a pivot table.

One to One (hasOne / belongsTo)

A one-to-one relationship is where one record in a table is related to exactly one record in another table. Example: A user has one profile.

Database Structure

// users table
id | name | email | created_at | updated_at

// profiles table
id | user_id | bio | website | avatar | created_at | updated_at

Defining the Relationship

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    // User has one Profile
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }
}

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

Using One-to-One Relationships

// Access related data
$user = User::find(1);
$profile = $user->profile;  // Returns Profile model

echo $profile->bio;
echo $profile->website;

// Reverse relationship
$profile = Profile::find(1);
$user = $profile->user;  // Returns User model

echo $user->name;

// Create related record
$user = User::find(1);
$user->profile()->create([
    'bio' => 'Laravel developer',
    'website' => 'https://example.com',
    'avatar' => 'avatar.jpg',
]);

// Update related record
$user->profile->update([
    'bio' => 'Updated bio',
]);

One to Many (hasMany / belongsTo)

A one-to-many relationship is where one record can be associated with multiple related records. Example: A user has many posts.

Database Structure

// users table
id | name | email | created_at | updated_at

// posts table
id | user_id | title | content | published_at | created_at | updated_at

Defining the Relationship

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

class Post extends Model
{
    protected $fillable = ['title', 'content', 'published_at'];

    // Post belongs to User
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Using One-to-Many Relationships

// Access all posts of a user
$user = User::find(1);
$posts = $user->posts;  // Returns Collection of Post models

foreach ($posts as $post) {
    echo $post->title;
}

// Query the relationship
$publishedPosts = $user->posts()
    ->where('published_at', '!=', null)
    ->orderBy('published_at', 'desc')
    ->get();

// Count related records
$postCount = $user->posts()->count();

// Check if relationship exists
if ($user->posts()->exists()) {
    echo "User has posts";
}

// Create related record
$user->posts()->create([
    'title' => 'My New Post',
    'content' => 'Post content here...',
    'published_at' => now(),
]);

// Save existing model
$post = new Post([
    'title' => 'Another Post',
    'content' => 'More content...',
]);
$user->posts()->save($post);

// Save multiple models
$user->posts()->saveMany([
    new Post(['title' => 'Post 1']),
    new Post(['title' => 'Post 2']),
]);

Many to Many (belongsToMany)

A many-to-many relationship is where multiple records in one table are related to multiple records in another table. Example: A post has many tags, and a tag has many posts.

Database Structure

// posts table
id | title | content | created_at | updated_at

// tags table
id | name | slug | created_at | updated_at

// post_tag table (pivot table)
id | post_id | tag_id | created_at | updated_at
Note: Pivot table names should be in alphabetical order: post_tag (not tag_post). Foreign keys should be singular: post_id and tag_id.

Defining the Relationship

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // Post belongs to many Tags
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

class Tag extends Model
{
    protected $fillable = ['name', 'slug'];

    // Tag belongs to many Posts
    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}

Using Many-to-Many Relationships

// Access all tags of a post
$post = Post::find(1);
$tags = $post->tags;

foreach ($tags as $tag) {
    echo $tag->name;
}

// Access all posts with a specific tag
$tag = Tag::where('slug', 'laravel')->first();
$posts = $tag->posts;

// Attach tags to a post (add relationships)
$post->tags()->attach([1, 2, 3]);  // By IDs
$post->tags()->attach($tag);       // By model

// Detach tags (remove relationships)
$post->tags()->detach([1, 2]);     // Remove specific tags
$post->tags()->detach();           // Remove all tags

// Sync tags (replace all existing)
$post->tags()->sync([1, 2, 3]);

// Sync without detaching
$post->tags()->syncWithoutDetaching([4, 5]);

// Toggle tags (attach if not attached, detach if attached)
$post->tags()->toggle([1, 2, 3]);

Pivot Table Data

Access and store additional data on the pivot table:

<?php

// Define pivot columns in relationship
class Post extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class)
            ->withPivot('featured', 'order')
            ->withTimestamps();
    }
}

// Access pivot data
$post = Post::find(1);
foreach ($post->tags as $tag) {
    echo $tag->name;
    echo $tag->pivot->featured;
    echo $tag->pivot->order;
    echo $tag->pivot->created_at;
}

// Attach with pivot data
$post->tags()->attach(1, [
    'featured' => true,
    'order' => 1
]);

// Sync with pivot data
$post->tags()->sync([
    1 => ['featured' => true, 'order' => 1],
    2 => ['featured' => false, 'order' => 2],
]);

Has Many Through

This relationship provides a shortcut for accessing distant relations through an intermediate relation. Example: A country has many posts through users.

Database Structure

// countries table
id | name

// users table
id | country_id | name

// posts table
id | user_id | title | content

Defining the Relationship

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    // Country has many Posts through Users
    public function posts()
    {
        return $this->hasManyThrough(
            Post::class,      // Final model
            User::class,      // Intermediate model
            'country_id',    // Foreign key on users table
            'user_id',       // Foreign key on posts table
            'id',            // Local key on countries table
            'id'             // Local key on users table
        );
    }
}

Using Has Many Through

$country = Country::find(1);
$posts = $country->posts;  // All posts by users in this country

foreach ($posts as $post) {
    echo $post->title;
}

Polymorphic Relationships

Polymorphic relationships allow a model to belong to more than one other model on a single association. Example: Comments can belong to both posts and videos.

Database Structure

// posts table
id | title | content

// videos table
id | title | url

// comments table
id | body | commentable_id | commentable_type | created_at
Note: The commentable_type column stores the model class name (e.g., "App\Models\Post"), and commentable_id stores the related model's ID.

Defining Polymorphic Relationships

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $fillable = ['body'];

    // Comment belongs to commentable (Post or Video)
    public function commentable()
    {
        return $this->morphTo();
    }
}

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

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

Using Polymorphic Relationships

// Create comments for a post
$post = Post::find(1);
$post->comments()->create([
    'body' => 'Great post!'
]);

// Create comments for a video
$video = Video::find(1);
$video->comments()->create([
    'body' => 'Nice video!'
]);

// Retrieve all comments
$post->comments;   // Comments for post
$video->comments;  // Comments for video

// Reverse relationship (get parent)
$comment = Comment::find(1);
$commentable = $comment->commentable;  // Returns Post or Video

if ($commentable instanceof Post) {
    echo "Comment on post: " . $commentable->title;
} elseif ($commentable instanceof Video) {
    echo "Comment on video: " . $commentable->title;
}

Eager Loading

Eager loading solves the N+1 query problem by loading relationships in advance, dramatically improving performance.

Warning: Without eager loading, accessing relationships in a loop causes N+1 queries. For 100 users, that's 1 query to load users + 100 queries to load each user's posts = 101 queries!

The N+1 Problem

// BAD: N+1 queries (1 + 100 = 101 queries for 100 users)
$users = User::all();  // 1 query

foreach ($users as $user) {
    echo $user->posts->count();  // 100 additional queries!
}

Solution: Eager Loading

// GOOD: Eager loading (only 2 queries)
$users = User::with('posts')->get();  // 2 queries total

foreach ($users as $user) {
    echo $user->posts->count();  // No additional queries!
}

// Eager load multiple relationships
$posts = Post::with(['user', 'tags', 'comments'])->get();

// Nested eager loading
$countries = Country::with('users.posts')->get();

// Conditional eager loading
$users = User::with([
    'posts' => function ($query) {
        $query->where('published_at', '!=', null)
              ->orderBy('published_at', 'desc')
              ->limit(5);
    }
])->get();

// Eager load specific columns
$users = User::with('posts:id,user_id,title')->get();

Lazy Eager Loading

$users = User::all();

// Load relationship after retrieving models
if ($someCondition) {
    $users->load('posts');
}

// Load with constraints
$users->load([
    'posts' => function ($query) {
        $query->where('published', true);
    }
]);

Querying Relationships

// Has: Get users who have at least one post
$users = User::has('posts')->get();

// Has with count
$users = User::has('posts', '>=', 3)->get();

// WhereHas: Get users with published posts
$users = User::whereHas('posts', function ($query) {
    $query->where('published_at', '!=', null);
})->get();

// DoesntHave: Get users without posts
$users = User::doesntHave('posts')->get();

// WhereDoesntHave: Get users without published posts
$users = User::whereDoesntHave('posts', function ($query) {
    $query->where('published_at', '!=', null);
})->get();

// Count related records
$users = User::withCount('posts')->get();

foreach ($users as $user) {
    echo $user->posts_count;  // posts_count attribute added
}
Exercise 1: Blog Relationships

Create a complete blog relationship structure:

  • User hasMany Posts
  • Post belongsTo User
  • Post hasMany Comments
  • Comment belongsTo Post and User
  • Post belongsToMany Tags
  • Create migrations for all tables including pivot table
  • Define all relationships in models
  • Test creating posts with tags and comments
Exercise 2: Polymorphic Images

Implement a polymorphic relationship for images:

  • Create an Image model that can belong to Users (avatar) and Posts (featured image)
  • Create migration with imageable_id and imageable_type
  • Define morphMany in User and Post models
  • Define morphTo in Image model
  • Test attaching images to users and posts
  • Query all posts with their images using eager loading
Exercise 3: Optimize N+1 Queries

Given this code with N+1 query problem:

$posts = Post::all();
foreach ($posts as $post) {
    echo $post->user->name;
    echo $post->comments->count();
    foreach ($post->tags as $tag) {
        echo $tag->name;
    }
}

Rewrite it using eager loading to execute only 4 queries total.

Best Practices

  • Always Use Eager Loading: When accessing relationships in loops, always eager load to avoid N+1 queries.
  • Name Relationships Clearly: Use singular for belongsTo, plural for hasMany and belongsToMany.
  • Use Query Scopes: Combine relationships with query scopes for cleaner code.
  • Leverage Relationship Methods: Use relationship methods like create(), save(), and attach() instead of manual foreign key assignment.
  • Monitor Query Count: Use Laravel Debugbar to monitor query count and identify N+1 problems.
  • Cascade Deletes: Set up proper foreign key constraints with onDelete('cascade') in migrations.

Summary

In this lesson, you've mastered:

  • One-to-one relationships (hasOne / belongsTo)
  • One-to-many relationships (hasMany / belongsTo)
  • Many-to-many relationships with pivot tables (belongsToMany)
  • Has many through relationships for distant relations
  • Polymorphic relationships for flexible associations
  • Eager loading to eliminate N+1 query problems
  • Querying relationships with has, whereHas, and withCount

Eloquent relationships transform complex database interactions into elegant, readable code. Master them, and you'll build powerful, efficient applications with ease!