Eloquent Relationships

Real applications are never just one isolated table — articles belong to authors, orders have many line items, users have one profile, and so on. Eloquent lets you describe these connections directly on your model classes, as ordinary PHP methods, and then work with related data almost as naturally as accessing a property.

The Main Relationship Types

RelationshipMeaningExample
hasOneThis model owns exactly one of another model.A User has one Profile.
hasManyThis model owns many of another model.An Author has many Article records.
belongsToThe inverse side — this model is owned by one of another model.An Article belongs to one Author.
belongsToManyA many-to-many relationship, using a pivot table in between.An Article has many Tag records, and a Tag is used on many articles.

A Full Worked Example: Author and Article

An author writes many articles, and each article belongs to exactly one author — the classic one-to-many relationship. Both sides are defined as methods, one on each model:

<?php
// app/Models/Author.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    public function articles()
    {
        return $this->hasMany(Article::class);
    }
}
<?php
// app/Models/Article.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}

Eloquent figures out the foreign key by convention too — it expects an author_id column on the articles table, matching the singular, snake_case name of the Author model plus _id. Like every other convention covered so far, this can be overridden by passing extra arguments to hasMany()/belongsTo(), but the default almost always fits naturally.

Accessing a Relationship Like a Property

Even though articles() and author() are defined as methods, once they exist you almost never call them with parentheses directly — instead you access them as if they were plain properties:

<?php
$author = Author::find(1);

foreach ($author->articles as $article) {
    echo $article->title;
}

This isn't a coincidence or special syntax — it's ordinary PHP "magic" at work. Eloquent's base Model class implements the __get() magic method, and when you access $author->articles, PHP can't find a real articles property, so it falls through to __get(), which notices there's an articles() method, calls it for you, executes the query, and caches the result on the model. The second time you access $author->articles in the same request, no extra query runs — it reuses the cached result.

Eager Loading: Avoiding the N+1 Query Problem

That caching only helps within a single model instance. The real danger shows up when you loop over a collection of models and access a relationship inside the loop — this is one of the most common performance mistakes in any ORM, well known enough to have its own name: the N+1 query problem.

The bad version — one query per author, inside the loop:
<?php
$authors = Author::all(); // 1 query, fetches all authors

foreach ($authors as $author) {
    echo $author->articles->count(); // +1 query, EVERY iteration
}
With 1 author, this runs 2 queries — not a problem. With 200 authors, this runs 201 queries: one to fetch the authors, then one more per author to fetch their articles, all firing separately against the database. That's the "N+1": one query, plus N more, one for every row in the first result.

The fix is eager loading with with(), which tells Eloquent up front which relationships you'll need, so it can fetch them all in one extra query instead of one-per-row:

<?php
$authors = Author::with('articles')->get(); // 2 queries total, no matter how many authors

foreach ($authors as $author) {
    echo $author->articles->count(); // already loaded, no extra query
}

With with('articles'), Eloquent runs one query to get every author, then a second query that fetches every article belonging to any of those authors in a single WHERE author_id IN (...) call, and stitches the results back together in PHP. The total stays at 2 queries whether you have 5 authors or 5,000 — a massive win as your data grows.

belongsToMany and Pivot Tables

A many-to-many relationship — articles having many tags, and tags being reused across many articles — can't be represented with a single foreign key on either table, since both sides can have many matches. It needs a third table in between, called a pivot table, whose job is just to record which article goes with which tag.

<?php
// app/Models/Article.php
public function tags()
{
    return $this->belongsToMany(Tag::class);
}

By convention, Eloquent expects the pivot table to be named after both model names, singular, alphabetically ordered, joined with an underscore — so Article and Tag gives you a pivot table named article_tag, with just two foreign key columns: article_id and tag_id.

If the pivot table itself carries extra data (a timestamp of when the tag was attached, for example), Eloquent exposes it through a special pivot property available on each related model:

<?php
$article = Article::find(1);
echo $article->tags->first()->pivot->created_at;
Next up: Now that models can be connected to each other, the next lesson goes deeper on filtering and reusing query logic with the Query Builder and Eloquent scopes — head to Query Builder & Scopes.