Skip to content

Commit 163a330

Browse files
fix: scope L2 cache removal to the OptionSet.options collection only
Keep the entity-level caches on Option and OptionSet (read-write) -- only the collection-level cache on OptionSet.options was actually driving the N+1: a collection-cache hit (cached id list) combined with an entity-cache miss on one of those ids is what makes Hibernate resolve ids one at a time instead of just re-running the collection's own `WHERE optionsetid = ?` query. Removing the entity caches too was conflated with a separate, unvalidated change (a service-layer Caffeine cache to compensate for lost keyed lookups) that we decided not to pursue. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3cbca9a commit 163a330

2 files changed

Lines changed: 10 additions & 12 deletions

File tree

dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/option/hibernate/Option.hbm.xml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,7 @@
88
<hibernate-mapping>
99
<class name="org.hisp.dhis.option.Option" table="optionvalue">
1010

11-
<!-- L2 cache intentionally omitted: tracker preheat's query-driven bulk loads
12-
unconditionally re-put every row on every fetch with no freshness check, and that put
13-
path contends on Ehcache's per-key lock under concurrent imports. Live evidence
14-
(ehcache_puts_total/ehcache_gets_total{cache="Option"}) showed put:get ratio climbing
15-
from ~0.06 to ~0.19 as concurrent load ramped up, i.e. puts scaling faster than genuine
16-
lookups, not just proportional volume growth. Also, on an L2 collection-cache hit for
17-
OptionSet.options combined with an entity-cache miss for the individual Option rows,
18-
Hibernate resolves each id one at a time instead of re-running the cheap
19-
`WHERE optionsetid = ?` collection query, which is what produced the N+1; with no L2
20-
cache configured here, that path no longer exists. -->
11+
<cache usage="read-write" />
2112

2213
<id name="id" column="optionvalueid">
2314
<generator class="native" />

dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/option/hibernate/OptionSet.hbm.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
<hibernate-mapping>
99
<class name="org.hisp.dhis.option.OptionSet" table="optionset">
1010

11-
<!-- L2 cache intentionally omitted; see Option.hbm.xml for the rationale, same mechanism
12-
applies here. -->
11+
<cache usage="read-write" />
1312

1413
<id name="id" column="optionsetid">
1514
<generator class="native" />
@@ -32,6 +31,14 @@
3231

3332
<property name="version" />
3433

34+
<!-- Collection L2 cache intentionally omitted: a cache hit here (a cached list of Option
35+
ids) combined with an entity-cache miss on one of those ids makes Hibernate resolve each
36+
id one at a time instead of just re-running this bag's own query, which is what produced
37+
an observed N+1 (thousands of individual `SELECT ... FROM optionvalue WHERE
38+
optionvalueid = ?`) during tracker import. Without a cached id list to reconcile against
39+
the entity cache, this collection always loads via its own single
40+
`WHERE optionsetid = ?` query instead, which is fast (single query, indexed FK). Entity-
41+
level caching for Option/OptionSet themselves is unaffected and still applies. -->
3542
<bag name="options" cascade="all" order-by="sort_order">
3643
<key column="optionsetid" foreign-key="fk_optionsetmembers_optionsetid" />
3744
<one-to-many class="org.hisp.dhis.option.Option" />

0 commit comments

Comments
 (0)