Laravel Eloquent Relationships: hasMany, belongsTo, and More

January 4, 2024 · 8 min read

2026 UPDATE - LARAVEL 13
We’re excited to announce that we have updated all of our blog post examples to reflect the new Laravel 13 version! Our previous examples were based on Laravel 10, but with the release of Laravel 13, we wanted to ensure that our readers have access to the most up-to-date information and examples.
Laravel Eloquent ORM Relationships
Embarking on a Laravel adventure means you’re going to deal with data, and not just any data, but data that are connected in all sorts of complex ways. That’s where model relationships come in, linking everything together as the glue of your application’s data.
Defining Relationships in Laravel
Laravel supports a variety of relationship types, allowing you to express the connections between your data models clearly. Here’s a brief overview of each type:
One To One: This is a straightforward relationship where one record from a table corresponds to one record in another table. For instance, each user might have one unique profile.
One To Many: A common relationship where one record can be associated with multiple records in another table. For example, a user can have many posts.
Many To One: The inverse of one-to-many, where many records in one table relate to a single record in another table.
Many To Many: This relationship allows multiple records in one table to be associated with multiple records in another table. A user might subscribe to multiple magazines, and each magazine might have multiple subscribers. We should also create a pivot table to make it work.
Polymorphic Relationships: These allow a model to belong to more than one other model on a single association. An image might be associated with either a post or a user profile.
Many To Many Polymorphic Relationships: An extension of polymorphic relationships where a model can have multiple relations to multiple models.
Has One Through and Has Many Through: These relationships allow you to access distant relations via an intermediate relation. For example, a country might have many posts through a user model.

