A relationship is a method on a model that tells Eloquent how two tables are linked, so you can write $user->posts instead of a join.
πͺThink of a family tree. You don't memorise everyone's ID number; you just say "my mother", "my children" or "my cousins". Relationships give your models the same kind of names for their connections.
Real data is connected
No hand-written joins
$post->comments fetches the right rows. You describe the link once, in the model.
Create through the link
$user->posts()->create([...]) fills in user_id for you, so ownership is never forgotten.
Code that reads like the domain
"A post belongs to a user and has many comments" becomes two short methods that anyone can read.
The blog's tables and how they link
post_tag box is a pivot table: it only exists to link posts and tags.| Relationship | Meaning | Blog example | Foreign key lives on⦠|
|---|---|---|---|
hasOne | owns exactly one | User β Profile | the other table (profiles.user_id) |
hasMany | owns many | User β Posts, Post β Comments | the other table (posts.user_id) |
belongsTo | is owned by one | Post β User, Post β Category | this table (posts.user_id) |
belongsToMany | many on both sides | Post β Tags | a pivot table (post_tag) |
hasManyThrough | reach across a middle table | Category β Comments (via Posts) | the middle and far tables |
Wire up the blog
The tables come from the Migrations lesson. Now we teach the models about each other.
-
hasManyandbelongsTo: the everyday pairMost links come in pairs. The side without the foreign key says
hasMany; the side with the foreign key saysbelongsTo. Eloquent guesses the key name (user_id) from the method and model names.use Illuminate\Database\Eloquent\Relations\HasMany; public function posts(): HasMany { return $this->hasMany(Post::class); }use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function comments(): HasMany { return $this->hasMany(Comment::class); } -
Property vs method: two ways to use a relationship
Access it as a property to get the results. Call it as a method to get a query builder you can keep chaining or create through.
$post->user->name; // the author (a User) $post->comments; // Collection of Comments $post->comments()->latest()->take(3)->get(); // a query you can refine // Create through the relationship: user_id is filled in for you $request->user()->posts()->create([ 'title' => 'Relationships are neat', 'slug' => 'relationships-are-neat', 'body' => '...', ]);select * from `users` where `users`.`id` = 1 limit 1; select * from `comments` where `comments`.`post_id` = 7; -
hasOne: exactly oneUse
hasOnewhen each parent has at most one child, like a user's profile. It works likehasManybut returns a single model (ornull).use Illuminate\Database\Eloquent\Relations\HasOne; public function profile(): HasOne { return $this->hasOne(Profile::class); // profiles.user_id }$user->profile?->bio; // ?-> because the profile might not exist yet -
belongsToMany: many-to-many via a pivotA post has many tags, and a tag has many posts. Neither table can hold the foreign key, so a pivot table sits in between. Laravel expects it to be named after both models, singular and alphabetical:
post_tag.Schema::create('post_tag', function (Blueprint $table) { $table->foreignId('post_id')->constrained()->cascadeOnDelete(); $table->foreignId('tag_id')->constrained()->cascadeOnDelete(); $table->primary(['post_id', 'tag_id']); // no duplicate pairs });use Illuminate\Database\Eloquent\Relations\BelongsToMany; public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } // ...and in Tag.php: return $this->belongsToMany(Post::class);Manage the links with
attach,detachandsync:$post->tags()->attach([1, 2]); // add tags 1 and 2 $post->tags()->detach(2); // remove tag 2 $post->tags()->sync([1, 3, 4]); // make it EXACTLY these tags (great for forms) -
hasManyThrough: a shortcut across tablesSometimes you want to skip a level. A category has many posts, and each post has many comments, so a category effectively "has many comments through posts".
use Illuminate\Database\Eloquent\Relations\HasManyThrough; public function comments(): HasManyThrough { return $this->hasManyThrough(Comment::class, Post::class); }$category->comments()->count(); // every comment on every post in this category
Looping over 20 posts and printing $post->user->name runs 1 query for the posts + 20 queries for the users. Fix it by loading the relationship up front with Post::with('user')->get(), which needs just 2 queries. The full story is in Eager Loading & N+1.
Avoid this, do that
Post::create([
'title' => $request->title,
'user_id' => $request->user_id,
]);
Trusting a user_id from the form lets anyone post as someone else.
$request->user()->posts()->create([
'title' => $request->title,
]);
Creating through the relationship sets user_id from the logged-in user.
public function comments()
{
return $this->hasMany(Comment::class);
}
// ...then: $post->comments()->title
Calling the method gives you a query, not results, so ->title fails.
public function comments(): HasMany { /* ... */ }
foreach ($post->comments as $comment) { /* ... */ }
Add a return type, and use the property when you want the actual records.
Post::withCount('comments')->get() adds a comments_count attribute to each post using one extra subquery, without loading any comment rows.
- Add
comments()(hasMany) toUser, anduser()+post()(belongsTo) toComment. - Add
posts()(hasMany) toCategory. - Create the
post_tagpivot migration and thetags()/posts()methods on both models. - In Tinker, attach two tags to a post, then
syncto a different set and inspect$post->tags. - Print each post's title with its author's name, first with
Post::all(), then withPost::with('user')->get(), and compare the query counts in Debugbar.
Key takeaways
- The model whose table holds the foreign key uses
belongsTo; the other side useshasOneorhasMany. belongsToManyneeds a pivot table named after both models alphabetically, likepost_tag.$post->commentsreturns results;$post->comments()returns a query you can chain.attach,detachandsyncmanage many-to-many links.hasManyThroughreaches across a middle table, e.g. category β posts β comments.- Use
with()to avoid N+1 queries when looping.