Back to Read More
LaravelPHP

Laravel Eloquent Relationships

Dec 10, 2025

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

RelationshipExampleForeign Key On
hasOneUser has one Profileprofiles table
belongsToProfile belongs to Userprofiles table
hasManyUser has many Postsposts table
belongsToManyPost has many Tagspivot table
hasOneThroughCountry has one Capital through Cityintermediate table

1. One to One (hasOne / belongsTo)

A User has one Profile. The profiles table stores a user_id foreign key.

users
idnameemail
1Johnjohn@mail.com
2Janejane@mail.com
profiles
iduser_idbio
11Developer
22Designer
User id: 1Profile user_id: 1
Migration: profiles table
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();
});
app/Models/User.php
class User extends Model
{
    // A user HAS ONE profile
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }
}
app/Models/Profile.php
class Profile extends Model
{
    // A profile BELONGS TO a user
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
Usage
// 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.

users
idname
1John
2Jane
posts
iduser_idtitle
11Laravel Basics
21Eloquent ORM
32CSS Grid
User id: 1Posts user_id: 1 (2 rows)
Migration: posts table
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('title');
    $table->text('body');
    $table->timestamps();
});
app/Models/User.php
class User extends Model
{
    // A user HAS MANY posts
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
app/Models/Post.php
class Post extends Model
{
    // A post BELONGS TO a user
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
Usage
// 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).

posts
idtitle
1Laravel Basics
2Vue Guide
post_tag (pivot)
post_idtag_id
11
12
21
tags
idname
1PHP
2Laravel
Post id: 1pivot post_id: 1Tags id: 1, 2
Migration: tags table + pivot table
// 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.
app/Models/Post.php
class Post extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}
app/Models/Tag.php
class Tag extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}
Usage
// 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.

countries
idname
1Cambodia
2Japan
users (intermediate)
idcountry_idname
11John
21Jane
32Yuki
posts
iduser_idtitle
11Laravel
22Vue.js
33React
Country id: 1Users country_id: 1Posts user_id: 1, 2
Tables
countries:  id, name
users:      id, country_id, name
posts:      id, user_id, title
app/Models/Country.php
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}
Usage
// 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.

Bad: N+1 Problem (1 + N queries)
// 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!
Good: Eager Loading (2 queries total)
// 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

Useful Relationship Queries
// 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

Β© 2026 Koeuk KOS. All rights reserved.

Built with Nuxt.js, Vue.js & Tailwind CSS