Let’s Build an App with Laravel
Creating a blog with Laravel is a good example of beginning your journey into the world of web development. As your blog grows, so does the complexity of the relationships between your data. Understanding how to define and use these relationships in Laravel is key to building a feature-rich and efficient blogging platform.
Note: Do not worry about coding for now, we will start coding in the next lesson. Focus on understanding these concepts as they will help you in your Laravel journey.
The Blueprint of Our App
Let’s define the main features of our blog:
Users: The authors who write blog posts.
Profiles: The profiles of the users.
Posts: The articles or blog entries published by users.
Tags: Keywords or topics that categorize posts.
Images: Photos or graphics associated with posts or users.
Now, let’s explore how these entities relate to each other in a typical blog.
Creating the Relationships
One To One: User and Profile
Each user on our blog might have a detailed profile. This is a one-to-one relationship.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Authenticatable
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}For the one-to-one relationship between a User and a Profile, the inverse would allow us to find the user from the profile.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Profile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}One To Many: User and Posts
A user can write many blog posts, but each post is written by one user. This is a one-to-many relationship.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Authenticatable
{
public function profile()
{
return $this->hasOne(Profile::class);
}
public function posts()
{
return $this->hasMany(Post::class);
}
}The inverse of the one-to-many relationship between User and Posts would allow us to find the author of a specific post.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}Many To Many: Posts and Tags
Table Structure
To define this relationship, three database tables are needed: posts, tags, and post_tag. The post_tag table is derived from the alphabetical order of the related model names and contains post_id and tag_id columns. This table is used as an intermediate table linking the posts and tags.
Remember, since a tag can belong to many users, we cannot simply place a tag_id column on the posts table. This would mean that a tag could only belong to a single post. In order to provide support for tags being assigned to multiple posts, the post_tag table is needed. We can summarize the relationship’s table structure like so:
posts
id - integer
name - string
tags
id - integer
name - string
post_tag
post_id - integer
tag_id - integer
A post can be tagged with multiple keywords, and each tag can be applied to multiple posts. This is a many-to-many relationship. We need to add a new pivot table post_tag (mind the alphabetical order here) so that post_id and tag_id are both saved there. This will allow us to add as many items as needed.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}For the many-to-many relationship between Posts and Tags, the inverse relationship would allow us to find all posts associated with a particular tag.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
public function posts()
{
return $this->belongsToMany(Post::class);
}
}Polymorphic: Images
An image might be associated with a user profile or a blog post. This is a polymorphic relationship.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Image extends Model
{
protected $fillable = ['path'];
public function imageable()
{
return $this->morphTo();
}
}The inverse relationship would have to be implemented on the Post and Profile model as well.
Let’s start with the Post:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}We do the same for the Profile:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Profile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}Using Relationships in Your App
To cover the different relationships in our blog application, we’ll create several controllers, each with a clear purpose. This approach follows the Single Responsibility Principle and makes our application easier to maintain and understand.
UserController
The UserController handles operations related to users, such as displaying a user’s profile and posts.
namespace App\Http\Controllers;
use App\Models\User;
class UserController extends Controller
{
// Display all users
public function index()
{
$users = User::all();
return view('users.index', ['users' => $users]);
}
// Display a user and their profile
public function show($id)
{
$user = User::with('profile')->findOrFail($id);
return view('users.show', ['user' => $user]);
}
}PostController
The PostController is concerned with blog post operations, including listing, creating, editing, and deleting posts.
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Http\Request;
class PostController extends Controller
{
// Display all blog posts
public function index()
{
$posts = Post::with('user', 'tags')->latest()->paginate();
return view('posts.index', ['posts' => $posts]);
}
// Show a form for creating a new blog post
public function create()
{
return view('posts.create');
}
// Store a newly created blog post
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
// other validation rules...
]);
$tags = $request->validate([
'tags' => 'required',
]);
// the new post belongs to the authenticated user
$post = auth()->user()->posts()->create($validatedData);
// we add tags to the post by using the sync() method
$post->tags()->sync($tags);
return redirect()->route('posts.show', $post);
}
// Display a single blog post
public function show($id)
{
$post = Post::with('user', 'tags', 'images')
->findOrFail($id);
return view('posts.show', ['post' => $post]);
}
// Show a form for editing the specified blog post
public function edit($id)
{
$post = Post::findOrFail($id);
$tags = Tag::all();
return view('posts.edit', ['post' => $post, 'tags' => $tags]);
}
// Update the specified blog post
public function update(Request $request, Post $post)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
// other validation rules...
]);
$tags = $request->validate([
'tags' => 'required',
]);
$post->update($validatedData);
// we modify the tags of the post by using the sync() method
$post->tags()->sync($tags);
return redirect()->route('posts.show', $post);
}
// Remove the specified blog post
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index');
}
}TagController
The TagController handles tag-related actions, such as viewing all posts associated with a tag.
namespace App\Http\Controllers;
use App\Models\Tag;
class TagController extends Controller
{
// Display all posts for a specific tag
public function show($id)
{
$tag = Tag::with('posts')->findOrFail($id);
return view('tags.show', ['tag' => $tag, 'posts' => $tag->posts]);
}
}Uploading Images (Polymorphic relationship)
Here’s an example of how you might handle image uploads in a PostController. This is a simplified example, and in a real-world scenario, you would likely have more complex validation, storage, and error handling.
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Image;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class PostController extends Controller
{
// ... (other methods)
// Store a newly created blog post with an image
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'image' => 'nullable|image|max:2048', // 2MB Max
]);
// we already showed how you can create a user post
$post = auth()->user()->posts()->create($validatedData);
// now let's add our image to the post we wrote
if ($request->hasFile('image')) {
$path = $request->file('image')->store('images', 'public');
$post->images()->create(['path' => $path]);
}
return redirect()->route('posts.show', $post);
}
// ... (other methods)
}In this store method, we’re validating the incoming request, including an image file. If an image is included, we use Laravel’s store method, which saves the file in the images directory within the public disk (public storage). We then create a new Image record associated with the post using the create method on the images relationship.
The 'public' parameter in the store method specifies the disk defined in config/filesystems.php, where you can set up different filesystems. The 'images' is a directory within that disk.
Remember to exectue command in terminal:
php artisan storage:linkalso includeenctype="multipart/form-data"in your<form>tag to allow file uploads.

