Skip to content

Commit db41604

Browse files
perf: DataEntry metadata with fewer n+1s [DHIS2-21757](2.41) (#24511)
* perf: DataEntry metadata with fewer n+1s [DHIS2-21757](2.41) * format
1 parent 0a61e55 commit db41604

13 files changed

Lines changed: 444 additions & 1 deletion

File tree

dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionComboStore.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
*/
2828
package org.hisp.dhis.category;
2929

30+
import java.util.Collection;
3031
import java.util.List;
3132
import java.util.Set;
3233
import org.hisp.dhis.common.IdentifiableObjectStore;
@@ -39,6 +40,18 @@ public interface CategoryOptionComboStore extends IdentifiableObjectStore<Catego
3940
CategoryOptionCombo getCategoryOptionCombo(
4041
CategoryCombo categoryCombo, Set<CategoryOption> categoryOptions);
4142

43+
/**
44+
* Returns the {@link CategoryOptionCombo}s of the given {@link CategoryCombo}s with their {@link
45+
* CategoryOptionCombo#getCategoryOptions()} eagerly fetched in a single query. Used to prime the
46+
* session before serialising category option combos, avoiding the per-combo N+1 select on the
47+
* {@code categoryoptioncombos_categoryoptions} join table.
48+
*
49+
* @param categoryCombos the category combos whose option combos to load.
50+
* @return the option combos with their category options initialised.
51+
*/
52+
List<CategoryOptionCombo> getCategoryOptionCombosWithCategoryOptions(
53+
Collection<CategoryCombo> categoryCombos);
54+
4255
void updateNames();
4356

4457
void deleteNoRollBack(CategoryOptionCombo categoryOptionCombo);

dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,18 @@ CategoryOptionCombo getCategoryOptionCombo(
412412
*/
413413
List<CategoryOptionCombo> getAllCategoryOptionCombos();
414414

415+
/**
416+
* Returns the {@link CategoryOptionCombo}s of the given {@link CategoryCombo}s with their
417+
* category options eagerly fetched in a single query. Used to prime the session before
418+
* serialising category option combos, avoiding the per-combo N+1 select on the {@code
419+
* categoryoptioncombos_categoryoptions} join table.
420+
*
421+
* @param categoryCombos the category combos whose option combos to load.
422+
* @return the option combos with their category options initialised.
423+
*/
424+
List<CategoryOptionCombo> getCategoryOptionCombosWithCategoryOptions(
425+
Collection<CategoryCombo> categoryCombos);
426+
415427
/**
416428
* Generates and persists a default Category, CategoryOption, CategoryCombo and
417429
* CategoryOptionCombo.

dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetService.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
*/
2828
package org.hisp.dhis.dataset;
2929

30+
import java.util.Collection;
3031
import java.util.Date;
3132
import java.util.List;
3233
import org.apache.commons.collections4.SetValuedMap;
@@ -85,6 +86,19 @@ public interface DataSetService extends DataSetDataIntegrityProvider {
8586
*/
8687
DataSet getDataSet(String uid);
8788

89+
/**
90+
* Returns the distinct {@link DataElement}s that are members of the given data sets, loaded in a
91+
* single query (with each data element's {@code dataSetElements} and {@code categoryCombo}
92+
* eagerly fetched) to avoid N+1 selects when iterating {@link DataSet#getDataElements()} and
93+
* {@link DataElement#getCategoryCombos()} across many data sets. Does not apply sharing/ACL
94+
* predicates on the data elements; callers are responsible for authorizing access to the data
95+
* sets.
96+
*
97+
* @param dataSets the data sets.
98+
* @return the distinct data elements of the given data sets.
99+
*/
100+
List<DataElement> getDataElementsByDataSet(Collection<DataSet> dataSets);
101+
88102
/**
89103
* Returns the DataSet with the given UID. Bypasses the ACL system.
90104
*

dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetStore.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
*/
2828
package org.hisp.dhis.dataset;
2929

30+
import java.util.Collection;
3031
import java.util.List;
3132
import org.hisp.dhis.common.IdentifiableObjectStore;
33+
import org.hisp.dhis.dataelement.DataElement;
3234
import org.hisp.dhis.dataentryform.DataEntryForm;
3335
import org.hisp.dhis.period.PeriodType;
3436

@@ -58,4 +60,20 @@ public interface DataSetStore
5860
* @return a list of DataSets.
5961
*/
6062
List<DataSet> getDataSetsByDataEntryForm(DataEntryForm dataEntryForm);
63+
64+
/**
65+
* Returns the distinct {@link DataElement}s that are members of the given data sets (via {@link
66+
* DataSetElement}), loaded in a single query. Each data element's {@link
67+
* DataElement#getDataSetElements()} collection and its {@link DataElement#getCategoryCombo()} are
68+
* eagerly fetched as well, so that {@link DataSet#getDataElements()} and {@link
69+
* DataElement#getCategoryCombos()} can be evaluated without triggering per-element (or
70+
* per-category-combo) N+1 selects.
71+
*
72+
* <p>This method does <b>not</b> apply sharing/ACL predicates on the data elements. Callers are
73+
* responsible for authorizing access to the data sets.
74+
*
75+
* @param dataSets the data sets.
76+
* @return the distinct data elements of the given data sets.
77+
*/
78+
List<DataElement> getDataElementsByDataSet(Collection<DataSet> dataSets);
6179
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
*/
2828
package org.hisp.dhis.category.hibernate;
2929

30+
import java.util.Collection;
3031
import java.util.List;
3132
import java.util.Set;
3233
import javax.persistence.EntityManager;
@@ -98,6 +99,26 @@ public CategoryOptionCombo getCategoryOptionCombo(
9899
return categoryOptionCombo;
99100
}
100101

102+
@Override
103+
public List<CategoryOptionCombo> getCategoryOptionCombosWithCategoryOptions(
104+
Collection<CategoryCombo> categoryCombos) {
105+
if (categoryCombos == null || categoryCombos.isEmpty()) {
106+
return List.of();
107+
}
108+
109+
String hql =
110+
"""
111+
select distinct coc from CategoryCombo cc
112+
join cc.optionCombos coc
113+
left join fetch coc.categoryOptions
114+
where cc in :categoryCombos
115+
""";
116+
117+
return getQuery(hql, CategoryOptionCombo.class)
118+
.setParameter("categoryCombos", categoryCombos)
119+
.list();
120+
}
121+
101122
@Override
102123
public void updateNames() {
103124
List<CategoryOptionCombo> categoryOptionCombos =

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/DefaultCategoryService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,13 @@ public CategoryOptionCombo getCategoryOptionCombo(
496496
return categoryOptionComboStore.getCategoryOptionCombo(categoryCombo, categoryOptions);
497497
}
498498

499+
@Override
500+
@Transactional(readOnly = true)
501+
public List<CategoryOptionCombo> getCategoryOptionCombosWithCategoryOptions(
502+
Collection<CategoryCombo> categoryCombos) {
503+
return categoryOptionComboStore.getCategoryOptionCombosWithCategoryOptions(categoryCombos);
504+
}
505+
499506
@Override
500507
@Transactional(readOnly = true)
501508
public List<CategoryOptionCombo> getAllCategoryOptionCombos() {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ public DataSet getDataSet(String uid) {
108108
return dataSetStore.getByUid(uid);
109109
}
110110

111+
@Override
112+
@Transactional(readOnly = true)
113+
public List<DataElement> getDataElementsByDataSet(Collection<DataSet> dataSets) {
114+
return dataSetStore.getDataElementsByDataSet(dataSets);
115+
}
116+
111117
@Override
112118
@Transactional(readOnly = true)
113119
public DataSet getDataSetNoAcl(String uid) {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030
import static com.google.common.base.Preconditions.checkNotNull;
3131

3232
import com.google.common.collect.Lists;
33+
import java.util.Collection;
3334
import java.util.List;
3435
import javax.annotation.Nonnull;
3536
import javax.persistence.EntityManager;
3637
import javax.persistence.criteria.CriteriaBuilder;
3738
import org.hibernate.query.Query;
3839
import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore;
40+
import org.hisp.dhis.dataelement.DataElement;
3941
import org.hisp.dhis.dataentryform.DataEntryForm;
4042
import org.hisp.dhis.dataset.DataSet;
4143
import org.hisp.dhis.dataset.DataSetStore;
@@ -124,4 +126,23 @@ public List<DataSet> getDataSetsByDataEntryForm(DataEntryForm dataEntryForm) {
124126
public List<DataSet> getDataSetsNotAssignedToOrganisationUnits() {
125127
return getQuery("from DataSet ds where size(ds.sources) = 0").list();
126128
}
129+
130+
@Override
131+
public List<DataElement> getDataElementsByDataSet(Collection<DataSet> dataSets) {
132+
if (dataSets == null || dataSets.isEmpty()) {
133+
return List.of();
134+
}
135+
136+
return getQuery(
137+
"""
138+
select distinct de from DataSetElement dse
139+
join dse.dataElement de
140+
left join fetch de.dataSetElements
141+
left join fetch de.categoryCombo
142+
where dse.dataSet in :dataSets
143+
""",
144+
DataElement.class)
145+
.setParameter("dataSets", dataSets)
146+
.list();
147+
}
127148
}

dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultDataSetMetadataExportService.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,18 @@ public ObjectNode getDataSetMetadata() {
145145
dataSetService.getDataSetOrganisationUnitsAssociations();
146146

147147
List<DataSet> dataSets = idObjectManager.getDataWriteAll(DataSet.class);
148-
List<DataElement> dataElements = sortById(flatMapToSet(dataSets, DataSet::getDataElements));
148+
List<DataElement> dataElements =
149+
sortById(new HashSet<>(dataSetService.getDataElementsByDataSet(dataSets)));
149150
List<Indicator> indicators = sortById(flatMapToSet(dataSets, DataSet::getIndicators));
150151
List<CategoryCombo> dataElementCategoryCombos =
151152
sortById(flatMapToSet(dataElements, DataElement::getCategoryCombos));
152153
List<CategoryCombo> dataSetCategoryCombos =
153154
sortById(mapToSet(dataSets, DataSet::getCategoryCombo));
155+
156+
// Preload the category options of the data-element category-option-combos in a single query, so
157+
// field-filter serialisation of categoryOptionCombos[...,categoryOptions~pluck[id]] does not
158+
// trigger a per-combo N+1 (categoryoptioncombos_categoryoptions).
159+
categoryService.getCategoryOptionCombosWithCategoryOptions(dataElementCategoryCombos);
154160
List<Category> dataElementCategories =
155161
sortById(flatMapToSet(dataElementCategoryCombos, CategoryCombo::getCategories));
156162
List<Category> dataSetCategories =

dhis-2/dhis-support/dhis-support-test/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
<groupId>org.hibernate</groupId>
100100
<artifactId>hibernate-core</artifactId>
101101
</dependency>
102+
<dependency>
103+
<groupId>net.ttddyy</groupId>
104+
<artifactId>datasource-proxy</artifactId>
105+
</dependency>
102106
<dependency>
103107
<groupId>javax.persistence</groupId>
104108
<artifactId>javax.persistence-api</artifactId>

0 commit comments

Comments
 (0)