Topic 9 of 75·M15
Eloquent Relationships
Must knowone-to-one · one-to-many · many-to-many · has-many-through · polymorphic · pivot table · `hasOne` vs `belongsTo`
The one-sentence answer
Eloquent relationships are methods that define how models relate — one-to-one, one-to-many, many-to-many, has-many-through, and polymorphic — and each one is a query builder you can chain and eager-load.
The mental model
A relationship is just a method that returns a relationship object — it's the database foreign key, expressed in PHP:
class User extends Model
{
public function posts() {
return $this->hasMany(Post::class); // users.id → posts.user_id
}
}
$user->posts then feels like a property, but it's a query: select * from posts where user_id = ?. Because it's a query, you can chain — $user->posts()->where('published', true)->get() — and eager-load it (Lesson 10/11).
The relationship map
One-to-one — hasOne / belongsTo
class User extends Model
{
public function profile() { return $this->hasOne(Profile::class); }
}
class Profile extends Model
{
public function user() { return $this->belongsTo(User::class); }
}
Which side is which: the model that owns the foreign key is belongsTo (Profile holds user_id); the model on the other side is hasOne. When in doubt: "the one holding the key belongs to the other."
One-to-many — hasMany / belongsTo
class User extends Model
{
public function posts() { return $this->hasMany(Post::class); }
}
class Post extends Model
{
public function user() { return $this->belongsTo(User::class); }
}
The most common relationship: one user, many posts.
Many-to-many — belongsToMany and the pivot
class User extends Model
{
public function roles() { return $this->belongsToMany(Role::class); }
}
// users ⋈ role_user ⋈ roles — the pivot table holds user_id + role_id
The pivot table is the junction: role_user with user_id and role_id. Extra pivot columns (like expires_at) are accessed via withPivot() and read as $user->roles->first()->pivot->expires_at.
Has-many-through — the middle table
class Country extends Model
{
public function posts() { return $this->hasManyThrough(Post::class, User::class); }
}
// countries → users → posts
When you want grandchildren through a middle table — all posts in a country, through the country's users — hasManyThrough skips the intermediate fetch.
Polymorphic — morphMany / morphTo
class Image extends Model
{
public function imageable() { return $this->morphTo(); }
}
class Post extends Model
{
public function images() { return $this->morphMany(Image::class, 'imageable'); }
}
class User extends Model
{
public function images() { return $this->morphMany(Image::class, 'imageable'); }
}
// images table: imageable_id + imageable_type ("App\Models\Post" | "App\Models\User")
One table serves many parent types — the same images table attaches to posts and users. The imageable_type column records which model it belongs to. (morphToMany/morphOne are the many/many and one variants.)
The plain-JS shape (what the exercise models)
// users.id → posts.user_id, resolved per user
function userPosts(userId, posts) {
return posts.filter((p) => p.userId === userId);
}Interview questions
Q1. hasOne() vs belongsTo() — how do you know which to use?
The model that holds the foreign key uses
belongsTo; the model on the other side useshasOne(orhasMany). Profile hasuser_id, soProfile::belongsTo(User); User is the "one" side, soUser::hasOne(Profile). If you can find the foreign key, you can name the relationship.
Q2. How does many-to-many work?
Through a pivot table.
belongsToManyuses a junction table (e.g.role_user) holding both foreign keys. Eloquent readsusers → role_user → rolesautomatically, and extra pivot columns are available viawithPivot().
Q3. What is a pivot table?
The junction table for a many-to-many relationship. It holds the two foreign keys (and optionally extra data like
expires_at). It exists because a many-to-many can't be represented with a single foreign key on either table.
Q4. What is a polymorphic relationship?
One table that belongs to multiple parent types. An
imagestable withimageable_idandimageable_typeattaches to posts and users. The_typecolumn records the parent class;morphToon the image resolves whichever parent it points at. Use it for shared attachments, comments, likes.
Q5. When would you use hasManyThrough()?
When you want records through a middle model — "all posts in a country" goes country → users → posts.
hasManyThroughgives you the grandchildren directly without loading the intermediate collection, and it eager-loads in one query.
Senior follow-up: How do you avoid N+1 with these?
Every relationship is a query, and lazy-loading one in a loop is the N+1 problem (Lesson 11). Eager-load with
with('posts.images'), or load on demand withload(). The relationship definitions are where eager-loading hooks in — naming them well is part of performance.
Common mistakes
❌ Forgetting the foreign key side — belongsTo goes on the model holding the key.
❌ Many-to-many without a pivot table — belongsToMany requires the junction.
❌ Polymorphic without the _type column — the type column is what makes it polymorphic.
❌ Loading relationships in a loop — that's N+1; eager-load instead.
Quick revision notes
hasOne/hasMany+belongsTo— the FK decides which side is whichbelongsToMany— pivot table (role_user) · extra columns viawithPivot()hasManyThrough— grandchildren through a middle table- Polymorphic —
imageable_id+imageable_type= one table, many parents - Every relationship is a query — chain it, eager-load it, never lazy-load in a loop
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/5How do you decide between hasOne and belongsTo?
What's the pivot table, and what lives in it?
When is a polymorphic relationship the right shape?
What problem does hasManyThrough solve?
Why does every relationship risk N+1, and how do you prevent it?