The N+1 Problem and Eager Loading
The N+1 problem is the most common performance mistake developers make when using Eloquent relationships. It happens when you load a collection of models and then access a relationship on each one inside a loop - each access fires a separate database query.
Here is the problem:
// This fires 1 query to get all posts...
$posts = Post::all();
foreach ($posts as $post) {
// ...then 1 query PER POST to get the author. With 100 posts = 101 queries!
echo $post->user->name;
}The fix is eager loading with with(), which loads the relationship data upfront in a single additional query:
// 2 queries total - no matter how many posts there are
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // No extra queries!
}
// Eager load multiple relationships at once
$posts = Post::with(['user', 'tags', 'images'])->get();
// Nested eager loading: load posts AND the tags on each post
$users = User::with('posts.tags')->get();
// Conditional eager loading: only load published posts
$users = User::with(['posts' => function ($query) {
$query->where('published', true)->orderBy('created_at', 'desc');
}])->get();If you have already retrieved a model and want to load its relationships afterwards, use load() (lazy eager loading):
$post = Post::find(1);
// Load relationships after the fact
$post->load(['user', 'tags']);
// Conditionally load only when needed
if ($someCondition) {
$post->load('comments');
}Querying Relationships
Laravel provides expressive methods to filter queries based on the existence or attributes of related models - without loading the related records into memory.
// Only return users who have at least one post
$users = User::has('posts')->get();
// Only return users who have more than 3 posts
$users = User::has('posts', '>=', 3)->get();
// Only return users with no posts
$users = User::doesntHave('posts')->get();
// Filter by a relationship attribute (without loading related records)
$users = User::whereHas('posts', function ($query) {
$query->where('title', 'like', '%laravel%');
})->get();
// Combine with other conditions
$users = User::whereHas('posts', function ($query) {
$query->where('published', true);
})->where('active', true)->get();
// Add a count of related records as a virtual column
$posts = Post::withCount('tags')->get();
// Access it as $post->tags_count
// Add multiple counts at once
$posts = Post::withCount(['tags', 'comments', 'images'])->get();
// Count with a condition
$posts = Post::withCount(['comments' => function ($query) {
$query->where('approved', true);
}])->get();Customizing Many-to-Many Pivot Tables
The pivot table in a many-to-many relationship often needs to store more than just the two foreign keys. For example, you might want to record when a tag was applied to a post, or whether a post is featured in a given category.
// In your migration, add extra columns to the pivot table:
Schema::create('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->boolean('is_featured')->default(false);
$table->timestamps(); // adds created_at and updated_at
});Then tell Eloquent to retrieve those extra columns by chaining withPivot() on the relationship definition:
// In Post.php model
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withPivot('is_featured')
->withTimestamps();
}Now you can access the pivot data on each related model via the pivot property, and filter by it:
$post = Post::with('tags')->find(1);
foreach ($post->tags as $tag) {
// Access the pivot column on each related model
echo $tag->pivot->is_featured; // true or false
echo $tag->pivot->created_at; // when the tag was added
}
// Attach a tag with pivot data
$post->tags()->attach($tagId, ['is_featured' => true]);
// Filter by pivot column
$featuredTags = $post->tags()->wherePivot('is_featured', true)->get();
// Update a pivot value
$post->tags()->updateExistingPivot($tagId, ['is_featured' => false]);Verifying Relationships in Tinker
Before wiring up a controller or building a view, you can verify that your relationships work correctly using Tinker - Laravel’s built-in REPL. This lets you test relationship queries interactively without writing a test or building UI:
php artisan tinker// Verify a user has posts
$user = User::find(1);
$user->posts; // returns a Collection
$user->posts()->count(); // returns an integer
// Verify eager loading works
$users = User::with('posts')->take(3)->get();
$users->first()->posts; // no extra query fired
// Verify a pivot column is accessible
$post = Post::with('tags')->find(1);
$post->tags->first()->pivot->is_featured;
// Test whereHas
User::whereHas('posts', fn($q) => $q->where('published', true))->count();
// Check withCount
Post::withCount('tags')->first()->tags_count;Conclusion
Model relationships in Laravel provide a powerful way to represent and manage the interconnected data of a blog. By defining clear relationships between users, posts, tags, and images, you can write code that’s both efficient and expressive. Laravel’s Eloquent ORM makes it easy to handle these relationships, allowing you to focus on creating the perfect blogging experience.
Upcoming Articles in the Series
This article is part of our series Laravel for Beginners: A Step-by-Step Guide to Learn the Concepts.
Bring Your Ideas to Life 🚀
If you need help with a Laravel project let’s get in touch.
Lucky Media is proud to be recognized as a leading Laravel Development Agency
Technologies

Stay up-to-date
Be updated with all news, products and tips we share!
On this page
- 2026 UPDATE - LARAVEL 13
- Laravel Eloquent ORM Relationships
- Defining Relationships in Laravel
- Let’s Build an App with Laravel
- The N+1 Problem and Eager Loading
- Querying Relationships
- Customizing Many-to-Many Pivot Tables
- Verifying Relationships in Tinker
- Conclusion
- Upcoming Articles in the Series
- Bring Your Ideas to Life 🚀

