DEV Community

Cover image for whereHas() vs whereRelation(): Readability Over Shortcuts
Ivan Mykhavko
Ivan Mykhavko

Posted on

whereHas() vs whereRelation(): Readability Over Shortcuts

Laravel devs love their shortcuts. Tighter syntax, less boilerplate it's satisfying to trim down a few lines. But let's be honest: just because code is shorter doesn't mean it's better. When you're working with a team and the specs are always shifting, clear code always wins.

I recently came across Laravel tip suggesting we should replace whereHas() with whereRelation() for cleaner code. The example went like this:

// BEFORE
$users = User::whereHas('profile', function ($query) {
    $query->where('is_verified', false);
})->get();

// AFTER
$users = User::whereRelation('profile', 'is_verified', false)->get();
Enter fullscreen mode Exit fullscreen mode

You can check the original post if you want the full breakdown.

Original post

Alright, here's where I land on this advice.

The tip isn't wrong, but presenting whereRelation() as a better alternative is misleading. It's just syntactic sugar. And honestly, in a real project, that surface-level "cleanliness" can backfire.

The Real Problem: Readability and Intent

Here's what I actually use in production:

$users = User::query()
    ->whereHas('profile', fn(Builder $query) => $query->where('is_verified', false))
    ->get();
Enter fullscreen mode Exit fullscreen mode

Why is this better? Because it communicates intent clearly.

When you see whereHas(), you immediately understand:

  • Relationship is being filtered
  • Logic lives inside that relationship scope
  • More conditions can be added naturally

Note: I'm using arrow functions here for conciseness, but that's a separate improvement, you could use them with either method. The key distinction is the method itself.

Where whereRelation() Falls Short

The problem with whereRelation() is that it obscures what's actually happening. It looks like a simple column filter, but under the hood it's executing a subquery against a related table.

->whereRelation('profile', 'is_verified', false)->get();
Enter fullscreen mode Exit fullscreen mode

This reads like filtering a direct column on users, not constraining a relationship. That's misleading.

And if you need more than one condition? With whereHas(), you just add them:

->whereHas('profile', fn(Builder $query) => $query
    ->where('is_verified', false)
    ->whereNotNull('phone')
    ->where('age', '>', 18)
)
Enter fullscreen mode Exit fullscreen mode

With whereRelation(), this becomes awkward or impossible. You'd need multiple chained calls or give up and switch back to whereHas() anyway.

When whereRelation() Is Actually Fine

I'm not totally against whereRelation(). It works fine for stuff like:

  • Quick admin reports
  • Throwaway scripts
  • Tiny filters that'll never get more complex

For example:

User::query()->whereRelation('profile', 'is_verified', true)->count();
Enter fullscreen mode Exit fullscreen mode

That's simple enough. But as soon as your logic gets even a little more complicated, just reach for whereHas(). It's clearer and won't confuse your future self or your teammates.

Performance Question

A lot of devs steer clear of whereHas() because someone told them it's slow. That's a different discussion entirely and usually the problem is missing indexes, or it's time to go with the query builder.
If you're filtering on profiles.is_verified and don't have an index, both whereHas() and whereRelation() will be slow, they generate nearly identical SQL.

But here's a practical issue: imagine you join a new project and get a ticket saying "the users endpoint is slow." What's your move? Search the codebase for relationship queries and check if proper indexes exist.

So you search for whereHas... and find nothing. Turns out the last dev used whereRelation() everywhere. Now you're hunting through method calls that don't look like relation filters at all. whereHas() is greppable and obvious. whereRelation() hides in plain sight.

The Consistency Problem

Here's what actually happens in real codebases:

You start simple. Maybe you reach for whereRelation():

User::query()->whereRelation('profile', 'is_verified', true)->get();
Enter fullscreen mode Exit fullscreen mode

But business logic always changes. Suddenly you need second condition, so you switch to whereHas():

User::query()->whereHas('profile', fn(Builder $query) => $query
    ->where('is_verified', true)
    ->whereNotNull('phone')
)->get();
Enter fullscreen mode Exit fullscreen mode

Now your codebase has both patterns. New folks don't know which one to use. Code reviews become inconsistent. You see whereRelation() in one file, whereHas() in another, and there's no real reason for it. Teams should agree on one default and deviate only with a reason.

If you just used whereHas() from the day one, this never happens. One style, consistent everywhere, and your code is ready when requirements get more complicated.

Laravel Internals: No Magic Here

Let's be real, whereRelation() is just a wrapper around whereHas(). It's not smarter, cleaner, efficent, or faster. It only saves you from writing a closure. If you don't believe me, check out the source code.

That's fine for trivial cases. But for real world app, queries with multiple conditions, maintainability concerns, or any chance of future growth, choosing whereHas() isn't old-fashioned it's more honest about what your code does.

My Rule of Thumb

  • One condition, zero chance it grows, quick script? Go with whereRelation().
  • Anything that matters: business logic, team projects, code you'll revisit use whereHas().

Don't try to save a few keystrokes. Write for the next developer (or yourself in six months) reading the code. whereRelation() is a nice shortcut, but don't mistake convenience for clarity. In most real scenarios, whereHas() with clean syntax wins for readability, scalability, and honest intent.

Author's Note

Thanks for sticking around!
Find me on dev.to, linkedin, or you can check out my work on github.

Notes from real-world Laravel.

Top comments (0)