laravel/framework

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

开放

#61,252 创建于 2026年8月20日

 (1 条评论) (0 个反应) (0 位负责人)PHP (11,951 个派生)batch import
help wanted

仓库指标

星标
 (34,875 个星标)
PR 合并指标
 (平均合并 1天 22小时) (30 天内合并 129 个 PR)

描述

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

贡献者指南