Laravel Eloquent Relationships Explained

A tour through Laravel Eloquent's relationship types — from simple one-to-many to polymorphic relationships — and how to avoid the N+1 trap.

Why Relationships Are Eloquent’s Superpower

Eloquent’s relationship methods turn what would otherwise be manual joins and foreign key lookups into expressive, chainable PHP. Understanding each relationship type — and when to use it — is one of the highest-value skills for working effectively in a Laravel codebase.

One-to-One and One-to-Many

hasOne and belongsTo describe a one-to-one relationship from either side, like a User and a Profile. hasMany and its inverse belongsTo describe one-to-many, like a Post having many Comments. Getting the direction right — which model “has” the other — is mostly about where the foreign key lives.

Many-to-Many

belongsToMany handles the classic pivot-table pattern, like Users and Roles. Don’t overlook pivot table extras — withPivot() and withTimestamps() let you attach extra metadata to the relationship itself, like when a role was assigned and by whom.

Has-Many-Through and Polymorphic Relationships

hasManyThrough is underused but powerful for reaching across an intermediate table — for example, getting all Comments on a Country's Posts through an intermediate Users table. Polymorphic relationships (morphMany, morphTo) let a single table like Comments belong to multiple other model types (Post or Video) without separate pivot tables for each.

Avoiding the N+1 Trap

The most common Eloquent performance mistake is looping over a collection and accessing a relationship inside the loop, triggering a fresh query per iteration. Eager loading with with() fixes this by fetching related records in a single additional query. Laravel’s Model::preventLazyLoading() in non-production environments is a great habit — it throws an exception the moment lazy loading would occur, making N+1 problems impossible to miss during development.

Practical Tips

  • Use withCount() when you only need a count of related records, not the records themselves.
  • Constrain eager loads with closures — with(['comments' => fn($q) => $q->latest()]) — to avoid pulling unnecessary data.
  • Define relationship return types explicitly for better IDE support and static analysis.

Mastering relationships pays off far beyond just query syntax — it shapes how you design your database schema and how naturally your domain logic reads in code.