Skip to content
Muhammet Şafak
tr
Framework & Library 4 min read

Eloquent Relationships: hasMany, belongsTo, and Eager Loading

How to define hasMany and belongsTo relationships in Laravel Eloquent, and how to solve the N+1 query problem with eager loading.


Eloquent ORM makes database work feel effortless — until you start pulling related data and walk straight into a trap: the N+1 query problem. In this post I’ll cover the two relationship types I use most — hasMany and belongsTo — and then show how eager loading gets you out of that trap.

hasMany and belongsTo

Imagine a blog application with User and Post models. A user can have many posts — that’s a classic one-to-many relationship.

The User model defines hasMany for posts:

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

The Post model points back to its owner with belongsTo:

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

By convention, Eloquent expects a user_id column on the posts table. If your column has a different name, pass it explicitly: hasMany(Post::class, 'author_id').

In practice:

$kullanici = User::find(1);
$gonderiler = $kullanici->posts; // All posts for this user

$gonderi = Post::find(5);
$yazar = $gonderi->user; // The post's author

When you access a relationship method as a property, Eloquent runs the query and returns the result. This is called a dynamic property.

The N+1 query problem

If relationships are this simple, why should you be careful?

Take a look at this code:

$kullanicilar = User::all();

foreach ($kullanicilar as $kullanici) {
    echo $kullanici->name . ': ' . $kullanici->posts->count() . ' gönderi';
}

It works. But what’s actually happening behind the scenes?

  1. User::all() — 1 query (fetch all users)
  2. Inside the loop, $kullanici->posts — 1 query per user

With 10 users, that’s 11 queries. With 100 users, 101 queries. The query count scales linearly with the number of records. That’s the N+1 query problem.

It took me a while to notice this. During development everything felt fast because I was working with small datasets. Then on a client project the user count climbed into the hundreds and page load times started dragging noticeably. When I checked the query log, I found 341 queries firing for 340 users. The bug wasn’t in the logic — it was in the habit.

Fixing it with eager loading

Eager loading loads related data together with the main query. You do it with the with() method:

$kullanicilar = User::with('posts')->get();

foreach ($kullanicilar as $kullanici) {
    echo $kullanici->name . ': ' . $kullanici->posts->count() . ' gönderi';
}

Now only 2 queries run:

  1. SELECT * FROM users
  2. SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)

Even with 100 users, it’s still just 2 queries. You really feel that difference once the data volume grows.

Loading multiple relationships

with() can load several relationships at once:

$gonderiler = Post::with(['user', 'comments'])->get();

For nested relationships, use dot notation:

$kullanicilar = User::with('posts.comments')->get();

This fetches users, their posts, and each post’s comments in one go — three queries total instead of potentially thousands.

Constrained eager loading

You can also apply constraints to the eagerly loaded relationship:

$kullanicilar = User::with([
    'posts' => function ($sorgu) {
        $sorgu->where('yayinlandi', true)->orderBy('created_at', 'desc');
    }
])->get();

Only published posts, ordered by date, get loaded.

One subtlety worth noting: a constrained eager load excludes posts that fail the condition — it does not exclude users whose posts all fail. Even if a user has no matching posts, that user object is still returned; you won’t get null. This can be misleading when you’re writing conditional rendering logic in the view layer.

Lazy eager loading

Sometimes you’ve already retrieved a model and only later realize you need the relationship. The load() method handles that:

$kullanici = User::find(1);

// Load the relationship after the fact
$kullanici->load('posts');

For multiple relationships on a collection:

$kullanicilar = User::all();
$kullanicilar->load('posts', 'profile');

Which approach to use, and when?

Use with() when you know upfront that you’ll need the relationship. Use load() when the need is conditional and only becomes clear later.

The quickest way to catch N+1 problems is to enable Laravel’s query log:

DB::enableQueryLog();
// ... your code ...
dd(DB::getQueryLog());

When you spot the same query repeating inside a loop, it’s time to add with().

Skipping eager loading and only noticing the slowdown after the fact has become a personal warning sign for me. Now I make a habit of asking myself whether with() is needed every time I reach for a relationship.

Tags: #Laravel
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind