Laravel Caching: Boosting Performance with Redis and Memcached
Speed up your application with intelligent caching strategies.
Relationships are the heart of relational databases. In Laravel, Eloquent makes working with relationships intuitive and powerful. Let's demystify the two most common relationship types: One-to-Many and Many-to-One.
Quick Overview:
A One-to-Many relationship exists when a single model owns multiple instances of another model.
Real-world Examples:
-- Users table
id | name | email
1 | John | john@example.com
2 | Jane | jane@example.com
-- Posts table
id | title | content | user_id
1 | My First Post | Hello World | 1
2 | Another Post | More content | 1
3 | Jane's Post | My content | 2
In the User Model (One side):
<?php
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/**
* Get all posts for the user.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
// If you need custom foreign key:
// return $this->hasMany(Post::class, 'author_id');
// If you need custom local key:
// return $this->hasMany(Post::class, 'author_id', 'id');
}
/**
* Get only published posts.
*/
public function publishedPosts(): HasMany
{
return $this->hasMany(Post::class)->where('is_published', true);
}
/**
* Get posts ordered by creation date.
*/
public function latestPosts(): HasMany
{
return $this->hasMany(Post::class)->latest();
}
}
In the Post Model (Many side):
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
/**
* Get the user that owns the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
// If you need custom foreign key:
// return $this->belongsTo(User::class, 'author_id');
// If you need custom owner key:
// return $this->belongsTo(User::class, 'author_id', 'id');
}
/**
* Get the user with only specific columns.
*/
public function userBasic(): BelongsTo
{
return $this->belongsTo(User::class)->select('id', 'name', 'email');
}
}
A Many-to-One relationship exists when many models belong to a single parent model. This is essentially the inverse of a One-to-Many relationship.
Real-world Examples:
-- Same tables as before
-- Users table
id | name | email
1 | John | john@example.com
-- Posts table (many posts belong to one user)
id | title | content | user_id
1 | My First Post | Hello World | 1
2 | Another Post | More content | 1
3 | Third Post | More content | 1
From the "One" side (User):
// Get a user with their posts
$user = User::find(1);
// Access posts as a collection
$posts = $user->posts;
// Loop through posts
foreach ($user->posts as $post) {
echo $post->title;
}
// Count posts
$postCount = $user->posts()->count();
// Get specific post
$firstPost = $user->posts()->first();
From the "Many" side (Post):
// Get a post with its user
$post = Post::find(1);
// Access the user
$user = $post->user;
// Get user attributes
echo $post->user->name;
echo $post->user->email;
// Check if post belongs to specific user
if ($post->user->id === 1) {
echo "This post belongs to user #1";
}
Creating from the "One" side:
$user = User::find(1);
// Method 1: Create and associate
$post = new Post([
'title' => 'New Post',
'content' => 'Post content here...'
]);
$user->posts()->save($post);
// Method 2: Create directly
$post = $user->posts()->create([
'title' => 'Another Post',
'content' => 'More content here...'
]);
// Method 3: Create multiple posts
$user->posts()->createMany([
[
'title' => 'Post One',
'content' => 'Content one...'
],
[
'title' => 'Post Two',
'content' => 'Content two...'
]
]);
Creating from the "Many" side:
// Create post and associate with user
$post = new Post([
'title' => 'New Post',
'content' => 'Post content...'
]);
$user = User::find(1);
$post->user()->associate($user);
$post->save();
// Or create with user_id directly
$post = Post::create([
'title' => 'New Post',
'content' => 'Post content...',
'user_id' => 1
]);
// Get users who have posts
$usersWithPosts = User::has('posts')->get();
// Get users with more than 5 posts
$activeUsers = User::has('posts', '>=', 5)->get();
// Get users without any posts
$inactiveUsers = User::doesntHave('posts')->get();
// Get posts with their users (eager loading)
$posts = Post::with('user')->get();
// Get users with their posts count
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo "{$user->name} has {$user->posts_count} posts";
}
// Get posts with specific user conditions
$posts = Post::whereHas('user', function ($query) {
$query->where('name', 'John')
->where('email', 'like', '%@example.com');
})->get();
// Get users with published posts only
$users = User::whereHas('posts', function ($query) {
$query->where('is_published', true);
})->get();
// Get posts with user and specific columns
$posts = Post::with(['user' => function ($query) {
$query->select('id', 'name', 'email');
}])->get();
// Order users by their latest post
$users = User::withMax('posts', 'created_at')
->orderBy('posts_max_created_at', 'desc')
->get();
Database Migrations:
<?php
// database/migrations/2024_01_15_000001_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->boolean('is_author')->default(false);
$table->timestamps();
});
}
};
<?php
// database/migrations/2024_01_15_000002_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->text('excerpt')->nullable();
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->integer('views')->default(0);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
// Indexes for performance
$table->index(['is_published', 'published_at']);
$table->index('user_id');
});
}
};
Complete Models:
<?php
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
class User extends Model
{
use HasFactory, Notifiable;
protected $fillable = [
'name', 'email', 'password', 'is_author'
];
protected $hidden = [
'password', 'remember_token',
];
protected $casts = [
'is_author' => 'boolean',
];
/**
* Get all posts for the user.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
/**
* Get only published posts.
*/
public function publishedPosts(): HasMany
{
return $this->posts()->where('is_published', true);
}
/**
* Get the user's latest post.
*/
public function latestPost()
{
return $this->hasOne(Post::class)->latestOfMany();
}
/**
* Get the user's oldest post.
*/
public function oldestPost()
{
return $this->hasOne(Post::class)->oldestOfMany();
}
/**
* Get posts count for dashboard.
*/
public function getPostsCountAttribute(): int
{
return $this->posts()->count();
}
/**
* Get published posts count.
*/
public function getPublishedPostsCountAttribute(): int
{
return $this->publishedPosts()->count();
}
/**
* Check if user can create posts.
*/
public function canCreatePosts(): bool
{
return $this->is_author || $this->posts_count > 0;
}
}
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title', 'slug', 'content', 'excerpt',
'is_published', 'published_at', 'user_id'
];
protected $casts = [
'is_published' => 'boolean',
'published_at' => 'datetime',
];
protected $attributes = [
'views' => 0,
];
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::creating(function ($post) {
if (empty($post->slug)) {
$post->slug = Str::slug($post->title);
}
if (empty($post->excerpt)) {
$post->excerpt = Str::limit(strip_tags($post->content), 150);
}
});
static::saving(function ($post) {
// Ensure slug is unique
$originalSlug = $post->slug;
$counter = 1;
while (static::where('slug', $post->slug)
->where('id', '!=', $post->id)
->exists()) {
$post->slug = $originalSlug . '-' . $counter++;
}
});
}
/**
* Get the user that owns the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the author's name.
*/
public function getAuthorNameAttribute(): string
{
return $this->user->name;
}
/**
* Get reading time in minutes.
*/
public function getReadingTimeAttribute(): int
{
$wordCount = str_word_count(strip_tags($this->content));
return max(1, ceil($wordCount / 200));
}
/**
* Scope a query to only include published posts.
*/
public function scopePublished($query)
{
return $query->where('is_published', true)
->where('published_at', '<=', now());
}
/**
* Scope a query to only include drafts.
*/
public function scopeDraft($query)
{
return $query->where('is_published', false)
->orWhereNull('published_at');
}
/**
* Publish the post.
*/
public function publish(): bool
{
return $this->update([
'is_published' => true,
'published_at' => $this->published_at ?: now(),
]);
}
/**
* Unpublish the post.
*/
public function unpublish(): bool
{
return $this->update([
'is_published' => false,
'published_at' => null,
]);
}
/**
* Increment views count.
*/
public function incrementViews(): void
{
$this->increment('views');
}
}
Database Structure:
-- Categories table
id | name | slug
1 | Electronics| electronics
2 | Books | books
-- Products table
id | name | price | category_id
1 | iPhone 15 | 999 | 1
2 | MacBook Pro | 1999 | 1
3 | Laravel Book | 39 | 2
Model Definitions:
<?php
// app/Models/Category.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
protected $fillable = ['name', 'slug', 'description'];
/**
* Get all products for the category.
*/
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
/**
* Get only active products.
*/
public function activeProducts(): HasMany
{
return $this->products()->where('is_active', true);
}
/**
* Get featured products.
*/
public function featuredProducts(): HasMany
{
return $this->activeProducts()->where('is_featured', true);
}
}
<?php
// app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
protected $fillable = [
'name', 'slug', 'description', 'price',
'category_id', 'is_active', 'is_featured'
];
protected $casts = [
'price' => 'decimal:2',
'is_active' => 'boolean',
'is_featured' => 'boolean',
];
/**
* Get the category that owns the product.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Get category name.
*/
public function getCategoryNameAttribute(): string
{
return $this->category->name;
}
}
// ❌ BAD: N+1 Query Problem
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // Makes a new query for each post
}
// ✅ GOOD: Eager Loading (2 queries total)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // No additional queries
}
// Eager load multiple relationships
$posts = Post::with(['user', 'category', 'tags'])->get();
// Nested eager loading
$users = User::with('posts.comments')->get();
// Conditional eager loading
$posts = Post::with(['user' => function ($query) {
$query->where('is_active', true);
}])->get();
// Lazy eager loading (after initial query)
$posts = Post::all();
$posts->load('user', 'category');
$user = User::find(1);
// Dynamic property (returns Collection)
$posts = $user->posts;
// Relationship method (returns Relation instance)
$postsQuery = $user->posts();
// You can chain query methods on the relationship method
$recentPosts = $user->posts()
->where('created_at', '>', now()->subDays(7))
->orderBy('created_at', 'desc')
->get();
// But NOT on the dynamic property
// ❌ This won't work:
// $recentPosts = $user->posts->where('created_at', '>', now()->subDays(7));
// If you have additional data in the relationship
class User extends Model
{
public function teams()
{
return $this->belongsToMany(Team::class)
->withPivot('role', 'joined_at')
->withTimestamps();
}
}
// Access pivot data
$user = User::find(1);
foreach ($user->teams as $team) {
echo $team->pivot->role;
echo $team->pivot->joined_at;
}
// Get user with their post statistics
$user = User::withCount([
'posts',
'posts as published_posts_count' => function ($query) {
$query->where('is_published', true);
},
'posts as draft_posts_count' => function ($query) {
$query->where('is_published', false);
}
])->with(['posts' => function ($query) {
$query->latest()->limit(5);
}])->find(auth()->id());
// Usage in controller
return view('dashboard', [
'user' => $user,
'recentPosts' => $user->posts,
'stats' => [
'total_posts' => $user->posts_count,
'published_posts' => $user->published_posts_count,
'draft_posts' => $user->draft_posts_count,
]
]);
// Get category with paginated products
$category = Category::with(['products' => function ($query) {
$query->where('is_active', true)
->orderBy('name')
->paginate(12);
}])->where('slug', $slug)->firstOrFail();
// Or separate queries for better control
$category = Category::where('slug', $slug)->firstOrFail();
$products = $category->products()
->where('is_active', true)
->orderBy('name')
->paginate(12);
// Update all posts for a user
$user = User::find(1);
$user->posts()->update(['is_featured' => false]);
// Delete all draft posts
$user->posts()->where('is_published', false)->delete();
// Add posts to multiple users (using relationship)
$posts = Post::whereIn('id', [1, 2, 3])->get();
$posts->each->user()->associate($newUser);
$posts->each->save();
// Check if foreign key matches
// Default: user_id (snake_case of model name + _id)
// If your foreign key is different:
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'author_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
// Make sure foreign keys are fillable
protected $fillable = [
'title', 'content', 'user_id' // Don't forget user_id!
];
// Or use guarded
protected $guarded = [];
// Make sure you're using with() before get()
// ❌ This won't eager load:
$posts = Post::all()->with('user');
// ✅ This will eager load:
$posts = Post::with('user')->get();
HasMany is used on the "one" side of the relationship (User has many Posts), while BelongsTo is used on the "many" side (Post belongs to User). They represent the same relationship from different perspectives.
Pass the custom foreign key as the second parameter: return $this->hasMany(Post::class, 'author_id') or return $this->belongsTo(User::class, 'author_id').
Eager loading loads related models in a single query to avoid the N+1 query problem, significantly improving performance when accessing relationships in loops.
Use $user->posts()->create([...]) from the "one" side, or set the foreign key directly $post->user_id = 1 from the "many" side.
$user->posts returns a Collection of related models, while $user->posts() returns a Relation instance that you can chain query methods on.
Use withCount(): User::withCount('posts')->get() which adds a posts_count attribute without loading all the related models.
Now you've mastered One-to-Many and Many-to-One relationships in Laravel Eloquent! In our next post, we'll explore Laravel Eloquent: Mastering Many-to-Many and Polymorphic Relationships to handle more complex relationship scenarios.