Copied!
Programming
Laravel
PHP

Laravel Eloquent Relationships – Complete Guide with Examples

Laravel Eloquent Relationships – Complete Guide with Examples
Shahroz Javed
Mar 13, 2026 . 123 views

Introduction

Database tables almost always have relationships with each other. A blog post has many comments, a user belongs to a role, an order has many products. Laravel's Eloquent ORM makes working with these relationships simple and expressive.

Instead of writing raw SQL joins, Eloquent lets you define relationships directly in your model classes and then access related data as if it were a simple property. In this guide, we'll cover every major relationship type with real-world examples.

One to One

A one-to-one relationship is the most basic type. For example, a User has one Profile.

Define the Relationship

In the User model, use hasOne:

// App\Models\User.php
public function profile()
{
    return $this->hasOne(Profile::class);
}
// Laravel assumes the foreign key is 'user_id' on the profiles table

Inverse: belongsTo

In the Profile model, define the inverse using belongsTo:

// App\Models\Profile.php
public function user()
{
    return $this->belongsTo(User::class);
}

Access the Related Model

$profile = User::find(1)->profile;

$user = Profile::find(1)->user;

Custom Foreign Key

If your foreign key name differs from the convention, pass it as the second argument:

return $this->hasOne(Profile::class, 'foreign_key', 'local_key');
⚠️ Note: Accessing a relationship as a property (e.g. $user->profile) runs a database query. Use eager loading when you need the relationship for multiple models.

One to Many

A one-to-many relationship is used when one model owns multiple instances of another. For example, a Post has many Comments.

Define the Relationship

// App\Models\Post.php
public function comments()
{
    return $this->hasMany(Comment::class);
}
// Laravel assumes foreign key is 'post_id' on the comments table

Inverse: belongsTo

// App\Models\Comment.php
public function post()
{
    return $this->belongsTo(Post::class);
}

Access Related Models

// Get all comments for a post (returns a Collection)
$comments = Post::find(1)->comments;

// Add a query constraint on the relationship
$activeComments = Post::find(1)->comments()->where('approved', true)->get();

// Get the parent post from a comment
$post = Comment::find(5)->post;

Default Model

If a related model might not exist (e.g. a post with no author), you can return a default model instead of null using withDefault():

public function author()
{
    return $this->belongsTo(User::class)->withDefault(function ($user, $post) {
        $user->name = 'Guest Author';
    });
}

Many to Many

Many-to-many relationships require a third pivot table to link the two models. A classic example is users and roles — a user can have many roles and a role can belong to many users.

Database Structure

users       → id, name
roles       → id, name
role_user   → user_id, role_id   (pivot table)

Define the Relationship

// App\Models\User.php
public function roles()
{
    return $this->belongsToMany(Role::class);
    // Laravel infers the pivot table name as 'role_user' (alphabetical order)
}

// App\Models\Role.php
public function users()
{
    return $this->belongsToMany(User::class);
}

Access & Attach

// Get all roles for a user
$roles = User::find(1)->roles;

// Attach a role
$user->roles()->attach($roleId);

// Detach a role
$user->roles()->detach($roleId);

// Sync roles (removes old, adds new)
$user->roles()->sync([1, 2, 3]);

// Toggle (attach if not exists, detach if exists)
$user->roles()->toggle([1, 2, 3]);

Pivot Table Data

You can store extra data on the pivot table (like an expiry date) and access it via the pivot property:

// Include extra pivot columns
public function roles()
{
    return $this->belongsToMany(Role::class)
                ->withPivot('active', 'expires_at')
                ->withTimestamps()
                ->as('subscription');  // rename pivot to 'subscription'
}

// Access it
foreach ($user->roles as $role) {
    echo $role->subscription->expires_at;
}

// Attach with pivot data
$user->roles()->attach($roleId, ['expires_at' => now()->addYear()]);

Filter by Pivot Table

$activeRoles = $user->roles()->wherePivot('active', 1)->get();

Querying Relationships

Eloquent provides powerful methods to query whether a relationship exists, without actually loading the related models.

Check Existence with has()

// Posts that have at least one comment
$posts = Post::has('comments')->get();

// Posts that have 3 or more comments
$posts = Post::has('comments', '>=', 3)->get();

// Posts that have comments with images (nested)
$posts = Post::has('comments.images')->get();

Filter with whereHas()

Add constraints on the related model while checking existence:

// Posts that have comments starting with "code"
$posts = Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'code%');
})->get();

// Posts with 10+ approved comments
$posts = Post::whereHas('comments', function ($query) {
    $query->where('approved', true);
}, '>=', 10)->get();

Query Absence with whereDoesntHave()

// Posts that have no comments from banned authors
$posts = Post::whereDoesntHave('comments.author', function ($query) {
    $query->where('banned', true);
})->get();

