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:

  1. Author has many books
  2. Each Book has many ratings

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.