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.
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.
<?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?
Add an indexEager load with Post::with('author')Use a raw queryCache each author
Show answer
Answer: Eager load with Post::with('author')