Introduction to Eloquent Relationships
Database tables are rarely isolated; they connect to other tables to form a complete data model. Eloquent relationships provide an elegant way to define these connections between models, making it easy to traverse and manipulate related data.
Think of relationships as the pathways between different entities in your application, similar to how highways connect cities in a road network. Just as we have different types of roads (highways, local roads, one-way streets), Eloquent provides various relationship types for different connection patterns.
This diagram shows common relationship types in a typical blog system. By defining these relationships in your models, you gain powerful capabilities to traverse, retrieve, and manipulate related data in an object-oriented way.
One-to-One Relationships
A one-to-one relationship is the simplest form of relationship, where one record in the first table is associated with exactly one record in the second table, and vice versa.
Examples in real applications include:
- A user has one profile
- A product has one main image
- A customer has one shipping address
Defining a One-to-One Relationship
// User model (the parent)
public function profile()
{
return $this->hasOne(Profile::class);
}
// Profile model (the child)
public function user()
{
return $this->belongsTo(User::class);
}
In this example:
hasOne: The User model "has one" Profile, with a foreign keyuser_idin the profiles table by conventionbelongsTo: The Profile model "belongs to" a User, with a foreign keyuser_idin its own table by convention
Customizing the Foreign Key
// Custom foreign key
public function profile()
{
return $this->hasOne(Profile::class, 'user_reference_id');
}
// Custom foreign key and local key
public function profile()
{
return $this->hasOne(Profile::class, 'user_reference_id', 'uuid');
}
Working with One-to-One Relationships
// Accessing the related model
$profile = $user->profile;
// Creating a related model
$user->profile()->create([
'bio' => 'Laravel developer',
'avatar' => 'avatar.jpg'
]);
// Updating a related model
$user->profile()->update([
'bio' => 'Senior Laravel developer'
]);
// Checking for the existence of a relationship
if ($user->profile) {
// User has a profile
}
// Eager loading to avoid N+1 problem
$users = User::with('profile')->get();
foreach ($users as $user) {
echo $user->profile->bio; // No additional queries
}
One-to-one relationships are like exclusive private lines between entities - each user has their own direct connection to a single profile. It's similar to how each person typically has one passport or one driver's license.
One-to-Many Relationships
A one-to-many relationship occurs when a single record in the first table can be associated with multiple records in the second table, but each record in the second table is associated with only one record in the first table.
Examples in real applications include:
- A user has many posts
- A category has many products
- An order has many order items
Defining a One-to-Many Relationship
// User model (the parent)
public function posts()
{
return $this->hasMany(Post::class);
}
// Post model (the child)
public function user()
{
return $this->belongsTo(User::class);
}
In this example:
hasMany: The User model "has many" Posts, with a foreign keyuser_idin the posts table by conventionbelongsTo: Each Post model "belongs to" a User, with a foreign keyuser_idin its own table by convention
Working with One-to-Many Relationships
// Accessing the related models
$posts = $user->posts; // Collection of Post models
// Creating a related model
$user->posts()->create([
'title' => 'Learning Laravel Relationships',
'content' => 'Relationships are a powerful feature...'
]);
// Using the relationship as a query builder
$recentPosts = $user->posts()
->where('created_at', '>=', now()->subDays(7))
->get();
// Counting related models
$postCount = $user->posts()->count();
// Eager loading with constraints
$users = User::with(['posts' => function ($query) {
$query->where('published', true)->orderBy('created_at', 'desc');
}])->get();
// Filtering models based on relationship existence
$usersWithPosts = User::has('posts')->get();
$usersWithRecentPosts = User::whereHas('posts', function ($query) {
$query->where('created_at', '>=', now()->subDays(7));
})->get();
One-to-many relationships are like a hub-and-spoke system - one central entity connects to many related entities. It's similar to how one teacher might have many students, or how a parent might have multiple children.
Many-to-Many Relationships
A many-to-many relationship occurs when multiple records in the first table can be associated with multiple records in the second table. This requires a pivot (or junction) table to manage the associations.
Examples in real applications include:
- A user can have many roles, and a role can belong to many users
- A post can have many tags, and a tag can belong to many posts
- A product can belong to many orders, and an order can contain many products
Defining a Many-to-Many Relationship
// Post model
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// Tag model
public function posts()
{
return $this->belongsToMany(Post::class);
}
In this example:
belongsToMany: Both models use this method to point to each other- By convention, Laravel looks for a pivot table named by alphabetically combining the singular snake-cased model names (in this case,
post_tag)
Customizing the Pivot Table
// Custom pivot table name
public function roles()
{
return $this->belongsToMany(Role::class, 'user_roles');
}
// Custom pivot table with custom keys
public function roles()
{
return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
}
Working with Many-to-Many Relationships
// Accessing the related models
$tags = $post->tags; // Collection of Tag models
// Attaching tags to a post
$post->tags()->attach([1, 2, 3]); // Attach by IDs
$post->tags()->attach($tagModels); // Attach model instances
// Detaching tags from a post
$post->tags()->detach([2, 3]); // Detach specific tags
$post->tags()->detach(); // Detach all tags
// Syncing tags (removes existing associations and adds new ones)
$post->tags()->sync([1, 4, 5]);
// Toggle tags (adds missing and removes existing)
$post->tags()->toggle([1, 2, 3]);
// Querying based on pivot columns
$post->tags()->wherePivot('featured', true)->get();
// Defining default ordering
public function tags()
{
return $this->belongsToMany(Tag::class)->orderBy('name');
}
Pivot Table with Additional Columns
Often, a pivot table needs to store additional information about the relationship itself:
// Migration for a pivot table with additional columns
Schema::create('course_student', function (Blueprint $table) {
$table->id();
$table->foreignId('course_id')->constrained();
$table->foreignId('student_id')->constrained('users');
$table->date('enrolled_at');
$table->integer('grade')->nullable();
$table->timestamps();
});
// Model definition
public function courses()
{
return $this->belongsToMany(Course::class)
->withPivot('enrolled_at', 'grade')
->withTimestamps();
}
// Attaching with additional data
$student->courses()->attach($course->id, [
'enrolled_at' => now(),
]);
// Accessing pivot data
foreach ($student->courses as $course) {
echo $course->pivot->enrolled_at;
echo $course->pivot->grade;
}
// Updating pivot data
$student->courses()->updateExistingPivot($course->id, [
'grade' => 95,
]);
Custom Pivot Models
For more complex pivot relationships, you can create a dedicated pivot model:
// Custom pivot model
class Enrollment extends Pivot
{
protected $table = 'course_student';
// Additional methods or relationships
public function isPassing()
{
return $this->grade >= 60;
}
}
// Using the custom pivot model
public function courses()
{
return $this->belongsToMany(Course::class)
->using(Enrollment::class)
->withPivot('enrolled_at', 'grade')
->withTimestamps();
}
// Now you can use the custom methods
foreach ($student->courses as $course) {
if ($course->pivot->isPassing()) {
echo 'Passing grade in ' . $course->name;
}
}
Many-to-many relationships are like networking events where attendees can connect with multiple other attendees. Each connection (stored in the pivot table) can also have its own attributes (like when they met, what they discussed, etc.).
Has One Through / Has Many Through
These relationships provide convenient shortcuts to access distant relations through an intermediate model.
Has One Through
A has-one-through relationship links models through a single intermediate relationship.
// A supplier has one history through a user
class Supplier extends Model
{
public function history()
{
return $this->hasOneThrough(
History::class, // Target model
User::class, // Intermediate model
'supplier_id', // Foreign key on intermediate
'user_id', // Foreign key on target
'id', // Local key on this model
'id' // Local key on intermediate
);
}
}
// Usage
$notes = $supplier->history->notes;
Has Many Through
A has-many-through relationship links models through a single intermediate relationship, but returns multiple models.
// A country has many posts through users
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(
Post::class, // Target model
User::class, // Intermediate model
'country_id', // Foreign key on intermediate
'user_id', // Foreign key on target
'id', // Local key on this model
'id' // Local key on intermediate
);
}
}
// Usage
$posts = $country->posts; // All posts from users in this country
Think of these "through" relationships as creating shortcut paths in a network. Instead of having to traverse from Country → Users → Posts manually, the has-many-through relationship creates a direct path from Country to Posts, similar to how an express train skips intermediate stations.
Polymorphic Relationships
Polymorphic relationships allow a model to belong to more than one type of model using a single association. This is particularly useful when you have multiple models that might share the same type of relationship.
One-to-One Polymorphic
For example, a model might have one current image that could belong to either a user, a post, or a product:
// Migration for images table
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->string('url');
$table->unsignedBigInteger('imageable_id');
$table->string('imageable_type');
$table->timestamps();
});
// Image model
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
// User model
class User extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
// Post model
class Post extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
// Usage
$user->image; // Get the user's image
$post->image; // Get the post's image
$user->image()->create(['url' => 'avatars/john.jpg']);
$post->image()->create(['url' => 'posts/header.jpg']);
// From the image, get the parent
$image = Image::find(1);
$parent = $image->imageable; // Could be a User, Post, etc.
One-to-Many Polymorphic
Similar to one-to-one polymorphic, but each parent can have multiple children:
// Comment model
class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}
// Post model
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
// Video model
class Video extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
// Usage
$post->comments; // All comments on this post
$video->comments; // All comments on this video
// Adding comments
$post->comments()->create([
'content' => 'Great post!',
'user_id' => 1
]);
// Finding the parent
$comment = Comment::find(1);
$parent = $comment->commentable; // Either a Post or Video
Many-to-Many Polymorphic
For scenarios where multiple types of models can be associated with multiple instances of another model:
// Migrations
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('taggables', function (Blueprint $table) {
$table->unsignedBigInteger('tag_id');
$table->unsignedBigInteger('taggable_id');
$table->string('taggable_type');
$table->timestamps();
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
});
// Tag model
class Tag extends Model
{
public function posts()
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function videos()
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
// Post model
class Post extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
// Video model
class Video extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
// Usage
$post->tags; // All tags for this post
$video->tags; // All tags for this video
$post->tags()->attach($tagId);
$video->tags()->attach($tagId);
// From the tag, get related models
$tag = Tag::find(1);
$tag->posts; // All posts with this tag
$tag->videos; // All videos with this tag
Polymorphic relationships are like multipurpose adapters that can connect to different types of devices. They provide flexibility when you have common functionality (like commenting, tagging, or image attachments) that needs to work across different model types.
Advanced Relationship Techniques
Relationship Existence Queries
Filtering models based on the existence of related models:
// Users who have posts
$users = User::has('posts')->get();
// Users who have 3 or more posts
$users = User::has('posts', '>=', 3)->get();
// Users who have posts with specific conditions
$users = User::whereHas('posts', function ($query) {
$query->where('published', true);
})->get();
// Users who don't have posts
$users = User::doesntHave('posts')->get();
// Users who have posts but none that are published
$users = User::whereDoesntHave('posts', function ($query) {
$query->where('published', true);
})->get();
// Count related models in a column
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count; // Number of posts
}
// Count with conditions
$users = User::withCount([
'posts',
'posts as published_posts_count' => function ($query) {
$query->where('published', true);
}
])->get();
// Conditional relationships
$users = User::with(['posts' => function ($query) {
$query->where('published', true);
}])->get();
Eager Loading
Eager loading is a technique to load all related models in a single query, avoiding the N+1 query problem:
// Basic eager loading
$posts = Post::with('user')->get();
// Multiple relationships
$posts = Post::with(['user', 'comments'])->get();
// Nested relationships
$posts = Post::with(['user.profile', 'comments.user'])->get();
// Eager loading with constraints
$posts = Post::with(['comments' => function ($query) {
$query->orderBy('created_at', 'desc')
->take(5); // Only load 5 most recent comments
}])->get();
// Lazy eager loading (add relationships after initial query)
$posts = Post::all();
if ($includeComments) {
$posts->load('comments');
}
// Conditional eager loading
$posts = Post::with(['user' => function ($query) {
$query->where('type', 'author');
}])->get();
Nested Eager Loading with Conditions
// Load posts with recent comments by active users
$posts = Post::with([
'comments' => function ($query) {
$query->where('created_at', '>=', now()->subDays(30))
->orderBy('created_at', 'desc');
},
'comments.user' => function ($query) {
$query->where('active', true);
}
])->get();
Eager loading is like preparing a shopping list before going to the store - instead of making separate trips for each item (individual queries), you get everything you need in one or two efficient visits.
Lazy Loading vs. Eager Loading
// Lazy loading (N+1 problem)
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // Separate query for each post's user
}
// Eager loading (2 queries total)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // No additional queries
}
Eager loading significantly improves performance when you're working with collections of models and their relationships.
Query Relationships with Constraints
Relationship Methods vs. Dynamic Properties
You can access relationships as dynamic properties or as methods:
// As a property (gets all posts)
$posts = $user->posts;
// As a method (returns a query builder)
$posts = $user->posts()->where('published', true)->get();
Constraining Eager Loads
// Load only published posts
$users = User::with(['posts' => function ($query) {
$query->where('published', true);
}])->get();
// Load latest post and its comments
$users = User::with([
'posts' => function ($query) {
$query->latest()->take(1);
},
'posts.comments' => function ($query) {
$query->orderBy('created_at', 'desc');
}
])->get();
// More complex constraints
$users = User::with([
'posts' => function ($query) {
$query->whereBetween('created_at', [
now()->subMonth(),
now()
])->where('published', true);
}
])->get();
Ordering Relationships
// Default ordering (always apply this ordering)
public function posts()
{
return $this->hasMany(Post::class)->orderBy('created_at', 'desc');
}
// Ordering eager loaded relationships
$users = User::with(['posts' => function ($query) {
$query->orderBy('title');
}])->get();
These techniques are like having a personalized filtering system for your data - you get exactly what you need, saving both processing time and bandwidth.
Inserting & Updating Related Models
The save Method
// Create a new related model
$post = new Post([
'title' => 'New Post',
'content' => 'Content...'
]);
$user->posts()->save($post);
// Create multiple related models
$user->posts()->saveMany([
new Post(['title' => 'Post 1']),
new Post(['title' => 'Post 2'])
]);
The create Method
// Create from array of attributes
$post = $user->posts()->create([
'title' => 'New Post',
'content' => 'Content...'
]);
// Create multiple
$user->posts()->createMany([
['title' => 'Post 1', 'content' => '...'],
['title' => 'Post 2', 'content' => '...']
]);
Attaching & Detaching
// Many-to-many relationships
$user->roles()->attach($roleId);
$user->roles()->attach([1, 2, 3]); // Multiple IDs
$user->roles()->attach($roleId, ['expires_at' => now()->addYear()]); // With pivot data
$user->roles()->detach($roleId);
$user->roles()->detach(); // Detach all roles
// Syncing (removes existing then adds new)
$user->roles()->sync([1, 2, 3]);
$user->roles()->sync([ // With pivot data
1 => ['expires_at' => now()->addYear()],
2 => ['expires_at' => now()->addMonths(6)]
]);
// Toggle (add missing, remove existing)
$user->roles()->toggle([1, 2, 3]);
Updating Many-to-Many Relationships
// Update pivot table data
$user->roles()->updateExistingPivot($roleId, [
'expires_at' => now()->addYears(2)
]);
These manipulation methods make working with related data intuitive and expressive. It's like having a well-organized filing system where you can easily add, remove, or update connected documents.
Real-World Example: E-commerce Relationships
Let's examine how relationships might work in a real e-commerce application:
User Model
class User extends Model
{
// One-to-Many: A user has many orders
public function orders()
{
return $this->hasMany(Order::class);
}
// One-to-Many: A user has many reviews
public function reviews()
{
return $this->hasMany(Review::class);
}
// Many-to-Many: A user can wishlist many products
public function wishlist()
{
return $this->belongsToMany(Product::class, 'wishlist')
->withTimestamps();
}
// Has-One: A user has one primary shipping address
public function address()
{
return $this->hasOne(Address::class)->where('is_primary', true);
}
// Has-Many: A user has many addresses
public function addresses()
{
return $this->hasMany(Address::class);
}
}
Product Model
class Product extends Model
{
// Belongs-To: A product belongs to a category
public function category()
{
return $this->belongsTo(Category::class);
}
// Belongs-To: A product belongs to a brand
public function brand()
{
return $this->belongsTo(Brand::class);
}
// One-to-Many: A product has many reviews
public function reviews()
{
return $this->hasMany(Review::class);
}
// Has-Many: A product has many images
public function images()
{
return $this->hasMany(ProductImage::class);
}
// Many-to-Many: A product can have many tags
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// Has-Many: A product can have many variants
public function variants()
{
return $this->hasMany(ProductVariant::class);
}
// Has-One: A product has one primary image
public function mainImage()
{
return $this->hasOne(ProductImage::class)
->orderBy('is_primary', 'desc')
->orderBy('id', 'asc');
}
// Many-to-Many: Users who have wishlisted this product
public function wishlistedBy()
{
return $this->belongsToMany(User::class, 'wishlist')
->withTimestamps();
}
// Many through: Get all reviews from this product's category
public function categorySiblingReviews()
{
return $this->hasManyThrough(
Review::class,
Product::class,
'category_id', // Foreign key on intermediate (Product)
'product_id', // Foreign key on target (Review)
'category_id', // Local key on this model (Product)
'id' // Local key on intermediate (Product)
)->where('products.id', '!=', $this->id);
}
}
Category Model
class Category extends Model
{
// One-to-Many: A category has many products
public function products()
{
return $this->hasMany(Product::class);
}
// Self-referential Belongs-To: A category may have a parent category
public function parent()
{
return $this->belongsTo(Category::class, 'parent_id');
}
// Self-referential Has-Many: A category may have child categories
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
// Recursive relationship to get all descendants
public function descendants()
{
return $this->children()->with('descendants');
}
// Recursive relationship to get all ancestors
public function ancestors()
{
return $this->parent()->with('ancestors');
}
// Has-Many-Through: Get all products from subcategories
public function allProducts()
{
// Get direct products
$directProducts = $this->products();
// Get child category IDs
$childIds = $this->descendants()->pluck('id')->toArray();
// If no children, just return direct products
if (empty($childIds)) {
return $directProducts;
}
// Otherwise, also get products from all child categories
return Product::where(function ($query) use ($childIds) {
$query->where('category_id', $this->id)
->orWhereIn('category_id', $childIds);
});
}
}
Order Model
class Order extends Model
{
// Belongs-To: An order belongs to a user
public function user()
{
return $this->belongsTo(User::class);
}
// Has-Many: An order has many items
public function items()
{
return $this->hasMany(OrderItem::class);
}
// Has-Many: An order has many payments
public function payments()
{
return $this->hasMany(Payment::class);
}
// Has-One: An order has one shipping address
public function shippingAddress()
{
return $this->hasOne(OrderAddress::class)
->where('type', 'shipping');
}
// Has-One: An order has one billing address
public function billingAddress()
{
return $this->hasOne(OrderAddress::class)
->where('type', 'billing');
}
// Has-Many-Through: Get all products in this order
public function products()
{
return $this->hasManyThrough(
Product::class,
OrderItem::class,
'order_id', // Foreign key on OrderItem
'id', // Foreign key on Product (actually primary key)
'id', // Local key on Order
'product_id' // Local key on OrderItem
);
}
}
OrderItem Model
class OrderItem extends Model
{
// Belongs-To: An order item belongs to an order
public function order()
{
return $this->belongsTo(Order::class);
}
// Belongs-To: An order item refers to a product
public function product()
{
return $this->belongsTo(Product::class);
}
// Accessor to calculate subtotal
public function getSubtotalAttribute()
{
return $this->quantity * $this->price;
}
}
This example demonstrates how a complex application can be modeled using Eloquent relationships. Each model maintains its own specific relationships, creating a web of connections that accurately represents the business domain.
Best Practices for Working with Relationships
- Always Use Eager Loading - Avoid the N+1 query problem by using
with()when you know you'll need related models - Constrain Eager Loads - Only load what you need by adding constraints to eager loads
- Use Model Events for Cascading Changes - Use model events to handle cascading updates or deletions
- Create Meaningful Relationship Methods - Name relationships clearly and add helper methods that represent common queries
- Consider Custom Pivot Models - For complex pivot relationships, create dedicated models
- Use Database Indexes - Ensure foreign keys and frequently queried fields are properly indexed
- Set Appropriate Foreign Key Constraints - Use cascading updates/deletes when appropriate, or restrict/set null when needed
- Leverage Model Factories for Testing - Create factories with relationships for testing complex interactions
Common Pitfalls to Avoid
- N+1 Query Problem - Not using eager loading when iterating over collections and accessing relationships
- Overeager Loading - Loading too many relationships or too deeply nested relationships
- Forgetting Indexes - Poor performance due to missing indexes on foreign keys
- Circular References - Creating circular dependencies between models that can cause infinite recursion
- Not Using Transactions - When updating multiple related models, forgetting to use database transactions
Practice Activity
Basic Relationships Exercise
Create a simple blog system with the following relationships:
- A user has many posts (one-to-many)
- A post belongs to a user (belongs-to)
- A post has many comments (one-to-many)
- A comment belongs to a post and a user (two belongs-to)
- A post can have many tags, and a tag can belong to many posts (many-to-many)
Define all the model relationships and create example code to demonstrate retrieving and creating related data.
Advanced Relationships Exercise
Extend the blog system with more advanced relationships:
- A user has one profile (one-to-one)
- A user can follow many other users, and be followed by many users (self-referential many-to-many)
- A post and a comment can both have many likes (polymorphic one-to-many)
- A category can have nested subcategories (self-referential one-to-many)
- A user can bookmark both posts and comments (polymorphic many-to-many)
Define these relationships and write code to demonstrate eager loading, querying with relationship constraints, and creating related models.
E-commerce Relationships Exercise
Design the relationships for an e-commerce system with these requirements:
- Products belong to categories, which can be nested
- Products can have multiple variants (size, color, etc.)
- Users can create orders containing multiple products
- Orders have shipping and billing addresses
- Users can review products they've purchased
- Products can be tagged with multiple attributes
- Products can be added to users' wishlists
Create the model relationship definitions and write example code for common operations like finding a user's orders, a product's reviews, and listing products by category (including subcategories).
Summary
- Eloquent provides various relationship types to model connections between database tables
- One-to-one relationships connect single records between tables
- One-to-many relationships connect a single record to multiple related records
- Many-to-many relationships connect multiple records on each side, requiring a pivot table
- More specialized relationships include has-one-through, has-many-through, and polymorphic relationships
- Eager loading solves the N+1 query problem by loading related models efficiently
- Relationship methods provide a query builder for adding constraints
- Laravel provides expressive methods for creating, updating, and manipulating related models
- Complex real-world applications can be modeled through networks of interconnected models
In the next lecture, we'll explore Eloquent querying and the database operations in more detail, building on our understanding of models and their relationships.