What are you looking for?
What are you looking for?
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.
A one-to-one relationship is the most basic type. For example, a User has one Profile.
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
In the Profile model, define the inverse using belongsTo:
// App\Models\Profile.php
public function user()
{
return $this->belongsTo(User::class);
}
$profile = User::find(1)->profile; $user = Profile::find(1)->user;
If your foreign key name differs from the convention, pass it as the second argument:
return $this->hasOne(Profile::class, 'foreign_key', 'local_key');
$user->profile) runs a database query. Use eager loading when you need the relationship for multiple models.
A one-to-many relationship is used when one model owns multiple instances of another.
For example, a Post has many Comments.
// App\Models\Post.php
public function comments()
{
return $this->hasMany(Comment::class);
}
// Laravel assumes foreign key is 'post_id' on the comments table
// App\Models\Comment.php
public function post()
{
return $this->belongsTo(Post::class);
}
// 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;
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 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.
users → id, name roles → id, name role_user → user_id, role_id (pivot table)
// 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);
}
// 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]);
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()]);
$activeRoles = $user->roles()->wherePivot('active', 1)->get();
Eloquent provides powerful methods to query whether a relationship exists, without actually loading the related models.
// 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();
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();
// Posts that have no comments from banned authors
$posts = Post::whereDoesntHave('comments.author', function ($query) {
$query->where('banned', true);
})->get();
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.
// 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
}
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();
// 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();
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true)->orderBy('created_at', 'desc');
}])->get();
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');
// In your model — author is always eager-loaded
protected $with = ['author'];
// Override for a specific query
Post::without('author')->get();
Post::withOnly('comments')->get();
In development, you can force eager loading by throwing an error whenever lazy loading occurs:
// In AppServiceProvider::boot() Model::preventLazyLoading(! $this->app->isProduction());
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.
// 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;
// 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
// Load count after the model is already fetched
$post = Post::find(1);
$post->loadCount('comments');
$post->loadSum('votes', 'value');
Eloquent provides intuitive methods to create and save related models through a relationship.
$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() 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'],
]);
// 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 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]);
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
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