How Laravel withWhereHas() Cleans Up Your Queries

How Laravel withWhereHas() Cleans Up Your Queries

A common scenario in Laravel is retrieving parent models based on some condition on their relationship and at the same time eager loading that same relationship. One such example might be to get all users who have active subscriptions and show those subscriptions.

The common approach here would be to use whereHas() and with() methods, but it requires writing down the same logic twice. Eloquent provides a helpful shortcut to this problem called withWhereHas().

The Repetitive Way: whereHas() + with()

Here is how developers typically write this query:

$users = User::whereHas('orders', function ($query) {
        $query->where('status', 'completed');
    })
    ->with(['orders' => function ($query) {
        $query->where('status', 'completed');
    }])
    ->get();

See duplication:

  • whereHas() performs an EXISTS subquery to get only those users who have at least one completed order.
  • with() loads only the completed orders on these users to avoid N+1 query.

You have written $query->where('status', 'completed') twice. So, if you need to add a condition to select only completed orders with a specific date or amount, you should edit both places.

The Cleaner Way: withWhereHas()

Laravel's withWhereHas() combines both actions into a single method call:

$users = User::withWhereHas('orders', function ($query) {
    $query->where('status', 'completed');
})->get();

Under the hood, Laravel runs the whereHas() scope on the parent users' table to filter records, and passes the same closure of constraints directly to the with() operation of the eager loading.

Thus, you get exactly the same SQL queries and results, but your code is reduced by half, and there is only one condition to maintain.

When Not to Use It

Use withWhereHas() only when the filter for the parent records matches the filter for the eager-loaded children. Keep them separate if:

  • You want all children loaded: You want users who have at least one completed order, but you want to eager load all orders (pending, cancelled, and completed) onto those users. In that case, you can use whereHas('orders', ...) combined with an unconstrained with('orders').
  • Different filters apply: You filter the parent by one column on the relation, but want to eager load rows filtered by another.

Summary

If you yourself write the same closures for whereHas() and with(), you should instead be using withWhereHas(). This avoids duplication and the possibility of only updating one of the two.

Thank you for reading this article 😊

For any query, do not hesitate to comment 💬


Previous Post Next Post

Contact Form