Eager Loading & the N+1 Problem

This is one of the most important performance topics in Laravel. When you lazy-load relationships in a loop, you create the N+1 query problem.

The N+1 Problem

// BAD — runs 1 query for posts, then 1 query PER post for author = N+1 queries
$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name;  // new DB query each iteration
}

Fix It with Eager Loading

Use with() to load all relationships in just 2 queries total, no matter how many records:

// GOOD — only 2 queries total
$posts = Post::with('author')->get();

// Multiple relationships
$posts = Post::with(['author', 'comments'])->get();

// Nested relationships
$posts = Post::with('author.contacts')->get();

Eager Loading Specific Columns

// Only select id and name from the author
// IMPORTANT: always include the foreign key in the selected columns
$posts = Post::with('author:id,name')->get();

Constrained Eager Loading

$posts = Post::with(['comments' => function ($query) {
    $query->where('approved', true)->orderBy('created_at', 'desc');
}])->get();

Lazy Eager Loading

When you already have a collection and want to load a relationship afterward:

$posts = Post::all();

// Load the relationship after the fact
$posts->load('author', 'comments');

// Only load if not already loaded
$posts->loadMissing('author');

Always Eager Load (Default)

// In your model — author is always eager-loaded
protected $with = ['author'];

// Override for a specific query
Post::without('author')->get();
Post::withOnly('comments')->get();

Prevent Lazy Loading

In development, you can force eager loading by throwing an error whenever lazy loading occurs:

// In AppServiceProvider::boot()
Model::preventLazyLoading(! $this->app->isProduction());
💡 Related: Check out our guide on Laravel Query Builder for writing powerful database queries alongside Eloquent.

Aggregating Related Models

Sometimes you don't need to load all related records — you just need a count or a sum. Eloquent provides dedicated methods for this that run efficient subqueries.

withCount

// Add a comments_count column to each post
$posts = Post::withCount('comments')->get();

echo $posts[0]->comments_count;

// Count with a condition
$posts = Post::withCount(['comments as approved_count' => function ($query) {
    $query->where('approved', true);
}])->get();

echo $posts[0]->approved_count;

Other Aggregate Methods

// Sum, Min, Max, Avg, Exists
Post::withSum('votes', 'value')->get();      // votes_sum_value
Post::withMax('reviews', 'rating')->get();   // reviews_max_rating
Post::withAvg('reviews', 'rating')->get();   // reviews_avg_rating
Post::withExists('comments')->get();         // comments_exists

Deferred Count Loading

// Load count after the model is already fetched
$post = Post::find(1);
$post->loadCount('comments');
$post->loadSum('votes', 'value');

Inserting & Updating Related Models

Eloquent provides intuitive methods to create and save related models through a relationship.

save() and saveMany()

$comment = new Comment(['message' => 'Great post!']);

// Associates and saves the comment to the post
$post->comments()->save($comment);

// Save multiple at once
$post->comments()->saveMany([
    new Comment(['message' => 'First comment']),
    new Comment(['message' => 'Second comment']),
]);

create() and createMany()

create() accepts a plain array instead of a model instance:

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

$post->comments()->createMany([
    ['message' => 'Comment one'],
    ['message' => 'Comment two'],
]);

associate() and dissociate() for BelongsTo

// Link the comment to a post (sets post_id on the comment)
$comment->post()->associate($post);
$comment->save();

// Remove the link
$comment->post()->dissociate();
$comment->save();

Attach, Detach & Sync for Many-to-Many

// Attach a single role
$user->roles()->attach($roleId);

// Attach with pivot data
$user->roles()->attach($roleId, ['active' => true]);

// Detach specific roles
$user->roles()->detach([1, 2, 3]);

// Sync — removes old associations not in the array, adds new ones
$user->roles()->sync([1, 2, 3]);

// Sync without removing existing
$user->roles()->syncWithoutDetaching([4, 5]);

// Update an existing pivot row
$user->roles()->updateExistingPivot($roleId, ['active' => false]);

push() — Recursively Save Models

Save a model and all of its loaded relationships at once:

$post->title = 'Updated Title';
$post->comments[0]->message = 'Updated comment';
$post->push();  // saves both post and its comments

Conclusion

Eloquent Relationships are the backbone of any Laravel application that works with a database. Understanding them well will make your code cleaner, faster, and much easier to maintain.

Here's a quick recap of what we covered:

  • hasOne / belongsTo — one-to-one, like User → Profile

  • hasMany / belongsTo — one-to-many, like Post → Comments

  • belongsToMany — many-to-many with a pivot table, like User → Roles

  • whereHas / whereDoesntHave — query based on relationship existence

  • with() — eager loading to solve the N+1 query problem

  • withCount / withSum — aggregate related data without loading all records

  • save / create / attach / sync — insert and update related models cleanly

📑 On This Page