Eloquent Relationships

Laravel Fundamentals
Course 1 Β· Chapter 6 Β· Eloquent Relationships

πŸ”— Eloquent Relationships

A real difference from Django worth noticing before anything else: Django declares a relationship as a field (ForeignKey). Eloquent declares one as a method that returns a relationship object β€” then lets you access it like a property anyway, through PHP "magic" that calls and caches the method the first time it's touched.

belongsTo and hasMany

// app/Models/Book.php
class Book extends Model
{
    protected $fillable = ['title', 'price', 'author_id'];

    public function author()
    {
        return $this->belongsTo(Author::class);  // assumes an author_id foreign key
    }
}

// app/Models/Author.php
class Author extends Model
{
    protected $fillable = ['name'];

    public function books()
    {
        return $this->hasMany(Book::class);
    }
}

Django's Declarative Field vs Eloquent's Relationship Method

Django: A Field Declaration
class Book(models.Model):
    author = models.ForeignKey(
        Author, on_delete=models.CASCADE, related_name="books"
    )

The relationship IS the field β€” both directions come from this one declaration.

Laravel: A Method Per Direction
// on Book:
public function author() { return $this->belongsTo(Author::class); }
// on Author:
public function books() { return $this->hasMany(Book::class); }

Each direction is its own explicit method β€” more to write, but each side reads clearly on its own model.

// Access "like a property" β€” Eloquent calls author() once, then caches the result
$book->author->name;

// Author -> Books β€” a Collection, since an author can have many
$author->books;

πŸ•ΈοΈ belongsToMany: Many-to-Many

// app/Models/Tag.php
class Tag extends Model
{
    protected $fillable = ['name'];

    public function books()
    {
        return $this->belongsToMany(Book::class);  // assumes a "book_tag" pivot table
    }
}

// app/Models/Book.php β€” add the inverse
public function tags()
{
    return $this->belongsToMany(Tag::class);
}
$book->tags()->attach($sciFiTag->id);   // add a tag
$book->tags()->detach($sciFiTag->id);   // remove a tag
$book->tags()->sync([1, 2, 3]);         // set the EXACT tag list, replacing whatever was there

$book->tags;                            // every tag on this book
$sciFiTag->books;                       // every book with this tag

Pivot Table Naming Convention

Eloquent assumes a table named from the two model names, alphabetized and singular, joined by an underscore β€” book_tag for Book/Tag β€” configurable if you need a different name.

attach / detach / sync

Three distinct pivot-management methods β€” add one, remove one, or replace the entire set β€” with no single direct Django ORM equivalent (Django manages M2M through the field's own .add()/.remove()/.set(), which map closely to these three).

⚠️ The N+1 Problem, a Third Time

This is the third framework in this project to run into exactly this trap β€” Rails, Django, and now Eloquent all share the identical root cause: a relationship access inside a loop triggers one query per row.

N+1 Loop vs Eager Loading

N+1: One Query Per Book
$books = Book::all();              // 1 query
foreach ($books as $book) {
    echo $book->author->name;    // 1 MORE query, per book
}
Fixed: with()
$books = Book::with('author')->get();  // 2 queries, total
foreach ($books as $book) {
    echo $book->author->name;    // already loaded β€” no extra query
}

One genuine simplification over Django here: with() is the single mechanism for eager loading regardless of relationship type β€” Django needs select_related() for ForeignKey/OneToOne and a separate prefetch_related() for ManyToMany/reverse FK. Eloquent doesn't split this into two methods:

One Method, Any Relationship Type

$books = Book::with(['author', 'tags'])->get();
// author is belongsTo, tags is belongsToMany β€” BOTH eager-loaded via the same with() call.
// Exactly 3 queries total: books, then authors, then tags β€” regardless of row count.

foreach ($books as $book) {
    echo $book->title . ': ' . $book->author->name;
    foreach ($book->tags as $tag) {
        echo $tag->name;
    }
}

Filtering by Relationship: whereHas()

// Authors who have written at least one book under $15
Author::whereHas('books', function ($query) {
    $query->where('price', '<', 15);
})->get();

πŸ’» Coding Challenges

Challenge 1: Add a belongsTo/hasMany Pair

Add a Publisher model with a hasMany relationship to Book, and add the matching belongsTo relationship method (plus a publisher_id foreign key) on Book.

Goal: Practice writing both directions of a one-to-many relationship as separate methods.

β†’ Solution

Challenge 2: Manage a Many-to-Many Relationship

Using the Book/Tag models from this chapter, write the code to attach two tags to a book, then remove one of them, then finally replace the whole tag list with sync().

Goal: Practice attach(), detach(), and sync() and understand how each behaves differently.

β†’ Solution

Challenge 3: Fix an N+1 Across Two Relationship Types

Given a loop over Book::all() that, for each book, prints its author's name AND every tag's name, rewrite the query to eliminate the N+1 problem for both relationships using a single with() call.

Goal: Practice using Eloquent's unified with() for both a belongsTo and a belongsToMany relationship at once.

β†’ Solution

⚠️ Gotcha: Eager Loading Has to Happen Before the Query Runs

with('author') only helps if it's part of the query before get() executes β€” calling it inside the loop, or trying to "add" eager loading after Book::all() has already run, does nothing; the N+1 queries have already happened by then. Also worth knowing: whereHas() runs an additional subquery to check the relationship condition β€” convenient for filtering, but it's a real performance cost of its own on a large table, not a free operation; reach for a plain join instead if the same filter needs to run on a hot, high-traffic path.

🎯 What's Next

Relationships between models are now readable and efficient β€” the next chapter turns to getting new data in: Form Requests & Validation, Laravel's FormRequest classes, validation rules, and Blade's @csrf directive compared to Django's {% csrf_token %}.