- Laravel version: 13.24.0
- PHP version: 8.5.9
- Database driver & version: SQLite 3 in memory, through Testbench 11.1
I was counting queries on an ordinary index endpoint and ended up with 101
queries for 100 records with ?include=comments. The same listing built as
Post::with('comments')->get() gives 2. Before I read anything into that I'd
like to know whether the loading is meant to be the developer's job here.
Where it comes from, as far as I can tell. ResolvesJsonApiElements::
compileResourceRelationships() calls $this->resource->loadMissing(...), and
$this->resource in that method is always a single model, because both callers
return early unless it is one. AnonymousResourceCollection::toAttributes()
then maps resolveResourceData() over the collection item by item, so every
item runs its own loadMissing against its own model.
The nested level behaves differently, and that's why I'm asking rather than
just eager loading and moving on. With 10 posts and
?include=comments.author I get 21 queries: one for the posts, ten for the
comments, ten for the authors. But the author lookups are batched inside each
post, select * from "authors" where "authors"."id" in (1, 2, 3), because the
second loadMissing runs on $relatedModels, which is already a collection. So
the batching is there. It just never reaches the outer level.
What I measured, all on 13.24.0:
| request |
queries |
| single post, ?include=comments |
2 |
| 10 posts, no eager loading |
11 |
| 10 posts, with('comments') |
2 |
| 10 posts, ?include=comments.author |
21 |
| 10 posts, with('comments.author') |
3 |
| 100 posts, no eager loading |
101 |
| 100 posts, with('comments') |
2 |
I'm leaving timings out on purpose. This is sqlite in memory, so the numbers
there say more about serialisation than about the database, and the counts
are what matters.
The case for "working as intended", which I can make myself: the call is
loadMissing and not load, which reads like topping up whatever the developer
forgot, not like owning the loading, and the documentation does show eager
loading in the controller. If that's the answer then this is a documentation
note at most and I'll drop it. It just seems like a lot to leave to a query
log on an endpoint whose output is completely correct.
Two things I did not check, in case they matter: whether the same happens
through an explicit ResourceCollection class and not the anonymous one, and
whether chaperone() changes any of it. Both looked like separate paths
when I skimmed them, so I might be missing something.
Steps to reproduce
Post hasMany Comment, Comment belongsTo Author. Resources generated with
make:resource --json-api, PostResource declaring $relationships = ['comments'],
CommentResource declaring $relationships = ['author'].
Route returning PostResource::collection(Post::all()), 100 posts with three
comments each, then GET /posts?include=comments with
Accept: application/vnd.api+json and DB::listen() counting.