Skip to content

JsonApiResource loads relationships one model at a time, so ?include= on a collection costs a query per record #61252

Description

@Bosun18
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions