Laravel’s Eloquent ORM transforms complex database relationships into elegant, readable code. If you’re building applications where one record relates to multiple others—like posts with comments or users with orders—mastering the hasMany
relationship is essential for clean, efficient data management.
This comprehensive guide walks you through everything you need to know about Laravel 12’s hasMany
Eloquent relationships, from basic setup to advanced techniques that will make your applications more maintainable and performant.
Why Relationships Matter in Laravel Applications
Database relationships form the backbone of modern web applications. Without proper relationship management, you’ll find yourself writing repetitive queries, dealing with data inconsistencies, and struggling to maintain clean code as your application grows.
Laravel’s Eloquent ORM eliminates these challenges by providing intuitive methods that mirror real-world relationships. Instead of writing complex SQL joins, you can access related data with simple, expressive syntax that makes your code both readable and maintainable.
Overview of Eloquent ORM and the Role of hasMany
Eloquent ORM serves as Laravel’s powerful database abstraction layer, turning database tables into PHP objects with built-in relationship capabilities. The hasMany
relationship specifically handles one-to-many associations, where a single parent record connects to multiple child records.
This relationship type appears everywhere in web development:
- A blog post has many comments
- A user has many orders
- A category has many products
- A department has many employees
Understanding One-to-Many Relationships
What is a One-to-Many Relationship in Database Design?
A one-to-many relationship occurs when a single record in one table associates with multiple records in another table. The parent table contains the primary key, while the child table stores a foreign key that references the parent.
In database terms, this relationship follows a simple rule: one parent can have many children, but each child belongs to only one parent.
Real-World Examples of One-to-Many Relationships
Consider these common scenarios:
Blog System:
- One author writes many articles
- One article receives many comments
- One category contains many posts
E-commerce Platform:
- One customer places many orders
- One order contains many items
- One product has many reviews
Content Management:
- One user creates many posts
- One album contains many photos
- One project has many tasks
Getting Started with Laravel 12 and Eloquent
Setting Up Models and Migrations
Before defining relationships, you need properly structured models and database tables. Let’s create a classic blog example with posts and comments.
First, generate your models and migrations:
php artisan make:model Post -m
php artisan make:model Comment -m
Set up your Post migration:
<?php
// database/migrations/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()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
};
Configure your Comment migration:
<?php
// database/migrations/create_comments_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()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->string('author_name');
$table->string('author_email');
$table->text('content');
$table->boolean('is_approved')->default(false);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('comments');
}
};
Run the migrations:
php artisan migrate
Naming Conventions That Matter in Eloquent Relationships
Laravel follows specific naming conventions that enable automatic relationship detection:
Convention | Example | Purpose |
Model Names | Post , Comment |
Singular, PascalCase |
Table Names | posts , comments |
Plural, snake_case |
Foreign Keys | post_id |
{model}_id format |
Primary Keys | id |
Standard auto-increment |
Following these conventions reduces configuration and potential errors in your relationships.
Defining a hasMany Relationship
How to Use the hasMany() Method in Laravel 12
The hasMany
method creates a one-to-many relationship in your parent model. Here’s the basic syntax:
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'content',
'published_at',
];
protected $casts = [
'published_at' => 'datetime',
];
/**
* Get the comments for the post.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
Example: A Post Has Many Comments
The relationship definition above establishes that each post can have multiple comments. Laravel automatically assumes:
- The foreign key is
post_id
in the comments table - The local key is
id
in the posts table
For custom key names, specify them explicitly:
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'custom_post_id', 'custom_id');
}
Creating the Inverse Relationship with belongsTo
Understanding the Role of belongsTo() in the Child Model
Every hasMany
relationship requires a corresponding belongsTo
relationship in the child model. This inverse relationship allows you to access the parent from the child record.
Example: A Comment Belongs to a Post
<?php
// app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
use HasFactory;
protected $fillable = [
'post_id',
'author_name',
'author_email',
'content',
'is_approved',
];
protected $casts = [
'is_approved' => 'boolean',
];
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
Working with Related Data
Eager Loading vs Lazy Loading with hasMany
Understanding when and how to load related data prevents the N+1 query problem and optimizes performance.
Lazy Loading (Default):
$posts = Post::all();
// This triggers one query per post to load comments
foreach ($posts as $post) {
echo $post->comments->count(); // N+1 queries!
}
Eager Loading (Recommended):
// Load posts with their comments in just 2 queries
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
echo $post->comments->count(); // No additional queries
}
Accessing and Displaying Related Records
Once relationships are defined, accessing related data becomes intuitive:
// Get a specific post with its comments
$post = Post::with('comments')->find(1);
// Display post title and comment count
echo $post->title . ' has ' . $post->comments->count() . ' comments';
// Loop through comments
foreach ($post->comments as $comment) {
echo $comment->author_name . ': ' . $comment->content;
}
Chaining Queries on Related Models
Eloquent allows you to chain query methods on relationships:
// Get only approved comments
$post = Post::find(1);
$approvedComments = $post->comments()->where('is_approved', true)->get();
// Count comments by a specific author
$authorCommentCount = $post->comments()
->where('author_email', '[email protected]')
->count();
// Get the latest 5 comments
$recentComments = $post->comments()
->latest()
->limit(5)
->get();
Filtering hasMany Relationships with Conditions
Ordering, Limiting, and Paginating Related Results
Apply conditions directly to relationship queries for precise data retrieval:
class Post extends Model
{
// Get only approved comments
public function approvedComments(): HasMany
{
return $this->hasMany(Comment::class)->where('is_approved', true);
}
// Get comments ordered by creation date
public function latestComments(): HasMany
{
return $this->hasMany(Comment::class)->latest();
}
// Get recent comments with pagination
public function recentCommentsPaginated()
{
return $this->comments()
->where('is_approved', true)
->latest()
->paginate(10);
}
}
Usage examples:
$post = Post::find(1);
// Get approved comments only
$approved = $post->approvedComments;
// Paginate through recent comments
$comments = $post->recentCommentsPaginated();
Inserting and Saving Related Records
Creating Related Records Using save() and create()
Laravel provides multiple methods for creating related records:
Using save() method:
$post = Post::find(1);
$comment = new Comment([
'author_name' => 'John Doe',
'author_email' => '[email protected]',
'content' => 'Great article!',
'is_approved' => true,
]);
$post->comments()->save($comment);
Using create() method:
$post = Post::find(1);
$comment = $post->comments()->create([
'author_name' => 'Jane Smith',
'author_email' => '[email protected]',
'content' => 'Thanks for sharing!',
'is_approved' => true,
]);
Using saveMany() for multiple records:
$post = Post::find(1);
$comments = [
new Comment(['author_name' => 'User 1', 'content' => 'First comment']),
new Comment(['author_name' => 'User 2', 'content' => 'Second comment']),
];
$post->comments()->saveMany($comments);
Bulk Inserting Data into hasMany Relationships
For large datasets, use createMany()
for better performance:
$post = Post::find(1);
$commentData = [
[
'author_name' => 'Bulk User 1',
'author_email' => '[email protected]',
'content' => 'Bulk comment 1',
'is_approved' => true,
],
[
'author_name' => 'Bulk User 2',
'author_email' => '[email protected]',
'content' => 'Bulk comment 2',
'is_approved' => false,
],
];
$post->comments()->createMany($commentData);
Updating and Deleting Related Records
Updating Child Records from the Parent Model
Update related records through the parent model:
$post = Post::find(1);
// Update all comments for a post
$post->comments()->update(['is_approved' => true]);
// Update specific comments
$post->comments()
->where('author_email', '[email protected]')
->update(['is_approved' => false]);
// Update a single comment
$comment = $post->comments()->where('id', 5)->first();
$comment->update(['content' => 'Updated content']);
Cascading Deletes: Managing Data Integrity
Handle data integrity when deleting parent records:
Database-level cascading (Recommended):
// In your migration
$table->foreignId('post_id')->constrained()->onDelete('cascade');
Application-level cascading:
class Post extends Model
{
protected static function boot()
{
parent::boot();
static::deleting(function ($post) {
$post->comments()->delete();
});
}
}
Using hasMany with Form Submissions
Handling Nested Form Inputs in One-to-Many Structures
Create forms that handle parent and child records simultaneously:
HTML Form Structure:
<form method="POST" action="/posts">
@csrf
<input type="text" name="title" placeholder="Post Title">
<textarea name="content" placeholder="Post Content"></textarea>
<div id="comments">
<div class="comment-input">
<input type="text" name="comments[0][author_name]" placeholder="Author Name">
<input type="email" name="comments[0][author_email]" placeholder="Author Email">
<textarea name="comments[0][content]" placeholder="Comment"></textarea>
</div>
</div>
<button type="button" id="add-comment">Add Comment</button>
<button type="submit">Save Post</button>
</form>
Validating and Storing Multiple Child Records
Controller handling:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'comments' => 'sometimes|array',
'comments.*.author_name' => 'required|string|max:255',
'comments.*.author_email' => 'required|email',
'comments.*.content' => 'required|string',
]);
$post = Post::create([
'title' => $validated['title'],
'content' => $validated['content'],
'published_at' => now(),
]);
if (isset($validated['comments'])) {
$post->comments()->createMany($validated['comments']);
}
return redirect()->route('posts.show', $post);
}
}
Advanced Eloquent Techniques with hasMany
Using hasManyThrough for Indirect Relationships
Access related records through intermediate models:
// Country -> hasMany -> Users -> hasMany -> Posts
class Country extends Model
{
public function posts(): HasManyThrough
{
return $this->hasManyThrough(Post::class, User::class);
}
}
// Usage
$country = Country::find(1);
$posts = $country->posts; // All posts by users in this country
Leveraging Relationship Events for Automation
Automate actions when relationships change:
class Post extends Model
{
protected static function boot()
{
parent::boot();
// Automatically approve comments from trusted users
static::saved(function ($post) {
$post->comments()
->whereIn('author_email', ['[email protected]', '[email protected]'])
->update(['is_approved' => true]);
});
}
}
Testing One-to-Many Relationships
Writing Unit Tests for hasMany Relationships
Ensure your relationships work correctly with comprehensive tests:
<?php
namespace Tests\Unit;
use App\Models\Post;
use App\Models\Comment;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostCommentRelationshipTest extends TestCase
{
use RefreshDatabase;
public function test_post_can_have_many_comments()
{
$post = Post::factory()->create();
$comments = Comment::factory()->count(3)->create(['post_id' => $post->id]);
$this->assertCount(3, $post->comments);
$this->assertTrue($post->comments->contains($comments->first()));
}
public function test_comment_belongs_to_post()
{
$post = Post::factory()->create();
$comment = Comment::factory()->create(['post_id' => $post->id]);
$this->assertEquals($post->id, $comment->post->id);
$this->assertEquals($post->title, $comment->post->title);
}
public function test_deleting_post_deletes_comments()
{
$post = Post::factory()->create();
Comment::factory()->count(2)->create(['post_id' => $post->id]);
$this->assertCount(2, Comment::all());
$post->delete();
$this->assertCount(0, Comment::all());
}
}
Seeding and Faking Related Data for Tests
Create realistic test data with factories:
<?php
// database/factories/PostFactory.php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition()
{
return [
'title' => $this->faker->sentence(),
'content' => $this->faker->paragraphs(3, true),
'published_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
];
}
public function withComments($count = 3)
{
return $this->afterCreating(function ($post) use ($count) {
Comment::factory()->count($count)->create(['post_id' => $post->id]);
});
}
}
<?php
// database/factories/CommentFactory.php
namespace Database\Factories;
use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
class CommentFactory extends Factory
{
public function definition()
{
return [
'post_id' => Post::factory(),
'author_name' => $this->faker->name(),
'author_email' => $this->faker->safeEmail(),
'content' => $this->faker->paragraph(),
'is_approved' => $this->faker->boolean(80), // 80% approved
];
}
}
Common Pitfalls and How to Avoid Them
Misnaming Foreign Keys and Table Names
Problem: Laravel can’t find relationships due to naming inconsistencies.
Solution: Follow Laravel’s naming conventions or specify keys explicitly:
// If your foreign key isn't following convention
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'article_id', 'id');
}
Avoiding Performance Issues with Eager Loading
Problem: N+1 query problems when accessing relationships in loops.
Solution: Always use eager loading for relationships accessed in iterations:
// Bad: N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
// Good: 2 queries total
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
echo $post->comments->count();
}
Best Practices for Clean and Scalable Relationships
Keeping Relationship Logic Out of Controllers
Move complex relationship logic to model methods or dedicated service classes:
class Post extends Model
{
public function getPopularComments($limit = 5)
{
return $this->comments()
->where('is_approved', true)
->withCount('likes')
->orderBy('likes_count', 'desc')
->limit($limit)
->get();
}
public function hasUnmoderatedComments(): bool
{
return $this->comments()->where('is_approved', false)->exists();
}
}
Reusing Relationships with Scopes and Accessors
Create reusable query scopes:
class Comment extends Model
{
public function scopeApproved($query)
{
return $query->where('is_approved', true);
}
public function scopeByAuthor($query, $email)
{
return $query->where('author_email', $email);
}
public function scopeRecent($query, $days = 7)
{
return $query->where('created_at', '>=', now()->subDays($days));
}
}
Usage:
$post = Post::find(1);
$recentApprovedComments = $post->comments()->approved()->recent(3)->get();
Use Case Examples Across Applications
Blog: Posts and Comments
Complete blog implementation with moderation:
class BlogController extends Controller
{
public function show(Post $post)
{
$post->load(['comments' => function ($query) {
$query->approved()->with('author')->latest();
}]);
return view('blog.show', compact('post'));
}
public function storeComment(Request $request, Post $post)
{
$validated = $request->validate([
'author_name' => 'required|string|max:255',
'author_email' => 'required|email',
'content' => 'required|string|max:1000',
]);
$comment = $post->comments()->create($validated);
return back()->with('success', 'Comment submitted for review!');
}
}
E-commerce: Orders and Order Items
Order management with items:
class Order extends Model
{
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public function getTotalAttribute(): float
{
return $this->items->sum(function ($item) {
return $item->quantity * $item->price;
});
}
public function addItem($product, $quantity = 1): OrderItem
{
return $this->items()->create([
'product_id' => $product->id,
'product_name' => $product->name,
'price' => $product->price,
'quantity' => $quantity,
]);
}
}
SaaS: Users and Subscriptions
Subscription management:
class User extends Model
{
public function subscriptions(): HasMany
{
return $this->hasMany(Subscription::class);
}
public function activeSubscriptions(): HasMany
{
return $this->hasMany(Subscription::class)
->where('status', 'active')
->where('expires_at', '>', now());
}
public function hasActiveSubscription($plan = null): bool
{
$query = $this->activeSubscriptions();
if ($plan) {
$query->where('plan_name', $plan);
}
return $query->exists();
}
}
Conclusion
Summary of Key Concepts and Techniques
Laravel 12’s hasMany
relationship provides a powerful, intuitive way to manage one-to-many database relationships. Key takeaways include:
- Follow Laravel’s naming conventions for automatic relationship detection
- Always define inverse relationships with
belongsTo
- Use eager loading to prevent N+1 query problems
- Leverage relationship methods for clean, maintainable code
- Test your relationships thoroughly to ensure data integrity
- Apply best practices to keep your codebase scalable and performant
What’s Next: Exploring Other Relationship Types in Laravel
Now that you’ve mastered hasMany
relationships, consider exploring other Eloquent relationship types:
- Many-to-Many: Perfect for tags, roles, and category systems
- Has-One-Through: Access related records through intermediate models
- Polymorphic Relationships: Handle relationships where a model can belong to multiple other model types
- Many-to-Many Polymorphic: Complex relationships with additional pivot data
Mastering these relationship types will give you the tools to model any real-world data structure efficiently and elegantly in your Laravel applications.
Ready to implement hasMany
relationships in your next Laravel project? Start with a simple parent-child structure and gradually add complexity as your application grows. Remember: clean relationships lead to maintainable code and better user experiences.