Eloquent Deep Relationship Queries: Mastering the has() Method
Laravel's Eloquent ORM provides a powerful method for querying relationships at multiple levels deep using the has()
method. This technique allows you to filter records based on the existence of related models, even when those relationships are nested several layers deep.
Understanding Deep Relationship Queries
The has()
method lets you query relationships using dot notation, enabling you to check for the existence of nested relationships without writing complex joins or subqueries.
Basic Syntax
// Find authors who have books that have ratings
$authors = Author::has('books.ratings')->get();
This simple query traverses two relationship levels:
Author
has manybooks
- Each
Book
has manyratings
The result includes only authors whose books have at least one rating.
Real-World Examples
E-commerce: Products with Reviews
class Product extends Model
{
public function categories()
{
return $this->belongsToMany(Category::class);
}
public function orderItems()
{
return $this->hasMany(OrderItem::class);
}
public function reviews()
{
return $this->hasMany(Review::class);
}
}
class OrderItem extends Model
{
public function order()
{
return $this->belongsTo(Order::class);
}
}
class Order extends Model
{
public function customer()
{
return $this->belongsTo(Customer::class);
}
}
// Find products that have been purchased and reviewed
$popularProducts = Product::has('orderItems.order')
->has('reviews')
->get();
// Find products purchased by verified customers with reviews
$trustedProducts = Product::has('orderItems.order.customer', '>=', 1)
->whereHas('orderItems.order.customer', function ($query) {
$query->whereNotNull('email_verified_at');
})
->has('reviews', '>=', 5)
->get();
Blog System: Posts with Engagement
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
}
class Post extends Model
{
public function comments()
{
return $this->hasMany(Comment::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
class Comment extends Model
{
public function replies()
{
return $this->hasMany(Comment::class, 'parent_id');
}
public function likes()
{
return $this->hasMany(CommentLike::class);
}
}
// Find users who have posts with comments that have replies
$activeUsers = User::has('posts.comments.replies')->get();
// Find posts with highly engaged comments (comments that have likes)
$engagedPosts = Post::has('comments.likes', '>=', 10)->get();
// Find users whose posts have comments with multiple replies
$influentialUsers = User::has('posts.comments.replies', '>=', 3)
->withCount([
'posts.comments.replies as total_comment_replies'
])
->orderBy('total_comment_replies', 'desc')
->get();
Advanced Filtering with whereHas()
Combine has()
with whereHas()
for more complex filtering:
Complex Business Logic
class SubscriptionService
{
public function getActiveSubscribersWithRecentActivity()
{
return User::has('subscription')
->whereHas('subscription', function ($query) {
$query->where('status', 'active')
->where('expires_at', '>', now());
})
->has('orders.items.product')
->whereHas('orders', function ($query) {
$query->where('created_at', '>=', now()->subDays(30))
->where('status', 'completed');
})
->get();
}
public function getPremiumUsersWithActivity()
{
return User::has('subscription.plan')
->whereHas('subscription.plan', function ($query) {
$query->where('tier', 'premium');
})
->has('posts.comments.likes', '>=', 10)
->withCount([
'posts as published_posts_count' => function ($query) {
$query->where('status', 'published');
}
])
->get();
}
}
Performance Analytics
class PerformanceAnalytics
{
public function getHighPerformingContent($days = 30)
{
$cutoffDate = now()->subDays($days);
// Find posts with high engagement (comments with likes and replies)
return Post::has('comments.likes', '>=', 5)
->has('comments.replies', '>=', 2)
->whereHas('comments', function ($query) use ($cutoffDate) {
$query->where('created_at', '>=', $cutoffDate);
})
->withCount([
'comments as recent_comments_count' => function ($query) use ($cutoffDate) {
$query->where('created_at', '>=', $cutoffDate);
},
'comments.likes as total_comment_likes_count'
])
->orderBy('total_comment_likes_count', 'desc')
->get();
}
public function getTopContributors()
{
return User::has('posts.comments.replies')
->has('comments.likes', '>=', 50)
->whereHas('posts', function ($query) {
$query->where('status', 'published')
->where('created_at', '>=', now()->subMonths(6));
})
->withCount([
'posts as published_posts_count' => function ($query) {
$query->where('status', 'published');
},
'comments as total_comments_count',
'comments.likes as comments_likes_received'
])
->orderBy('comments_likes_received', 'desc')
->get();
}
}
Counting with Deep Relationships
Use withCount()
to get numerical data about deep relationships:
// Get authors with counts of their books' ratings
$authors = Author::withCount([
'books',
'books.ratings as total_ratings',
'books.ratings as high_ratings' => function ($query) {
$query->where('rating', '>=', 4);
}
])->get();
// Get products with complex sales metrics
$products = Product::withCount([
'orderItems as times_ordered',
'orderItems.order as completed_orders' => function ($query) {
$query->where('status', 'completed');
},
'reviews as total_reviews',
'reviews as positive_reviews' => function ($query) {
$query->where('rating', '>=', 4);
}
])->get();
// Calculate average ratings through deep relationships
$productsWithRatings = Product::withAvg('reviews', 'rating')
->has('reviews', '>=', 5)
->orderBy('reviews_avg_rating', 'desc')
->get();
Performance Considerations
Optimizing Deep Relationship Queries
class OptimizedQueries
{
public function efficientDeepQuery()
{
// Instead of multiple has() calls, use exists subqueries
return Author::whereExists(function ($query) {
$query->select(DB::raw(1))
->from('books')
->whereColumn('books.author_id', 'authors.id')
->whereExists(function ($subQuery) {
$subQuery->select(DB::raw(1))
->from('ratings')
->whereColumn('ratings.book_id', 'books.id');
});
})->get();
}
public function optimizedWithIndices()
{
// Ensure proper indexing for deep relationship queries
return User::has('posts.comments.likes', '>=', 10)
->whereHas('posts', function ($query) {
$query->where('created_at', '>=', now()->subMonth())
->where('status', 'published');
})
->limit(100) // Always limit deep queries
->get();
}
}
Database Indexing Strategy
// Migration for optimized deep relationship queries
Schema::table('comments', function (Blueprint $table) {
// Composite index for deep relationship traversal
$table->index(['post_id', 'created_at', 'approved']);
$table->index(['user_id', 'created_at']);
});
Schema::table('comment_likes', function (Blueprint $table) {
// Index for counting likes in deep queries
$table->index(['comment_id', 'created_at']);
$table->index(['user_id', 'comment_id']); // Unique constraint as index
});
Schema::table('posts', function (Blueprint $table) {
// Index for post status and date filtering
$table->index(['user_id', 'status', 'created_at']);
$table->index(['status', 'created_at']);
});
Best Practices
1. Use Specific Relationships
Be explicit about the relationships you're querying:
// Good: Clear and specific
$users = User::has('orders.items.product.reviews', '>=', 5)->get();
// Better: Add conditions to be more specific
$users = User::whereHas('orders.items.product', function ($query) {
$query->whereHas('reviews', function ($reviewQuery) {
$reviewQuery->where('rating', '>=', 4)
->where('created_at', '>=', now()->subMonths(6));
});
})->get();
2. Limit Results
Always consider limiting results for deep relationship queries:
$topUsers = User::has('posts.comments.likes', '>=', 100)
->withCount('posts.comments.likes as total_likes')
->orderBy('total_likes', 'desc')
->limit(50) // Prevent performance issues
->get();
3. Monitor Query Performance
class DeepQueryAnalyzer
{
public function analyzeQuery($query)
{
DB::enableQueryLog();
$results = $query->get();
$queries = DB::getQueryLog();
foreach ($queries as $query) {
Log::info('Deep Query Analysis', [
'sql' => $query['query'],
'bindings' => $query['bindings'],
'time' => $query['time']
]);
}
return $results;
}
}
Conclusion
Deep relationship queries with Eloquent's has()
method provide a powerful way to filter data based on complex nested conditions. By mastering these techniques, you can:
- Simplify complex filtering without writing raw SQL
- Maintain readable code that clearly expresses business logic
- Optimize performance through proper indexing and query structure
- Build sophisticated applications with complex data relationships
Remember to always consider performance implications, use proper indexing, and limit result sets when working with deep relationship queries in production applications.