In real applications, database tables are related to each other. A user has many posts, a post belongs to a user, a post has many tags. Eloquent Relationships let you define these connections directly in your models, so you can access related data easily without writing complex SQL joins.
Relationship Types Overview
| Relationship | Example | Foreign Key On |
|---|
| hasOne | User has one Profile | profiles table |
| belongsTo | Profile belongs to User | profiles table |
| hasMany | User has many Posts | posts table |
| belongsToMany | Post has many Tags | pivot table |
| hasOneThrough | Country has one Capital through City | intermediate table |
1. One to One (hasOne / belongsTo)
A User has one Profile. The profiles table stores a user_id foreign key.
| id | name | email |
|---|
| 1 | John | john@mail.com |
| 2 | Jane | jane@mail.com |
| id | user_id | bio |
|---|
| 1 | 1 | Developer |
| 2 | 2 | Designer |
User id: 1Profile user_id: 1
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('bio')->nullable();
$table->string('avatar')->nullable();
$table->string('phone')->nullable();
$table->timestamps();
});
class User extends Model
{
// A user HAS ONE profile
public function profile()
{
return $this->hasOne(Profile::class);
}
}
class Profile extends Model
{
// A profile BELONGS TO a user
public function user()
{
return $this->belongsTo(User::class);
}
}
// Get user's profile
$user = User::find(1);
echo $user->profile->bio;
// Get profile's user
$profile = Profile::find(1);
echo $profile->user->name;
// Create profile for user
$user->profile()->create([
'bio' => 'Hello world',
'phone' => '012345678',
]);
2. One to Many (hasMany / belongsTo)
A User has many Posts. Each post stores a user_id to know who wrote it.
| id | user_id | title |
|---|
| 1 | 1 | Laravel Basics |
| 2 | 1 | Eloquent ORM |
| 3 | 2 | CSS Grid |
User id: 1Posts user_id: 1 (2 rows)
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('body');
$table->timestamps();
});
class User extends Model
{
// A user HAS MANY posts
public function posts()
{
return $this->hasMany(Post::class);
}
}
class Post extends Model
{
// A post BELONGS TO a user
public function user()
{
return $this->belongsTo(User::class);
}
}
// Get all posts by a user
$user = User::find(1);
foreach ($user->posts as $post) {
echo $post->title;
}
// Get the author of a post
$post = Post::find(1);
echo $post->user->name;
// Count user's posts
echo $user->posts()->count();
// Add a new post to user
$user->posts()->create([
'title' => 'New Post',
'body' => 'Content here...',
]);
3. Many to Many (belongsToMany)
A Post can have many Tags, and a Tag can belong to many Posts. This requires a pivot table (a third table that connects them).
| id | title |
|---|
| 1 | Laravel Basics |
| 2 | Vue Guide |
Post id: 1pivot post_id: 1Tags id: 1, 2
// Tags table
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
// Pivot table (naming convention: alphabetical order)
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
});
Pivot table naming: Use both table names in singular form, in alphabetical order, separated by underscore. So post + tag = post_tag.
class Post extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
class Tag extends Model
{
public function posts()
{
return $this->belongsToMany(Post::class);
}
}
// Get all tags of a post
$post = Post::find(1);
foreach ($post->tags as $tag) {
echo $tag->name;
}
// Attach tags to a post (add to pivot table)
$post->tags()->attach([1, 2, 3]);
// Detach tags (remove from pivot table)
$post->tags()->detach([1]);
// Sync tags (replace all existing with these)
$post->tags()->sync([2, 3, 4]);
// Toggle tags (attach if missing, detach if exists)
$post->tags()->toggle([1, 2]);
4. Has Many Through
Access distant relations through an intermediate model. Example: A Country has many Posts through Users.
| id | country_id | name |
|---|
| 1 | 1 | John |
| 2 | 1 | Jane |
| 3 | 2 | Yuki |
| id | user_id | title |
|---|
| 1 | 1 | Laravel |
| 2 | 2 | Vue.js |
| 3 | 3 | React |
Country id: 1Users country_id: 1Posts user_id: 1, 2
countries: id, name
users: id, country_id, name
posts: id, user_id, title
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}
}
// Get all posts from a country (goes through users)
$country = Country::find(1);
foreach ($country->posts as $post) {
echo $post->title;
}
5. Eager Loading (N+1 Problem)
Without eager loading, accessing relations in a loop causes many extra database queries (the N+1 problem). Eager loading fixes this.
// This runs 1 query for posts + 1 query PER post for user
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // Extra query each time!
}
// If 100 posts = 101 queries!
// with() loads the relation in advance
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // No extra query!
}
// Always just 2 queries regardless of post count!
// Load multiple relations
$posts = Post::with(['user', 'tags'])->get();
// Nested eager loading
$posts = Post::with('user.profile')->get();
// Eager load with conditions
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true)->latest();
}])->get();
Rule of thumb: Always use with() when you know you'll access a relation inside a loop. This is one of the most important performance optimizations in Laravel.
6. Querying Relationships
// Get posts that HAVE at least one comment
$posts = Post::has('comments')->get();
// Get posts with 5 or more comments
$posts = Post::has('comments', '>=', 5)->get();
// Get posts that have comments containing "great"
$posts = Post::whereHas('comments', function ($query) {
$query->where('body', 'like', '%great%');
})->get();
// Get posts WITHOUT any comments
$posts = Post::doesntHave('comments')->get();
// Count relations
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
// Load relation after initial query
$post = Post::find(1);
$post->load('comments'); // Lazy eager loading
Summary
- βhasOne / belongsTo β one-to-one (User β Profile)
- βhasMany / belongsTo β one-to-many (User β Posts)
- βbelongsToMany β many-to-many with pivot table (Posts β Tags)
- βhasManyThrough β distant relations through intermediate model
- βEager Loading β always use
with() to avoid N+1 queries - βhas / whereHas β query based on relationship existence