Skip to content

whereHas

Filter parent rows by the existence of related rows that match a callback. Compiles to WHERE EXISTS (SELECT ...) against the related table — no join, no eager-load.

Signature

ts
FedacoBuilder<T>.whereHas(
  relation: string,
  callback?: (q: FedacoBuilder<R>) => void,
  operator?: string,
  count?: number,
): this

Parameters

NameDescription
relationName of the relation method on the model.
callbackConstraints applied to the related query. Pass nothing for "any related row exists".
operatorComparison operator on the count: >, >=, <, <=, =, !=. Default >=.
countThreshold for the count comparison. Default 1.

Real-World Use Cases

1. Parents with at least one matching child

ts
const usersWithPublishedPosts = await User.createQuery()
  .whereHas('posts', (q) => {
    q.where('published', true);
  })
  .get();

Compiles to roughly:

sql
SELECT * FROM users WHERE EXISTS (
  SELECT * FROM posts WHERE posts.user_id = users.id AND published = ?
)

2. Existence without constraints

ts
const writers = await User.createQuery().whereHas('posts').get();

Equivalent to has('posts')whereHas with no callback degenerates to plain existence.

3. Count comparisons

ts
// Users who wrote 5+ posts
const prolific = await User.createQuery()
  .whereHas('posts', (q) => q.where('published', true), '>=', 5)
  .get();

4. Nested whereHas across two levels

ts
const users = await User.createQuery()
  .whereHas('posts', (q) => {
    q.whereHas('comments', (cq) => {
      cq.where('flagged', true);
    });
  })
  .get();

Each level fires its own EXISTS subquery.

5. Excluding parents — whereDoesntHave

The negation:

ts
const usersWithoutPosts = await User.createQuery()
  .whereDoesntHave('posts')
  .get();

6. Combined with eager loading

ts
const users = await User.createQuery()
  .whereHas('posts', (q) => q.where('published', true))
  .with('posts')        // eager-load *all* posts (including drafts)
  .get();

The constraint in whereHas filters parents; it doesn't restrict which posts get loaded by with. To load only published posts, constrain inside with:

ts
.with('posts', (q) => q.where('published', true))

whereHas vs has vs with

ToolEffect on parentsEffect on relation rows
withnone — all parents returnedloads them onto each parent
hasfilters parents to those with related rowsnone — no rows hydrated
whereHasfilters with custom constraintsnone
with + whereHasfilters parents and loads relationboth — independent constraint sets

Common Pitfalls

  • Constraints inside whereHas don't affect what with loads. They're separate concerns — keep both if you need both.
  • whereHas runs subqueries; large datasets may benefit from a join + group instead. For polymorphic or deep nesting, profile before assuming whereHas is fastest.
  • Dotted relation names ('posts.comments') — supported, each segment becomes its own EXISTS.

See Also

Released under the MIT License.