laravel/framework

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

Ouverte

#61 252 ouverte le 20 août 2026

 (1 commentaire) (0 réaction) (0 personne assignée)PHP (11 951 forks)batch import
help wanted

Métriques du dépôt

Stars
 (34 875 étoiles)
Métriques de merge PR
 (Merge moyen 1j 22h) (129 PRs mergées en 30 j)

Description

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

Guide contributeur