Skip to content

Commit 02e705c

Browse files
feat: Batch entity when persisting [DHIS2-21378]
1 parent 14ea335 commit 02e705c

2 files changed

Lines changed: 193 additions & 10 deletions

File tree

dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/AbstractTrackerPersister.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,12 @@
7979
import org.hisp.dhis.tracker.imports.programrule.engine.Notification;
8080
import org.hisp.dhis.tracker.imports.report.Entity;
8181
import org.hisp.dhis.tracker.imports.report.TrackerTypeReport;
82+
import org.hisp.dhis.tracker.model.Enrollment;
83+
import org.hisp.dhis.tracker.model.Relationship;
84+
import org.hisp.dhis.tracker.model.SingleEvent;
8285
import org.hisp.dhis.tracker.model.TrackedEntity;
8386
import org.hisp.dhis.tracker.model.TrackedEntityAttributeValue;
87+
import org.hisp.dhis.tracker.model.TrackerEvent;
8488
import org.hisp.dhis.user.UserDetails;
8589
import org.springframework.jdbc.datasource.DataSourceUtils;
8690

@@ -154,7 +158,7 @@ public PersistResult persist(TrackerBundle bundle) {
154158
// Save or update the entity
155159
//
156160
if (isNew(bundle, trackerDto)) {
157-
entityManager.persist(convertedDto);
161+
stageInsert(convertedDto, batch);
158162
updateDataValues(
159163
bundle.getPreheat(),
160164
trackerDto,
@@ -186,7 +190,7 @@ public PersistResult persist(TrackerBundle bundle) {
186190
bundle.getUser(),
187191
changeLogs,
188192
batch);
189-
entityManager.merge(convertedDto);
193+
stageUpdate(convertedDto, batch);
190194
typeReport.getStats().incUpdated();
191195
typeReport.addEntity(objectReport);
192196
bundle.addUpdatedTrackedEntities(getUpdatedTrackedEntities(convertedDto));
@@ -259,6 +263,40 @@ public PersistResult persist(TrackerBundle bundle) {
259263
return new PersistResult(typeReport, notifications);
260264
}
261265

266+
private void stageInsert(V convertedDto, EntityWriteBatch batch) {
267+
if (convertedDto instanceof TrackedEntity te) {
268+
batch.stageInsert(te);
269+
} else if (convertedDto instanceof Enrollment e) {
270+
batch.stageInsert(e);
271+
} else if (convertedDto instanceof TrackerEvent e) {
272+
batch.stageInsert(e);
273+
} else if (convertedDto instanceof SingleEvent e) {
274+
batch.stageInsert(e);
275+
} else if (convertedDto instanceof Relationship r) {
276+
batch.stageInsert(r);
277+
} else {
278+
throw new IllegalArgumentException(
279+
"Unsupported entity type: " + convertedDto.getClass().getName());
280+
}
281+
}
282+
283+
// Relationships are not updated -- the persister branch above ignores update payloads before
284+
// this is called -- so no Relationship case is needed here.
285+
private static void stageUpdate(Object convertedDto, EntityWriteBatch batch) {
286+
if (convertedDto instanceof TrackedEntity te) {
287+
batch.stageUpdate(te);
288+
} else if (convertedDto instanceof Enrollment e) {
289+
batch.stageUpdate(e);
290+
} else if (convertedDto instanceof TrackerEvent e) {
291+
batch.stageUpdate(e);
292+
} else if (convertedDto instanceof SingleEvent e) {
293+
batch.stageUpdate(e);
294+
} else {
295+
throw new IllegalArgumentException(
296+
"Unsupported entity type for update: " + convertedDto.getClass().getName());
297+
}
298+
}
299+
262300
// // // // // // // //
263301
// // // // // // // //
264302
// TEMPLATE METHODS //

dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EntityWriteBatch.java

Lines changed: 153 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,87 @@
3333
import java.util.ArrayList;
3434
import java.util.List;
3535
import java.util.stream.Stream;
36+
import org.hisp.dhis.tracker.model.Enrollment;
37+
import org.hisp.dhis.tracker.model.Relationship;
38+
import org.hisp.dhis.tracker.model.SingleEvent;
3639
import org.hisp.dhis.tracker.model.TrackedEntity;
3740
import org.hisp.dhis.tracker.model.TrackedEntityAttributeValue;
41+
import org.hisp.dhis.tracker.model.TrackerEvent;
3842

3943
/**
4044
* Accumulates entity-level writes staged during the persist loop and applies them at a single flush
4145
* point. Mirrors {@link ChangeLogAccumulator} and shares the same mark/rollback contract for
4246
* per-entity error isolation in non-atomic mode.
4347
*
44-
* <p>Phase 3 scope: TEAV inserts/updates/deletes only. The flush implementation currently delegates
45-
* back to {@link EntityManager} so that Hibernate continues to drive persistence while the staging
46-
* shape is in place. Phase 6 replaces {@link #flush(EntityManager)} with a connection-based
47-
* implementation that emits multi-row INSERT / unnest UPDATE / tuple DELETE statements. Top-level
48-
* entities (TrackedEntity, Enrollment, events, relationships) are added by Phase 4.
48+
* <p>Current scope: TrackedEntity, Enrollment, TrackerEvent, SingleEvent, Relationship inserts and
49+
* updates, plus TEAV inserts/updates/deletes. The flush implementation delegates back to {@link
50+
* EntityManager} so that Hibernate continues to drive persistence while the staging shape is in
51+
* place. Phase 6 replaces {@link #flush(EntityManager)} with a connection-based implementation that
52+
* emits multi-row INSERT / unnest UPDATE / tuple DELETE statements.
53+
*
54+
* <p>Each {@link AbstractTrackerPersister#persist} call creates its own batch, so in practice only
55+
* one top-level entity type list is populated per batch (the persister's own type) alongside any
56+
* TEAVs staged by its attribute-handling code.
4957
*/
5058
class EntityWriteBatch {
5159

60+
private final List<TrackedEntity> teInserts = new ArrayList<>();
61+
private final List<TrackedEntity> teUpdates = new ArrayList<>();
62+
63+
private final List<Enrollment> enrollmentInserts = new ArrayList<>();
64+
private final List<Enrollment> enrollmentUpdates = new ArrayList<>();
65+
66+
private final List<TrackerEvent> trackerEventInserts = new ArrayList<>();
67+
private final List<TrackerEvent> trackerEventUpdates = new ArrayList<>();
68+
69+
private final List<SingleEvent> singleEventInserts = new ArrayList<>();
70+
private final List<SingleEvent> singleEventUpdates = new ArrayList<>();
71+
72+
private final List<Relationship> relationshipInserts = new ArrayList<>();
73+
5274
private final List<TrackedEntityAttributeValue> teavInserts = new ArrayList<>();
5375
private final List<TrackedEntityAttributeValue> teavUpdates = new ArrayList<>();
5476
private final List<TrackedEntityAttributeValue> teavDeletes = new ArrayList<>();
5577

78+
void stageInsert(TrackedEntity trackedEntity) {
79+
teInserts.add(trackedEntity);
80+
}
81+
82+
void stageUpdate(TrackedEntity trackedEntity) {
83+
teUpdates.add(trackedEntity);
84+
}
85+
86+
void stageInsert(Enrollment enrollment) {
87+
enrollmentInserts.add(enrollment);
88+
}
89+
90+
void stageUpdate(Enrollment enrollment) {
91+
enrollmentUpdates.add(enrollment);
92+
}
93+
94+
void stageInsert(TrackerEvent event) {
95+
trackerEventInserts.add(event);
96+
}
97+
98+
void stageUpdate(TrackerEvent event) {
99+
trackerEventUpdates.add(event);
100+
}
101+
102+
void stageInsert(SingleEvent event) {
103+
singleEventInserts.add(event);
104+
}
105+
106+
void stageUpdate(SingleEvent event) {
107+
singleEventUpdates.add(event);
108+
}
109+
110+
/**
111+
* Relationships are never updated -- {@link AbstractTrackerPersister} ignores update payloads.
112+
*/
113+
void stageInsert(Relationship relationship) {
114+
relationshipInserts.add(relationship);
115+
}
116+
56117
void stageTeavInsert(TrackedEntityAttributeValue value) {
57118
teavInserts.add(value);
58119
}
@@ -79,10 +140,31 @@ Stream<TrackedEntityAttributeValue> stagedFor(TrackedEntity trackedEntity) {
79140
}
80141

81142
Mark mark() {
82-
return new Mark(teavInserts.size(), teavUpdates.size(), teavDeletes.size());
143+
return new Mark(
144+
teInserts.size(),
145+
teUpdates.size(),
146+
enrollmentInserts.size(),
147+
enrollmentUpdates.size(),
148+
trackerEventInserts.size(),
149+
trackerEventUpdates.size(),
150+
singleEventInserts.size(),
151+
singleEventUpdates.size(),
152+
relationshipInserts.size(),
153+
teavInserts.size(),
154+
teavUpdates.size(),
155+
teavDeletes.size());
83156
}
84157

85158
void rollbackTo(Mark mark) {
159+
truncate(teInserts, mark.teInsertSize);
160+
truncate(teUpdates, mark.teUpdateSize);
161+
truncate(enrollmentInserts, mark.enrollmentInsertSize);
162+
truncate(enrollmentUpdates, mark.enrollmentUpdateSize);
163+
truncate(trackerEventInserts, mark.trackerEventInsertSize);
164+
truncate(trackerEventUpdates, mark.trackerEventUpdateSize);
165+
truncate(singleEventInserts, mark.singleEventInsertSize);
166+
truncate(singleEventUpdates, mark.singleEventUpdateSize);
167+
truncate(relationshipInserts, mark.relationshipInsertSize);
86168
truncate(teavInserts, mark.teavInsertSize);
87169
truncate(teavUpdates, mark.teavUpdateSize);
88170
truncate(teavDeletes, mark.teavDeleteSize);
@@ -92,12 +174,27 @@ void rollbackTo(Mark mark) {
92174
* Applies all staged writes through the provided {@link EntityManager}. Temporary delegating
93175
* implementation; Phase 6 swaps this for JDBC multi-row statements that take a {@link
94176
* java.sql.Connection}.
177+
*
178+
* <p>Top-level entities are flushed before TEAVs so that Hibernate assigns ids (and inserts the
179+
* rows, when {@code em.flush()} runs) before any TEAV that references them. The top-level order
180+
* (TE, Enrollment, TrackerEvent, SingleEvent, Relationship) matches the persister call order
181+
* enforced by {@code DefaultTrackerBundleService.commit()} and is FK-safe.
95182
*/
96183
void flush(EntityManager entityManager) {
97-
if (teavInserts.isEmpty() && teavUpdates.isEmpty() && teavDeletes.isEmpty()) {
184+
if (isEmpty()) {
98185
return;
99186
}
100187

188+
persistAll(entityManager, teInserts);
189+
mergeAll(entityManager, teUpdates);
190+
persistAll(entityManager, enrollmentInserts);
191+
mergeAll(entityManager, enrollmentUpdates);
192+
persistAll(entityManager, trackerEventInserts);
193+
mergeAll(entityManager, trackerEventUpdates);
194+
persistAll(entityManager, singleEventInserts);
195+
mergeAll(entityManager, singleEventUpdates);
196+
persistAll(entityManager, relationshipInserts);
197+
101198
for (TrackedEntityAttributeValue v : teavInserts) {
102199
entityManager.persist(v);
103200
}
@@ -108,16 +205,64 @@ void flush(EntityManager entityManager) {
108205
entityManager.remove(entityManager.contains(v) ? v : entityManager.merge(v));
109206
}
110207

208+
teInserts.clear();
209+
teUpdates.clear();
210+
enrollmentInserts.clear();
211+
enrollmentUpdates.clear();
212+
trackerEventInserts.clear();
213+
trackerEventUpdates.clear();
214+
singleEventInserts.clear();
215+
singleEventUpdates.clear();
216+
relationshipInserts.clear();
111217
teavInserts.clear();
112218
teavUpdates.clear();
113219
teavDeletes.clear();
114220
}
115221

222+
private boolean isEmpty() {
223+
return teInserts.isEmpty()
224+
&& teUpdates.isEmpty()
225+
&& enrollmentInserts.isEmpty()
226+
&& enrollmentUpdates.isEmpty()
227+
&& trackerEventInserts.isEmpty()
228+
&& trackerEventUpdates.isEmpty()
229+
&& singleEventInserts.isEmpty()
230+
&& singleEventUpdates.isEmpty()
231+
&& relationshipInserts.isEmpty()
232+
&& teavInserts.isEmpty()
233+
&& teavUpdates.isEmpty()
234+
&& teavDeletes.isEmpty();
235+
}
236+
237+
private static <T> void persistAll(EntityManager entityManager, List<T> entities) {
238+
for (T entity : entities) {
239+
entityManager.persist(entity);
240+
}
241+
}
242+
243+
private static <T> void mergeAll(EntityManager entityManager, List<T> entities) {
244+
for (T entity : entities) {
245+
entityManager.merge(entity);
246+
}
247+
}
248+
116249
private static <T> void truncate(List<T> list, int size) {
117250
if (list.size() > size) {
118251
list.subList(size, list.size()).clear();
119252
}
120253
}
121254

122-
record Mark(int teavInsertSize, int teavUpdateSize, int teavDeleteSize) {}
255+
record Mark(
256+
int teInsertSize,
257+
int teUpdateSize,
258+
int enrollmentInsertSize,
259+
int enrollmentUpdateSize,
260+
int trackerEventInsertSize,
261+
int trackerEventUpdateSize,
262+
int singleEventInsertSize,
263+
int singleEventUpdateSize,
264+
int relationshipInsertSize,
265+
int teavInsertSize,
266+
int teavUpdateSize,
267+
int teavDeleteSize) {}
123268
}

0 commit comments

Comments
 (0)