jaspercreel's avatar

Customizing query using 'with' eliminates results

Hi all,

Ran into a funny thing. I have a pretty robust query I am using to pull data for a report. Lots of joins and withs. I ran into an issue where a relationship of a relationship would disappear from the results when I tried to customize the query. For example, this works:

->with([
  'deliverables.timeEntries'
])

But this doesn't:

->with([
  'deliverables.timeEntries' => fn ($query) => $query->whereBetween('timeEntries.date', $range)
])

What's even more bizarre is that some calculations I was doing in the query did seem to be affected by the customization, and the data seemed accurate, but the relationship was empty.

Any ideas?

0 likes
2 replies
LaryAI's avatar
Level 58

This is a common "gotcha" when customizing nested eager loads in Eloquent. The issue is that when you use a closure to constrain a nested relationship (like deliverables.timeEntries), Eloquent will only include related models that match the constraint. If a parent deliverable has no timeEntries in the given $range, the timeEntries relationship will be an empty collection for that deliverable.

Why does this happen?
When you use a constraint on a nested relationship, Eloquent does not filter out the parent models (deliverables), but it will only eager load the related timeEntries that match the constraint. If none match, the relationship will be empty.

Example

Suppose you have:

->with([
    'deliverables.timeEntries' => fn ($query) => $query->whereBetween('timeEntries.date', $range)
])

If a deliverable has no timeEntries in $range, then $deliverable->timeEntries will be an empty collection.

What if you want to filter out parent models with no related children?

If you want to only include deliverables that have at least one timeEntry in the range, you need to use whereHas:

->whereHas('deliverables.timeEntries', function ($query) use ($range) {
    $query->whereBetween('timeEntries.date', $range);
})
->with([
    'deliverables' => function ($q) use ($range) {
        $q->whereHas('timeEntries', function ($query) use ($range) {
            $query->whereBetween('timeEntries.date', $range);
        });
    },
    'deliverables.timeEntries' => function ($query) use ($range) {
        $query->whereBetween('timeEntries.date', $range);
    }
])

Summary

  • Using a closure with with() on a nested relationship does not filter out the parent; it only filters the related models.
  • If you want to filter out parents with no matching children, use whereHas() in addition to your eager loading.

TL;DR

If you want to only get parents with children in the range, use both whereHas and a constrained with:

->whereHas('deliverables.timeEntries', fn ($q) => $q->whereBetween('timeEntries.date', $range))
->with([
    'deliverables.timeEntries' => fn ($q) => $q->whereBetween('timeEntries.date', $range)
])

If you just want to eager load the relationship, but allow it to be empty, your original code is correct.

Let me know if you need a more specific example based on your models!

jlrdw's avatar

What is the generated sql for this?

Please or to participate in this conversation.