Skip to content

Commit 5f22456

Browse files
authored
fix: declare synchronized entity classes on native writes DHIS2-21963 (#24803) (#24845)
* fix: declare synchronized entity classes on native writes Native queries that run executeUpdate() without declaring which tables they touch make Hibernate conservatively evict EVERY L2 entity region, not just the affected one. NativeSQLQueryPlan.performExecuteUpdate calls coordinateSharedCacheCleanup with getCustomQuery().getQuerySpaces(). SQLCustomQuery leaves that set empty unless addSynchronizedEntityClass/QuerySpace was called, and BulkOperationCleanupAction.affectedEntity treats an empty set as "affects everything", so every persister that can write to cache gets an EntityCleanup. EntityCleanup.release() calls unlockRegion(), which is evictAll() -> evictData() on the whole region. Note AbstractReadWriteAccess.removeAll IS a no-op under read-write, which is why this is easy to miss; the eviction comes from unlockRegion, not removeAll. Both stores here use stateless sessions, where isEventSource() is false, so the cleanup runs immediately rather than being deferred to the ActionQueue. Draft to confirm the mechanism: measured on a Sierra Leone instance, a single data value write with a new period emptied the whole DataElement region (0 hits / 3037 logical misses on the next read), while the same write with an existing period left it fully warm. No eviction or removal counter moves and nothing is logged, so this is invisible without instrumenting Hibernate. HousekeepingJob runs every 20s by default and drives the jobconfiguration writes here, so on a stock instance every cached entity region is dropped on that cadence. * fix: declare query spaces on the remaining native writes * fix: synchronize entities not join tables so cached collections are evicted * docs: explain query spaces in nativeSynchronizedQuery javadoc * refactor: add stateless session variant of nativeSynchronizedQuery * refactor: use nativeSynchronizedQuery in complete data set registration store * refactor: run audit trigger DDL via jdbcTemplate * fix: declare element entities for native writes to collection tables A write to a collection table needs the collection's element entity declared, not the owning entity. An entity's query spaces are its own table(s) only (SingleTableEntityPersister), never the tables of the collections it owns, and BulkOperationCleanupAction resolves collection regions via getCollectionRolesByEntityParticipant, which is keyed by the collection's element entity. So synchronizing on the owner leaves its cached collection stale: * categories_categoryoptions needs CategoryOption, not Category * users_catdimensionconstraints needs Category, not User Confirmed against a running instance. With a category collection warm and in steady state (+1 hit, 0 misses per read), a merge of an unrelated pair of categories left a bystander category's cached collection reloading cold (+2 misses, +1 put), while an unrelated write left it fully warm. Also correct the comments in the category combo and data set stores, which reached the right regions but described the wrong reason, and expand the nativeSynchronizedQuery javadoc with the element-entity rule.
1 parent 7b4cd04 commit 5f22456

15 files changed

Lines changed: 104 additions & 47 deletions

File tree

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/analytics/hibernate/HibernateCategoryDimensionStore.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ public int updateCatDimensionCategoryRefs(
7979
set categoryid = :targetCategoryId
8080
where cd.categoryid in :sourceCategoryIds
8181
""";
82-
return getSession()
83-
.createNativeQuery(sql)
82+
return nativeSynchronizedQuery(sql)
8483
.setParameter("targetCategoryId", targetCategoryId)
8584
.setParameter("sourceCategoryIds", sourceCategoryIds)
8685
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(5000))

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryComboStore.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.List;
3838
import java.util.Set;
3939
import org.hibernate.LockOptions;
40+
import org.hisp.dhis.category.Category;
4041
import org.hisp.dhis.category.CategoryCombo;
4142
import org.hisp.dhis.category.CategoryComboStore;
4243
import org.hisp.dhis.category.CategoryOptionCombo;
@@ -97,8 +98,11 @@ public int updateCatComboCategoryRefs(Set<Long> sourceCategoryIds, long targetCa
9798
set categoryid = :targetCategoryId
9899
where cc_c.categoryid in :sourceCategoryIds
99100
""";
100-
return getSession()
101-
.createNativeQuery(sql)
101+
return nativeSynchronizedQuery(sql)
102+
// categorycombos_categories backs both the cached CategoryCombo.categories and
103+
// Category.categoryCombos collections. Collection regions are keyed by the collection's
104+
// element entity, so naming both entities is what reaches both regions.
105+
.addSynchronizedEntityClass(Category.class)
102106
.setParameter("targetCategoryId", targetCategoryId)
103107
.setParameter("sourceCategoryIds", sourceCategoryIds)
104108
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(5000))

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryStore.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import javax.annotation.Nonnull;
4040
import org.hibernate.LockOptions;
4141
import org.hisp.dhis.category.Category;
42+
import org.hisp.dhis.category.CategoryOption;
4243
import org.hisp.dhis.category.CategoryStore;
4344
import org.hisp.dhis.common.DataDimensionType;
4445
import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore;
@@ -118,8 +119,11 @@ public int removeCatOptionCategoryRefs(Set<Long> sourceCategoryIds) {
118119
delete from categories_categoryoptions c_co
119120
where c_co.categoryid in :sourceCategoryIds
120121
""";
121-
return getSession()
122-
.createNativeQuery(sql)
122+
return nativeSynchronizedQuery(sql)
123+
// categories_categoryoptions is the Category.categoryOptions collection table. Collection
124+
// regions are keyed by the collection's element entity, so CategoryOption is what reaches
125+
// that region, not the owning Category.
126+
.addSynchronizedEntityClass(CategoryOption.class)
123127
.setParameter("sourceCategoryIds", sourceCategoryIds)
124128
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(5000))
125129
.executeUpdate();

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalWorkflowStore.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ public int updateCategoryComboRefs(Set<Long> sourceCategoryComboIds, long target
6666
set categorycomboid = :targetCategoryComboId
6767
where categorycomboid in :sourceCategoryComboIds
6868
""";
69-
return getSession()
70-
.createNativeQuery(sql)
69+
return nativeSynchronizedQuery(sql)
7170
.setParameter("targetCategoryComboId", targetCategoryComboId)
7271
.setParameter("sourceCategoryComboIds", sourceCategoryComboIds)
7372
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(5000))

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateCompleteDataSetRegistrationStore.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ public void deleteCompleteDataSetRegistration(
135135
AND c.sourceid = (SELECT organisationunitid FROM organisationunit ou WHERE ou.uid = :ou)
136136
AND c.attributeoptioncomboid = (SELECT categoryoptioncomboid FROM categoryoptioncombo aoc WHERE aoc.uid = :aoc)
137137
""";
138-
entityManager
139-
.createNativeQuery(sql)
138+
nativeSynchronizedQuery(sql)
140139
.setParameter("ds", dataSet.getValue())
141140
.setParameter("pe", period.getIsoDate())
142141
.setParameter("ou", orgUnit.getValue())

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateDataSetStore.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,11 @@ public int updateDataSetElementCategoryComboRefs(
171171
set categorycomboid = :targetCategoryComboId
172172
where categorycomboid in :sourceCategoryComboIds
173173
""";
174-
return getSession()
175-
.createNativeQuery(sql)
174+
return nativeSynchronizedQuery(sql)
175+
// datasetelement is DataSetElement's own table. It is also the DataSet.dataSetElements
176+
// collection table, and since collection regions are keyed by the collection's element
177+
// entity, naming DataSetElement reaches that cached collection too.
178+
.addSynchronizedEntityClass(DataSetElement.class)
176179
.setParameter("targetCategoryComboId", targetCategoryComboId)
177180
.setParameter("sourceCategoryComboIds", sourceCategoryComboIds)
178181
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(5000))

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void deleteByOrgUnit(@Nonnull UID orgUnit) {
7373
DELETE FROM datavalueaudit dva
7474
WHERE dva.organisationunitid = (SELECT ou.organisationunitid FROM organisationunit ou WHERE ou.uid = :ou)""";
7575

76-
entityManager.createNativeQuery(sql).setParameter("ou", orgUnit.getValue()).executeUpdate();
76+
nativeSynchronizedQuery(sql).setParameter("ou", orgUnit.getValue()).executeUpdate();
7777
}
7878

7979
@Override
@@ -83,7 +83,7 @@ public void deleteByDataElement(@Nonnull UID dataElement) {
8383
DELETE FROM datavalueaudit dva
8484
WHERE dva.dataelementid = (SELECT de.dataelementid FROM dataelement de WHERE de.uid = :de)""";
8585

86-
entityManager.createNativeQuery(sql).setParameter("de", dataElement.getValue()).executeUpdate();
86+
nativeSynchronizedQuery(sql).setParameter("de", dataElement.getValue()).executeUpdate();
8787
}
8888

8989
@Override
@@ -93,8 +93,7 @@ public void deleteByOptionCombo(@Nonnull UID categoryOptionCombo) {
9393
DELETE FROM datavalueaudit dva
9494
WHERE dva.categoryoptioncomboid = (SELECT categoryoptioncomboid FROM categoryoptioncombo WHERE uid = :coc)
9595
OR dva.attributeoptioncomboid = (SELECT categoryoptioncomboid FROM categoryoptioncombo WHERE uid = :coc);""";
96-
entityManager
97-
.createNativeQuery(sql)
96+
nativeSynchronizedQuery(sql)
9897
.setParameter("coc", categoryOptionCombo.getValue())
9998
.executeUpdate();
10099
}
@@ -258,14 +257,16 @@ IF NOT EXISTS (
258257
END IF;
259258
END;
260259
$$""";
261-
getSession().createNativeQuery(sql).executeUpdate();
260+
// DDL, not entity persistence, so it skips Hibernate and its cache invalidation
261+
jdbcTemplate.execute(sql);
262262
}
263263

264264
@Override
265265
public void disableAudit() {
266266
String sql =
267267
"""
268268
DROP TRIGGER IF EXISTS trg_datavalue_audit ON datavalue""";
269-
getSession().createNativeQuery(sql).executeUpdate();
269+
// DDL, not entity persistence, so it skips Hibernate and its cache invalidation
270+
jdbcTemplate.execute(sql);
270271
}
271272
}

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueTrimStore.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ public int updateDeletedIfNotZeroIsSignificant() {
8787
AND de.zeroissignificant = false
8888
AND dv.dataelementid = de.dataelementid
8989
AND (dv.value IS NULL OR dv.value = '')""";
90-
return getSession()
91-
.createNativeQuery(sql)
90+
return nativeSynchronizedQuery(sql)
9291
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(1000))
9392
.executeUpdate();
9493
}

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/period/hibernate/HibernatePeriodStore.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ INSERT INTO period (periodid, periodtypeid, startdate, enddate, iso)
124124
if (pk != null) {
125125
return pk;
126126
}
127-
session
128-
.createNativeQuery(sql2)
127+
nativeSynchronizedQuery(session, sql2)
129128
.setParameter("type", period.getPeriodType().getName())
130129
.setParameter("start", period.getStartDate())
131130
.setParameter("end", period.getEndDate())
@@ -144,8 +143,7 @@ INSERT INTO period (periodid, periodtypeid, startdate, enddate, iso)
144143
public void delete(@Nonnull Period period) {
145144
String isoDate = period.getIsoDate();
146145
int deleted =
147-
getSession()
148-
.createNativeQuery("DELETE FROM period where iso = :iso")
146+
nativeSynchronizedQuery("DELETE FROM period where iso = :iso")
149147
.setParameter("iso", isoDate)
150148
.executeUpdate();
151149
if (deleted > 0) {
@@ -261,7 +259,12 @@ INSERT INTO periodtype (periodtypeid, name)
261259
if (pk != null) {
262260
return pk;
263261
}
264-
session.createNativeQuery(sql2).setParameter("name", name).executeUpdate();
262+
session
263+
.createNativeQuery(sql2)
264+
// PeriodType, not the store's own Period
265+
.addSynchronizedEntityClass(PeriodType.class)
266+
.setParameter("name", name)
267+
.executeUpdate();
265268
return session.createNativeQuery("SELECT lastval()").uniqueResult();
266269
});
267270
if (id instanceof Number n) {
@@ -286,6 +289,7 @@ public void updatePeriodType(PeriodType periodType) {
286289
session ->
287290
session
288291
.createNativeQuery(sql)
292+
.addSynchronizedEntityClass(PeriodType.class)
289293
.setParameter("name", name)
290294
.setParameter("label", label)
291295
.executeUpdate());

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/program/hibernate/HibernateProgramIndicatorStore.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ WHEN attributecomboid IN (:sourceCategoryComboIds)
118118
categorycomboid IN (:sourceCategoryComboIds)
119119
OR attributecomboid IN (:sourceCategoryComboIds);
120120
""";
121-
return getSession()
122-
.createNativeQuery(sql)
121+
return nativeSynchronizedQuery(sql)
123122
.setParameter("targetCategoryComboId", targetCategoryComboId)
124123
.setParameter("sourceCategoryComboIds", sourceCategoryComboIds)
125124
.setLockOptions(new LockOptions(PESSIMISTIC_WRITE).setTimeOut(5000))

0 commit comments

Comments
 (0)