In the last two articles, we established a solid foundation with Laravel's MVC pattern and routing system. Now, it's time to tackle the heart of any data-driven application, the connections between your data.
Most applications don't just deal with a single type of data.
For example,
A blog can have posts that belong to users (writers or contributors), and each post can have multiple comments.
An e-commerce store can have orders that contain many products.
Laravel's Eloquent ORM makes managing these complex relationships intuitive and elegant.
If you've ever found yourself running multiple database queries to get related data or struggling to understand how to link your models, this guide is for you. We'll demystify Eloquent relationships and show you how to build a truly connected application with clean, readable code.
Why Use Eloquent Relationships?
Eloquent relationships are convenient, but beyond that, they are also a fundamental part of building an efficient and well-structured application.
Some of the main reasons to use them include:
Readability: They make your code more expressive. Instead of writing a complex query, you can simply write $post->user to get the user who authored a post.
Efficiency: They prevent the notorious "N+1 query problem", where you accidentally run one query to get a list of items and then N separate queries to get related data for each item (more on that below).
Maintainability: The relationship logic is defined once in your model, so you don't have to repeat yourself. If a relationship changes, you only update it in one place.
The Four Key Relationship Types in Laravel
Eloquent provides several types of relationships, but you'll usually use the four we discuss below the most. If you want to become an efficient Laravel developer, understanding them is the key to building almost any application.
1. One-to-One (hasOne / belongsTo)
This is the simplest relationship and the easiest to understand. It entails one model instance being associated with exactly one other model instance.
A classic example is a User model and a Phone model. Each user can have one phone, and each phone belongs to one user.
User Model (hasOne)
You would define the "hasOne" relationship on the model that "has" the related model. In our example, the User "has one" Phone.
// app/Models/User.php
public function phone()
{
return $this->hasOne(Phone::class);
}
Phone Model (belongsTo)
You would define the "belongsTo" relationship on the model that "belongs to" the other model. The Phone "belongs to" a User.
// app/Models/Phone.php
public function user()
{
return $this->belongsTo(User::class);
}
Database Migration
In your database migration file, you would set the foreign key (user_id) on the "phones" table:
// database/migrations/xxxx_xx_xx_create_phones_table.php
Schema::create('phones', function (Blueprint $table) {
$table->id();
$table->string('number');
$table->foreignId('user_id')->constrained(); // Creates the foreign key
$table->timestamps();
});
Alternatively, you can use "foreignIdFor" to define the relationship:
// database/migrations/xxxx_xx_xx_create_phones_table.php
use App\Models\User; // Assuming a User model exists
Schema::create('phones', function (Blueprint $table) {
$table->id();
$table->string('number');
$table->foreignIdFor(User::class)->constrained(); // Creates the foreign key
$table->timestamps();
});
Important Notes:
If you decide to use "foreignIdFor" in your migration, remember to import the "User" class at the top of the migration file or artisan will scream at you when doing the migration.
When you pass a model class to "foreignIdFor", it automatically generates the foreign key column name by appending _id to the snake-cased version of the model's table name (in this case, user).
For example, foreignIdFor(App\Models\User::class) will create a column named user_id.
You can provide a second argument to "foreignIdFor" to specify a custom column name if the default naming convention (the automatic column naming) is not desired.
One-to-One Relationship Usage Example
$user = User::find(1);
$phoneNumber = $user->phone->number; // Use this to access the phone number of the user
$phone = Phone::find(1); // Find a phone using its id
$userName = $phone->user->name; // Access the name of the user who owns the phone
2. One-to-Many (hasMany / belongsTo)
This is the most common relationship in Laravel and one that you will more than likely use the most.
With this relationship, one model instance is associated with multiple instances of another model.
For example, a User can have many Posts, but each Post belongs to only one User.
User Model (hasMany)
A user "has many" posts.
// app/Models/User.php
public function posts()
{
return $this->hasMany(Post::class);
}
Post Model (belongsTo)
A post "belongsTo" a single user
// app/Models/Post.php
public function user()
{
return $this->belongsTo(User::class);
}
Database Migration:
When creating a database migration for a one-to-many relationship, the foreign key (user_id) goes on the posts table. This makes sure that each post row in the database has a column for the associated user ID.
// database/migrations/xxxx_xx_xx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->foreignId('user_id')->constrained(); // Creates the foreign key
$table->timestamps();
});
One-to-Many Relationship Usage Example
$user = User::find(1);
$posts = $user->posts; // Get all posts by this user (returns a Collection that you can loop over or transform in other ways)
$post = Post::find(1);
$authorName = $post->user->name; // Get the name of the post's author
3. Many-to-Many (belongsToMany)
The many-to-many relationship is very similar to the one-to-one relationship, but with a significant difference (you can guess this from its name!)
This relationship connects multiple instances of one model to multiple instances of another.
For example, a User can have many Roles, and a Role can be assigned to many Users.
Building many-to-many relationships require a pivot table (e.g., role_user) to store the intermediate records (more on this below).
User Model (belongsToMany)
A user "belongs to many" roles.
public function roles()
{
return $this->belongsToMany(Role::class);
}
Role Model (belongsToMany)
A role "belongs to many" users.
// app/Models/Role.php
public function users()
{
return $this->belongsToMany(User::class);
}
Database Migration
A separate pivot table is required when creating a many-to-many association.
// database/migrations/xxxx_xx_xx_create_role_user_table.php
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->primary(['role_id', 'user_id']); // Compound primary key
});
Many-to-Many Relationship Usage Example
//Get a specific user using their id
$user = User::find(1);
// Get the name of each role for the user (use a loop because there are potentially multiple roles assocuated with a specific user)
foreach ($user->roles as $role) {
echo $role->name;
}
// Attaching and detaching relationships on the pivot table
$user->roles()->attach($roleId); // Add a role
$user->roles()->detach($roleId); // Remove a role
$user->roles()->sync([1, 2, 3]); // Sync roles to a given array of IDs
A Closer Look at Pivot Tables
When dealing with a many-to-many relationship, a dedicated "pivot table" (also known as an intermediate or join table) is necessary. This table sits between the two models and stores the foreign keys for both.
For example, in our example above, a User can have many Roles, and a Role can be assigned to many Users.
A pivot table, conventionally named role_user (the names of the two models in alphabetical order separated by an underscore), would store a record for each association.
This pivot table contains two foreign keys: user_id and role_id as seen in the migration above.
Understanding this, the question becomes: Why not just add role_id to the users table?
Because a user can have multiple roles, a single role_id column wouldn't be enough.
The pivot table allows you to have multiple rows for a single user, each corresponding to a different role. This is the key to managing many-to-many relationships in a relational database.
Here’s a visual representation of how a pivot table connects two models:
The role_user pivot table records that Alice (user_id 1) has two roles (Admin and Editor), while Bob (user_id 2) has one role (Editor).
Eloquent automatically handles the interactions with this pivot table behind the scenes. When you call $user->roles, Eloquent knows to look for the role_user table, find all entries with the matching user_id, and then fetch the corresponding Role models.
You can also access the pivot table columns directly via the pivot property on the related model:
// Assuming the pivot table has an additional column, e.g., 'expires_at'
foreach ($user->roles as $role) {
echo $role->name . " expires at " . $role->pivot->expires_at;
}
This simple, elegant design is what makes Laravel's "belongsToMany" relationship so powerful and easy to use.
The N+1 Query Problem and Eager Loading
The N + 1 problem is one of the most important concepts to grasp. Imagine you have a list of 100 posts, and you want to display the author's name for each one.
The N+1 Problem (Lazy Loading):
// Fetches 100 posts
$posts = Post::all();
// A separate query runs for EACH post to get the user
foreach ($posts as $post) {
echo $post->user->name;
}
// Total queries: 1 (for posts) + 100 (for users) = 101 queries
Now, consider an ecommerce store that needs to fetch hundreds of products, each with multiple pieces of data (SKU, price, shipping cost, multiple colours, multiple sizes, etc.)
Fetching data like this is very inefficient and slow, and will have significant implications, especially if you pay for compute, bandwidth, and how much data you fetch from the database.
The Solution (Eager Loading):
Eloquent provides the 'with()' method to "eager load" related data. This fetches all the posts and all their related users in just two queries.
// app/Http/Controllers/PostController.php
public function index()
{
// Fetches 100 posts AND all their related users in just 2 queries
$posts = Post::with('user')->get();
return view('posts.index', compact('posts'));
}
Important Notes:
You can provide "with()" with an array for eager loading multiple relationships at once
Here is a code example (consider a Post that has author and tag relationships):
// Eager load both the 'author' and 'tags' relationships
$posts = Post::with(['author', 'tags'])->get();
You can also eager load nested relationships using dot notation. This is known as Nesting Eager Loading. For example:
$posts = Post::with(['author', 'comments.user'])->get();
In the example above (nested eager loading), Laravel will:
Fetch all posts from the database.
Fetch all authors for those posts (in a single query).
Fetch all comments for those posts (in a single query).
Fetch all users for those comments (in a single query).
Eager Loading Usage In the View
@foreach ($posts as $post)
<div>
<h2>{{ $post->title }}</h2>
<p>By: {{ $post->user->name }}</p> {{-- The user is already loaded! --}}
</div>
@endforeach
4. Polymorphic Relationships (morphTo / morphMany)
The fourth type of relationship you need to know about is the polymorphic relationship.
Think about this:
What if a model, like a Comment, could belong to more than one type of model? For example, a comment could be left on a Post or on a Video.
This is a job for polymorphic relationships.
Let's look at some code.
Comment Model (morphTo)
// app/Models/Comment.php
public function commentable()
{
return $this->morphTo();
}
Post & Video Models (morphMany)
Both the Post and Video models define the morphMany relationship.
// app/Models/Post.php
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
// app/Models/Video.php
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
Polymorphic Relationships Relationship Usage Example
$post = Post::find(1);
$postComments = $post->comments; // Get all comments for the post
$video = Video::find(1);
$videoComments = $video->comments; // Get all comments for the video
$comment = Comment::find(1);
$commentParent = $comment->commentable; // Can be a Post or a Video instance!
Conclusion: Build Connected, Efficient Applications
Eloquent relationships are a gateway to building powerful, clean, and performant applications in Laravel.
By defining your data connections clearly in your models, using eager loading to prevent performance issues, and understanding advanced concepts like polymorphic relationships, you'll write less code, prevent common bugs, and make your application a joy to work on.
Take some time to practice these concepts. Try building a simple blog system or an e-commerce platform and use relationships to connect your data. You'll quickly see why Eloquent is one of Laravel's most beloved features.
What's the most complex relationship you've ever built with Eloquent? Share your experiences and tips below!
Stay tuned for the next article, where we'll dive into Laravel validations and form requests.
We briefly touched on this in our MVC article, but we will dive in deeper next week. Keep following along on our journey to becoming better Laravel developers!
Top comments (0)