Eloquent Relationships
π Eloquent Relationships
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.
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.
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.
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 %}.