jpie 3.8.1 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/PERFORMANCE_BASELINE.md +219 -19
- data/lib/json_api/controllers/concerns/resource_actions/include_preloading.rb +40 -9
- data/lib/json_api/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d78efd83c17a8d1e0b9c7e13966c083de82d95d92463dd5b945a4478d981c30
|
|
4
|
+
data.tar.gz: 71594c1b5687a4d20268d79b4e83e3760d399f3467029b8f0787329b6cf599fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1bdbd01c0c181b59be183a8d72decb7d3e13f254c92cd477285f69a909953d4bfe58f6dff6001ddbc437036888e30b6ab9e921675fd65a70003206f714f37ac
|
|
7
|
+
data.tar.gz: 6bdadef777e82f021a67b820e77e61a7c2051dbb5cddc037447b0c42c26c3142cc738f8a6aa864ef26b2cfb19a3a0f5e6f6a42afd5ec483b1049bd15d24acf52
|
data/Gemfile.lock
CHANGED
data/PERFORMANCE_BASELINE.md
CHANGED
|
@@ -5,9 +5,12 @@ problem: controller actions with large `include=` trees fire hundreds to
|
|
|
5
5
|
thousands of SQL queries and allocate hundreds of MB per request. Production
|
|
6
6
|
examples: `/workstreams` with 25 include paths (~300 queries at page 10, up to
|
|
7
7
|
`page[size]=100`), `/policies` with 17 include paths at `page[size]=1` (kills
|
|
8
|
-
the app on AI-conversation data; one index request allocated ~600MB),
|
|
8
|
+
the app on AI-conversation data; one index request allocated ~600MB),
|
|
9
9
|
`/evidence_checks` with a 45-id batch filter and files at depth 3 (~1,250
|
|
10
|
-
queries)
|
|
10
|
+
queries), `/incidents` with 18 include paths at page 20 (577 ActiveRecord
|
|
11
|
+
calls, 2,794ms), and `/suggested_risk_scenarios` with only two include paths
|
|
12
|
+
(541 ActiveRecord calls, 2,059ms). The last two fail for opposite reasons; see
|
|
13
|
+
"Two shapes, two causes" below.
|
|
11
14
|
|
|
12
15
|
The numbers come from the benchmark suite in `spec/benchmarks/`. The evaluation
|
|
13
16
|
behind them ran 32 independent code-path analyses; a second wave verified the
|
|
@@ -26,15 +29,19 @@ shapes, wall time, allocated/retained bytes, allocation sites, RSS). Diff that
|
|
|
26
29
|
file against this baseline after any optimization. `DEBUG_QUERIES=1` also dumps
|
|
27
30
|
raw SQL logs to `tmp/benchmarks/query_logs/`.
|
|
28
31
|
|
|
29
|
-
The suite seeds a stand-in domain (`Assignment`, `Gadget`, `
|
|
30
|
-
and others) that copies the production include-tree topology: wide +
|
|
31
|
-
polymorphic hops, shared prefixes, STI-free but otherwise faithful.
|
|
32
|
-
`assignments_full` tree mirrors the production `/workstreams` request
|
|
32
|
+
The suite seeds a stand-in domain (`Assignment`, `Gadget`, `Fault`, `Edition`,
|
|
33
|
+
`Remark`, and others) that copies the production include-tree topology: wide +
|
|
34
|
+
deep trees, polymorphic hops, shared prefixes, STI-free but otherwise faithful.
|
|
35
|
+
The `assignments_full` tree mirrors the production `/workstreams` request
|
|
33
36
|
path-for-path; `assignments_single_conversation` mirrors the `/policies` killer
|
|
34
37
|
(fat document bodies, a 600-remark conversation, ActiveStorage files, per-remark
|
|
35
38
|
children); `gadgets_evidence_shape` mirrors `/evidence_checks` (a 45-id `id_in`
|
|
36
39
|
batch, two branches converging on ActiveStorage files, polymorphic leaves at
|
|
37
|
-
depth 2-3)
|
|
40
|
+
depth 2-3); `faults_index` mirrors `/incidents` (link joins to depth-3 gadget
|
|
41
|
+
and appliance targets whose resources carry resource-default preloads and
|
|
42
|
+
per-record meta, plus a full discussion thread); `suggestions_polymorphic`
|
|
43
|
+
mirrors `/suggested_risk_scenarios` (two depth-1 paths, one of them
|
|
44
|
+
polymorphic, onto target resources that under-declare their preloads).
|
|
38
45
|
|
|
39
46
|
## Baseline (large dataset, sqlite, 2026-08-14)
|
|
40
47
|
|
|
@@ -98,18 +105,55 @@ active — the domain shape alone is not the problem.
|
|
|
98
105
|
`{files_attachments: {blob: {}}}` in the preload hash and reads blobs
|
|
99
106
|
through the loaded associations. See "Optimizations landed" below.
|
|
100
107
|
|
|
101
|
-
4. **
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
4. **Resource-default preloads reached depth 1 only — FIXED** (see
|
|
109
|
+
"Optimizations landed" #3). Two separate passes load data, and they did not
|
|
110
|
+
cover the same ground:
|
|
111
|
+
- `scope_with_includes` preloads the include tree at **every** depth. It
|
|
112
|
+
builds one nested hash from all paths, so a 4-level path costs 4 queries,
|
|
113
|
+
not 4 per record. Control: `users_deep_include` serializes 400 records in
|
|
114
|
+
3 queries; `assignments_single_conversation` serializes 2,191 records at
|
|
115
|
+
depth 4 in 22 queries.
|
|
116
|
+
- `preload_included_resource_associations` applies each resource's own
|
|
117
|
+
`records` preloads to include targets. It used to read
|
|
118
|
+
`include_path.split(".").first`, so only **depth-1** targets got them.
|
|
119
|
+
|
|
120
|
+
A resource at depth 2 or deeper therefore serialized with its own
|
|
121
|
+
associations unloaded. Every `meta` method that branches on `loaded?` took
|
|
122
|
+
the fallback branch and queried per record. Production:
|
|
123
|
+
`SelectedControlResource.records` preloads `evidence_checks`,
|
|
124
|
+
`selected_control_checks`, and `framework_criteria`, but `/incidents`
|
|
125
|
+
reaches `SelectedControl` at depth 2 through
|
|
126
|
+
`related_controls.selected_control`, so none of the three apply.
|
|
127
|
+
Anchor: `include_preloading.rb` (`preload_included_resource_associations`).
|
|
128
|
+
Baseline signature: 390 of 415 queries in `faults_index` are per-record;
|
|
129
|
+
40× draft-edition and 40× published-edition lookups in `assignments_full`.
|
|
130
|
+
|
|
131
|
+
The depth-1 pass itself is sound, including through a polymorphic hop. Probe:
|
|
132
|
+
`GET /assignments?include=subject` (polymorphic to `Gadget`) batches all five
|
|
133
|
+
of `GadgetResource`'s declared preloads into one query each, and the count
|
|
134
|
+
stays flat as records grow. Regression spec:
|
|
135
|
+
`polymorphic_resource_preload_spec.rb`. Read the two facts together before
|
|
136
|
+
blaming jpie for an N+1: at depth 1 a declared preload is applied, so a
|
|
137
|
+
per-record query there means the resource never declared it.
|
|
138
|
+
|
|
139
|
+
5. **Resources that never declare the preloads their own reads need.** A
|
|
140
|
+
`meta` method or attribute that walks an association costs one query per
|
|
141
|
+
serialized record when the resource's `records` scope does not preload it.
|
|
142
|
+
jpie cannot infer the need; nothing in the include tree names the
|
|
143
|
+
association. This is an application fault, not a library limit, and it is
|
|
144
|
+
the whole cost of a shallow request. Production `/suggested_risk_scenarios`
|
|
145
|
+
sends two depth-1 paths and still runs 541 ActiveRecord calls:
|
|
146
|
+
`RiskScenarioTemplateResource#meta` walks `inherent_risk_template.category`
|
|
147
|
+
with no `records` override (2 queries per template);
|
|
148
|
+
`SelectedControlResource#baseline` reads `control_template`, which its
|
|
149
|
+
preload list omits; `ComplianceScopeableResource` exposes `in_scope` and
|
|
150
|
+
`in_scope_justification`, which probe and then load
|
|
151
|
+
`resource_framework_scopes`, and no resource preloads them.
|
|
152
|
+
Baseline signature: `suggestions_polymorphic` runs one `baseline_estimates`
|
|
153
|
+
and one `estimate_categories` query per serialized repair plan, while every
|
|
154
|
+
association a resource does declare loads in a single batched query.
|
|
155
|
+
|
|
156
|
+
6. **Virtual sort disables everything.** A virtual (resource-method) sort field
|
|
113
157
|
materializes the whole filtered table as an Array before pagination, and
|
|
114
158
|
`scope_with_includes` bails on Arrays, so nothing preloads. Probe: 10 users,
|
|
115
159
|
`include=posts.comments`: 3 queries with a column sort, 41 with a virtual
|
|
@@ -259,3 +303,159 @@ Cumulative against the original baseline: the /policies shape is down from
|
|
|
259
303
|
1,728 queries to 22; the /evidence_checks shape from 2,550 to 146; page 100
|
|
260
304
|
from 5,102 to 1,233. The remaining residue is mostly mechanism 4 (per-record
|
|
261
305
|
meta lookups: 120 of the 153 queries in assignments_full).
|
|
306
|
+
|
|
307
|
+
### 3. Apply resource-default preloads at every depth (mechanism 4)
|
|
308
|
+
|
|
309
|
+
`preload_included_resource_associations` read only the first segment of each
|
|
310
|
+
include path, so a resource reached at depth 2 or deeper never received the
|
|
311
|
+
preloads its own `records` scope declares. It now nests the paths into one tree
|
|
312
|
+
and walks it, applying each target class's preloads at every hop. Shared
|
|
313
|
+
prefixes collapse, so a prefix is visited once however many paths cross it, and
|
|
314
|
+
a polymorphic hop continues once per class present in the loaded targets.
|
|
315
|
+
|
|
316
|
+
The walk costs no association queries: every hop reads targets that
|
|
317
|
+
`scope_with_includes` already loaded. Pure batching — included counts are
|
|
318
|
+
identical, scenario for scenario, and the whole suite passes unchanged.
|
|
319
|
+
Regression spec: `deep_resource_preload_spec.rb` pins a depth-2 target at a
|
|
320
|
+
flat query count (36 now, 141 before).
|
|
321
|
+
|
|
322
|
+
Large dataset, before and after:
|
|
323
|
+
|
|
324
|
+
| Scenario | Queries before | Queries after | Alloc MB before | Alloc MB after |
|
|
325
|
+
|---|---|---|---|---|
|
|
326
|
+
| faults_index | 415 | 81 (−80%) | 117.0 | 114.4 |
|
|
327
|
+
| assignments_full | 292 | 183 (−37%) | 83.7 | 82.4 |
|
|
328
|
+
| assignments_full + authz | 315 | 206 (−35%) | 86.0 | 84.7 |
|
|
329
|
+
| gadgets_single | 87 | 34 (−61%) | 3.09 | 2.57 |
|
|
330
|
+
| assignments_page_1 | 108 | 55 (−49%) | 40.2 | 39.5 |
|
|
331
|
+
| assignments_page_25 | 582 | 385 (−34%) | 156.3 | 154.1 |
|
|
332
|
+
| assignments_page_50 | 1,027 | 693 (−33%) | 280.8 | 277.2 |
|
|
333
|
+
| assignments_page_100 | 1,830 | 1,293 (−29%) | 526.2 | 520.5 |
|
|
334
|
+
| gadgets_evidence_shape (control) | 150 | 150 | 59.2 | 59.3 |
|
|
335
|
+
| assignments_single_conversation (control) | 22 | 22 | 79.7 | 80.0 |
|
|
336
|
+
| suggestions_polymorphic (control) | 84 | 84 | 9.37 | 9.37 |
|
|
337
|
+
| users_deep_include (control) | 3 | 3 | 9.83 | 9.84 |
|
|
338
|
+
|
|
339
|
+
The controls carry the argument. `suggestions_polymorphic` does not move,
|
|
340
|
+
because its targets already sat at depth 1 — that request's cost is
|
|
341
|
+
mechanism 5, in the application, and no jpie change touches it.
|
|
342
|
+
`gadgets_evidence_shape` and `users_deep_include` do not move either: neither
|
|
343
|
+
reaches a preload-declaring resource below depth 1.
|
|
344
|
+
|
|
345
|
+
Allocations barely move because the saving is query count, not object count.
|
|
346
|
+
Wall time on sqlite is too noisy between runs to attribute — the same scenario
|
|
347
|
+
varied by 2x across runs with identical query counts. Against production
|
|
348
|
+
postgres, where per-query latency is real, a 334-query drop on `faults_index`
|
|
349
|
+
is the whole point.
|
|
350
|
+
|
|
351
|
+
## Two shapes, two causes (2026-08-17)
|
|
352
|
+
|
|
353
|
+
Two staging requests look alike from the outside: both are index pages, both
|
|
354
|
+
spend about 2 seconds, both fire hundreds of ActiveRecord calls. They fail for
|
|
355
|
+
opposite reasons, and the fix for one does nothing for the other.
|
|
356
|
+
|
|
357
|
+
| | `/incidents` | `/suggested_risk_scenarios` |
|
|
358
|
+
|---|---|---|
|
|
359
|
+
| Include paths | 18, up to depth 3 | 2, both depth 1 |
|
|
360
|
+
| Where targets sit | depth 2-3 | depth 1 |
|
|
361
|
+
| Did jpie apply the target's preloads? | No — depth-1 pass missed them | Yes |
|
|
362
|
+
| Cause | jpie: mechanism 4 | application: mechanism 5 |
|
|
363
|
+
| Fix belongs in | jpie | kiln resources |
|
|
364
|
+
| Benchmark scenario | `faults_index` | `suggestions_polymorphic` |
|
|
365
|
+
| Status | fixed: 415 → 81 queries | open, needs kiln preloads |
|
|
366
|
+
|
|
367
|
+
### The /incidents shape: mechanism 4
|
|
368
|
+
|
|
369
|
+
`faults_index` reproduces the production `/incidents` request: 18 include
|
|
370
|
+
paths, `page[size]=20`, `sort=-created_at`, `filter[archived]=false`. Three
|
|
371
|
+
link branches reach `Gadget` and `Appliance` at depth 2-3. Both resources now
|
|
372
|
+
carry what production carries: a `records` scope that preloads what `meta`
|
|
373
|
+
reads, and `meta` methods that fall back to a query when nothing preloaded.
|
|
374
|
+
|
|
375
|
+
**This change moved the older scenarios too.** `Gadget` and `Appliance` sit in
|
|
376
|
+
the `assignments_full` tree as well, so that scenario went from 153 queries to
|
|
377
|
+
292 on the same code. No optimization regressed; the benchmark domain became
|
|
378
|
+
more faithful. Compare future runs against the table below, not against the
|
|
379
|
+
2026-08-14 recordings.
|
|
380
|
+
|
|
381
|
+
| Scenario | Primary | Included | Queries | ms (median) | Alloc MB |
|
|
382
|
+
|---|---|---|---|---|---|
|
|
383
|
+
| faults_index (page 20, 18 paths) | 20 | 4,290 | 415 | 2,430 | 117.0 |
|
|
384
|
+
| assignments_full (re-recorded) | 10 | 2,701 | 292 | 1,548 | 83.7 |
|
|
385
|
+
| assignments_full + authz (re-recorded) | 10 | 2,701 | 315 | 1,088 | 86.0 |
|
|
386
|
+
| gadgets_evidence_shape (re-recorded) | 45 | 1,656 | 150 | 767 | 59.2 |
|
|
387
|
+
| assignments_single_conversation (unchanged) | 1 | 2,191 | 22 | 432 | 79.7 |
|
|
388
|
+
| users_deep_include (control) | 20 | 400 | 3 | 95 | 9.8 |
|
|
389
|
+
|
|
390
|
+
Adding the suggestion domain changed none of these counts. Every scenario above
|
|
391
|
+
reproduced its query count exactly on the run that first measured
|
|
392
|
+
`suggestions_polymorphic`, which confirms the new tables and resources sit
|
|
393
|
+
outside the other include trees.
|
|
394
|
+
|
|
395
|
+
390 of the 415 `faults_index` queries are per-record. The include tree itself
|
|
396
|
+
costs about 25. The breakdown, against 45 gadgets and 25 appliances reached at
|
|
397
|
+
depth 2-3:
|
|
398
|
+
|
|
399
|
+
| Count | Query | Source | After the depth fix |
|
|
400
|
+
|---|---|---|---|
|
|
401
|
+
| 70 | `archivals` by owner | `records` preload, unreachable at depth 2 | 2 |
|
|
402
|
+
| 45 | `criteria.id` pluck | `criterium_ids` attribute | 1 |
|
|
403
|
+
| 45 + 45 | `inspections` order-limit, then exists | inspection history in `meta` | 1 |
|
|
404
|
+
| 45 + 45 | `standard_scopes` rows, then exists | scope verdict in `meta` | 1 |
|
|
405
|
+
| 45 | `gadget_checks` exists | checklist verdict in `meta` | 1 |
|
|
406
|
+
| 25 + 25 | `stream_entries` by stream | append-only read, no association | 50 |
|
|
407
|
+
|
|
408
|
+
Only the last row was beyond jpie's reach: a raw stream read has no association
|
|
409
|
+
for any preloader to batch. Everything above it collapsed to a handful of
|
|
410
|
+
batched queries once resource-default preloads applied at every depth, taking
|
|
411
|
+
the scenario from 415 queries to 81. The 50 stream reads are now the largest
|
|
412
|
+
single item, and they need a change in the application.
|
|
413
|
+
|
|
414
|
+
### The /suggested_risk_scenarios shape: mechanism 5
|
|
415
|
+
|
|
416
|
+
`suggestions_polymorphic` reproduces the production request:
|
|
417
|
+
`filter[account_level]=true`, `sort=-priority`, and two include paths —
|
|
418
|
+
`repair_plan` (plain) and `suggestable` (polymorphic, onto `Gadget` and
|
|
419
|
+
`Appliance`). `SuggestionResource.records` copies the production scope, with
|
|
420
|
+
the plain hop on `includes` and the polymorphic hop on `preload`.
|
|
421
|
+
|
|
422
|
+
Both targets sit at depth 1, so jpie applies their `records` preloads. The
|
|
423
|
+
split in the results is the whole point:
|
|
424
|
+
|
|
425
|
+
- `Gadget` declares its preloads. All five load in one batched query each,
|
|
426
|
+
however many gadgets the page holds.
|
|
427
|
+
- `RepairPlan` declares none, and its `meta` walks
|
|
428
|
+
`baseline_estimate.estimate_category`. That costs two queries per serialized
|
|
429
|
+
plan — the exact `Risk::InherentRiskTemplate` and `Risk::Category` bands in
|
|
430
|
+
the staging trace.
|
|
431
|
+
- `Appliance` reads an append-only stream, which has no association to preload
|
|
432
|
+
at any depth.
|
|
433
|
+
|
|
434
|
+
| Dataset | Primary | Included | Queries | ms (median) | Alloc MB |
|
|
435
|
+
|---|---|---|---|---|---|
|
|
436
|
+
| small | 5 | 10 | 22 | 14 | - |
|
|
437
|
+
| medium | 25 | 37 | 46 | 31 | 2.9 |
|
|
438
|
+
| large | 100 | 112 | 84 | 203 | 9.4 |
|
|
439
|
+
|
|
440
|
+
Queries grow far slower than the page, because the repair-plan pool caps at 12
|
|
441
|
+
and only 25 of the 100 suggestables are appliances. Production behaves the same
|
|
442
|
+
way: 18 suggestions reached 13 distinct templates. The large run splits like
|
|
443
|
+
this:
|
|
444
|
+
|
|
445
|
+
| Count | Query | Verdict |
|
|
446
|
+
|---|---|---|
|
|
447
|
+
| 25 + 25 | `stream_entries` by stream | unfixable by preloading |
|
|
448
|
+
| 12 + 12 | `baseline_estimates`, then `estimate_categories` | resource never declared it |
|
|
449
|
+
| 2 | `archivals` by owner type | correct: one per polymorphic class |
|
|
450
|
+
| 8 | primary, both target classes, plan, declared preloads | correct: batched |
|
|
451
|
+
|
|
452
|
+
Only 8 of the 84 queries are structural. Every association a resource declares
|
|
453
|
+
is batched, including through the polymorphic hop, and the 24 `RepairPlan`
|
|
454
|
+
queries are the shape a one-line preload removes.
|
|
455
|
+
|
|
456
|
+
No change to jpie removes the `RepairPlan` queries. The resource has to ask for
|
|
457
|
+
the preload. In production the three to fix are
|
|
458
|
+
`RiskScenarioTemplateResource` (add `inherent_risk_template: :category`),
|
|
459
|
+
`SelectedControlResource` (add `control_template` to its existing preload
|
|
460
|
+
list), and `ComplianceScopeableResource` (add `resource_framework_scopes`,
|
|
461
|
+
which alone accounts for 82 of the 541 calls).
|
|
@@ -8,10 +8,14 @@ module JSONAPI
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
10
|
def includes_to_hash(paths)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
filter_includable(includes_to_tree(paths), model_class)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Nests dotted include paths into one tree, collapsing shared prefixes.
|
|
15
|
+
def includes_to_tree(paths)
|
|
16
|
+
paths.each_with_object({}) do |path, tree|
|
|
17
|
+
path.split(".").reduce(tree) { |node, part| node[part.to_sym] ||= {} }
|
|
13
18
|
end
|
|
14
|
-
filter_includable(hash, model_class)
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
# Preloads associations from the include param so the serializer avoids N+1 queries
|
|
@@ -82,18 +86,45 @@ module JSONAPI
|
|
|
82
86
|
assoc.polymorphic? || (value.present? && hash_contains_polymorphic?(value, assoc.klass))
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
# Applies each included resource's own `records` preloads to the records
|
|
90
|
+
# of that resource's class. Walks the whole include tree: a resource
|
|
91
|
+
# reached at depth 3 gets its preloads just as one reached at depth 1.
|
|
92
|
+
#
|
|
93
|
+
# The tree collapses shared prefixes, so a prefix is visited once however
|
|
94
|
+
# many paths run through it. Walking costs no association queries — every
|
|
95
|
+
# hop reads targets that `scope_with_includes` already loaded. The only
|
|
96
|
+
# queries are the batched resource-default preloads themselves, each of
|
|
97
|
+
# which replaces one query per record during serialization.
|
|
85
98
|
def preload_included_resource_associations(resources, includes)
|
|
86
99
|
return if includes.empty? || resources.empty?
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
assoc_reflection = model_class.reflect_on_association(association_name)
|
|
91
|
-
next unless assoc_reflection
|
|
101
|
+
preload_resource_tree(resources, model_class, includes_to_tree(includes))
|
|
102
|
+
end
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
def preload_resource_tree(records, klass, tree)
|
|
105
|
+
tree.each do |association_name, subtree|
|
|
106
|
+
reflection = klass.reflect_on_association(association_name)
|
|
107
|
+
next unless reflection
|
|
108
|
+
|
|
109
|
+
targets = collect_include_targets(records, association_name)
|
|
94
110
|
next if targets.empty?
|
|
95
111
|
|
|
96
|
-
apply_preloads_for_targets(targets,
|
|
112
|
+
apply_preloads_for_targets(targets, reflection.polymorphic?)
|
|
113
|
+
next if subtree.empty?
|
|
114
|
+
|
|
115
|
+
descend_resource_tree(targets, reflection, subtree)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A polymorphic hop has no single target class, so continue once per
|
|
120
|
+
# class actually present in the loaded targets.
|
|
121
|
+
def descend_resource_tree(targets, reflection, subtree)
|
|
122
|
+
if reflection.polymorphic?
|
|
123
|
+
targets.group_by(&:class).each do |target_class, records|
|
|
124
|
+
preload_resource_tree(records, target_class, subtree)
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
preload_resource_tree(targets, reflection.klass, subtree)
|
|
97
128
|
end
|
|
98
129
|
end
|
|
99
130
|
|
data/lib/json_api/version.rb
CHANGED