# 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.

- Published: 2015-09-06
- Updated: 2026-09-07
- Category: Framework & Library
- Tags: Laravel
- Reading time: 4 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/eloquent-relationships-hasmany-belongsto-and-eager-loading/
- Language: en-US
- Author: Muhammet Şafak

---
[Eloquent ORM makes database work feel effortless](/en/blog/simplifying-database-operations-with-eloquent-orm/) — 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:

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

The `Post` model points back to its owner with `belongsTo`:

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

By convention — [as the documentation describes](https://laravel.com/docs/5.1/eloquent-relationships#one-to-many) — 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:

```php
$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:

```php
$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

The documentation defines both the problem and the fix in one place: [when you access a relationship as a property the data is "lazy loaded"](https://laravel.com/docs/5.1/eloquent-relationships#eager-loading) — it isn't fetched until you first touch the property — and eager loading "alleviates the N + 1 query problem". **Eager loading** loads related data together with the main query. You do it with the `with()` method:

```php
$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:

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

For nested relationships, use dot notation:

```php
$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:

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

Only published posts, ordered by date, get loaded — [the documentation calls this "constraining eager loads"](https://laravel.com/docs/5.1/eloquent-relationships#constraining-eager-loads).

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 — [what the documentation calls "lazy eager loading"](https://laravel.com/docs/5.1/eloquent-relationships#lazy-eager-loading) — handles that:

```php
$kullanici = User::find(1);

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

For multiple relationships on a collection:

```php
$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:

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

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

A note from today: you no longer have to catch this by hand. You can [tell Laravel to forbid lazy loading of relationships altogether](https://laravel.com/docs/12.x/eloquent-relationships#preventing-lazy-loading) — calling `Model::preventLazyLoading()` in the `boot` method of your `AppServiceProvider` makes accessing a relationship that wasn't eager loaded throw an exception. Leaving that on in development shows you the mistake described in this post instead of waiting for you to notice it.

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.

---

### Sources

- [Laravel 5.1: Eloquent Relationships — One To Many](https://laravel.com/docs/5.1/eloquent-relationships#one-to-many)
- [Laravel 5.1: Eloquent Relationships — Eager Loading](https://laravel.com/docs/5.1/eloquent-relationships#eager-loading)
- [Laravel 5.1: Eloquent Relationships — Constraining Eager Loads](https://laravel.com/docs/5.1/eloquent-relationships#constraining-eager-loads)
- [Laravel 5.1: Eloquent Relationships — Lazy Eager Loading](https://laravel.com/docs/5.1/eloquent-relationships#lazy-eager-loading)
- [Laravel 12.x: Eloquent Relationships — Preventing Lazy Loading](https://laravel.com/docs/12.x/eloquent-relationships#preventing-lazy-loading)
