Laravel Tutorial · Database and Eloquent · Lesson 16 of 27

Eloquent Relationships

Connect models with hasMany, belongsTo, belongsToMany and hasOne, query through relationships, and avoid the N+1 problem with eager loading.

Create a free account to save this page, take notes and track your progress. Sign in

Defining relationships

Relationships are methods on models:

Relationship Example Table setup
hasMany / belongsTo A user has many posts; a post belongs to a user posts.user_id
hasOne A user has one profile profiles.user_id
belongsToMany Posts have many tags and tags many posts a pivot table post_tag
hasManyThrough, polymorphic (morphMany) Comments on posts and videos commentable_type, commentable_id

Use them as properties ($user->posts, loads the related models) or as methods to keep querying ($user->posts()->where('published', true)->count()). Create related records through the relationship: $user->posts()->create([...]).

The N+1 problem

Looping over 50 posts and printing $post->user->name runs 1 query for posts + 50 for users. Eager loading with with('user') loads them in 2 queries. withCount('posts') adds counts without loading rows. In development, Model::preventLazyLoading() turns accidental N+1s into errors.

hasMany, belongsTo, belongsToMany and eager loading
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

Schema::create('users', function (Blueprint $t) {
    $t->id();
    $t->string('name');
    $t->string('email')->unique();
    $t->timestamps();
});
Schema::create('posts', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->constrained();
    $t->string('title');
    $t->boolean('published')->default(false);
    $t->timestamps();
});

Schema::create('tags', function (Blueprint $t) { $t->id(); $t->string('name'); });
Schema::create('post_tag', function (Blueprint $t) {
    $t->foreignId('post_id')->constrained();
    $t->foreignId('tag_id')->constrained();
});

class Author extends Model
{
    protected $table = 'users';
    protected $guarded = [];
    public function posts() { return $this->hasMany(Article::class, 'user_id'); }
}

class Article extends Model
{
    protected $table = 'posts';
    protected $guarded = [];
    public function author() { return $this->belongsTo(Author::class, 'user_id'); }
    public function tags() { return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id'); }
}

class Tag extends Model
{
    protected $guarded = [];
    public $timestamps = false;
}

$ana = Author::create(['name' => 'Ana', 'email' => '[email protected]']);
$ben = Author::create(['name' => 'Ben', 'email' => '[email protected]']);
$php = Tag::create(['name' => 'php']);
$tips = Tag::create(['name' => 'tips']);

$first = $ana->posts()->create(['title' => 'Eloquent tips', 'published' => true]);
$first->tags()->attach([$php->id, $tips->id]);
$ana->posts()->create(['title' => 'Draft ideas']);
$ben->posts()->create(['title' => 'Blade basics', 'published' => true])->tags()->attach($php);

echo 'Ana has ', $ana->posts()->count(), ' posts, ', $ana->posts()->where('published', true)->count(), ' published', "\n";
echo "'{$first->title}' is by {$first->author->name}, tagged ", $first->tags->pluck('name')->implode(', '), "\n";

DB::enableQueryLog();
foreach (Article::all() as $post) { $post->author->name; }          // lazy: 1 + N queries
$lazy = count(DB::getQueryLog());
DB::flushQueryLog();
foreach (Article::with('author')->get() as $post) { $post->author->name; }   // eager: 2 queries
$eager = count(DB::getQueryLog());
echo "author lookups for 3 posts: lazy $lazy queries, eager $eager queries\n";

foreach (Author::withCount('posts')->orderBy('name')->get() as $author) {
    echo "$author->name: $author->posts_count post(s)\n";
}
echo 'posts tagged php: ', Article::whereHas('tags', fn ($q) => $q->where('name', 'php'))->pluck('title')->implode(', '), "\n";

Output

Ana has 2 posts, 1 published
'Eloquent tips' is by Ana, tagged php, tips
author lookups for 3 posts: lazy 4 queries, eager 2 queries
Ana: 2 post(s)
Ben: 1 post(s)
posts tagged php: Eloquent tips, Blade basics

Run this on your computer — see “Get Started” for setup.

Test yourself

How do you avoid the N+1 query problem when listing posts with their authors?

  1. Add an index
  2. Eager load with Post::with('author')
  3. Use a raw query
  4. Cache each author
Show answer

Answer: Eager load with Post::with('author')

Was this page helpful?