From 6f5b5dba656c2c8e1a64ab15d7e0dc52ad8dcdb4 Mon Sep 17 00:00:00 2001 From: Enrico Date: Thu, 28 May 2026 14:10:45 +0200 Subject: [PATCH] feat: TrackedEntity JDBC insert [DHIS2-21378] --- .../imports/TrackerImportReportTest.java | 4 +- .../persister/AbstractTrackerPersister.java | 98 +++++++++- .../bundle/persister/EnrollmentPersister.java | 9 +- .../bundle/persister/EntityWriteBatch.java | 171 ++++++++++++++++-- .../persister/RelationshipPersister.java | 11 +- .../persister/SingleEventPersister.java | 11 +- .../persister/TrackedEntityPersister.java | 11 +- .../persister/TrackerEventPersister.java | 11 +- 8 files changed, 290 insertions(+), 36 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportReportTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportReportTest.java index a0001e17e607..c994b6dd6679 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportReportTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportReportTest.java @@ -43,7 +43,7 @@ import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.relationship.RelationshipType; -import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; +import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.tracker.imports.report.ImportReport; import org.hisp.dhis.webapi.controller.tracker.JsonImportReport; @@ -57,7 +57,7 @@ * requests */ @Transactional -class TrackerImportReportTest extends H2ControllerIntegrationTestBase { +class TrackerImportReportTest extends PostgresControllerIntegrationTestBase { private static final String ORG_UNIT_UID = "PSeMWi7rBgb"; diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/AbstractTrackerPersister.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/AbstractTrackerPersister.java index 9f9b038f117a..395efb12afa2 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/AbstractTrackerPersister.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/AbstractTrackerPersister.java @@ -34,9 +34,12 @@ import static org.hisp.dhis.changelog.ChangeLogType.DELETE; import static org.hisp.dhis.changelog.ChangeLogType.UPDATE; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -107,6 +110,8 @@ public abstract class AbstractTrackerPersister notifications = new ArrayList<>(); ChangeLogAccumulator changeLogs = new ChangeLogAccumulator(); - EntityWriteBatch batch = new EntityWriteBatch(); + EntityWriteBatch batch = new EntityWriteBatch(objectMapper); // // Extract the entities to persist from the Bundle @@ -132,6 +137,11 @@ public PersistResult persist(TrackerBundle bundle) { Connection conn = DataSourceUtils.getConnection(dataSource); try { + // Pre-allocate primary keys in a single round-trip for entity types that opt in. + // The cursor advances only on isNew branches inside the loop below. + long[] preAllocatedIds = preAllocateIds(conn, bundle, dtos); + int preAllocatedIdsCursor = 0; + for (T trackerDto : dtos) { Entity objectReport = new Entity(getType(), trackerDto.getUID()); @@ -150,6 +160,15 @@ public PersistResult persist(TrackerBundle bundle) { // V convertedDto = convert(bundle, trackerDto); + // + // Assign the pre-allocated id (if any) before staging so the flush path can emit it + // unchanged, and so any TEAV/changelog code that reads convertedDto.getId() sees the + // final value. + // + if (preAllocatedIds != null && isNew(bundle, trackerDto)) { + assignId(convertedDto, preAllocatedIds[preAllocatedIdsCursor++]); + } + // // Handle ownership records, if required // @@ -214,7 +233,7 @@ public PersistResult persist(TrackerBundle bundle) { if (FlushMode.OBJECT == bundle.getFlushMode()) { // Flush entity INSERTs/UPDATEs before changelog INSERTs so FK references // (trackedentityid, eventid) exist before changelog rows reference them. - batch.flush(entityManager); + batch.flush(entityManager, conn); entityManager.flush(); changeLogs.flushAll(conn); } @@ -252,7 +271,7 @@ public PersistResult persist(TrackerBundle bundle) { if (FlushMode.AUTO == bundle.getFlushMode()) { // Flush entity INSERTs/UPDATEs before changelog INSERTs so FK references // (trackedentityid, eventid) exist before changelog rows reference them. - batch.flush(entityManager); + batch.flush(entityManager, conn); entityManager.flush(); changeLogs.flushAll(conn); } @@ -264,6 +283,35 @@ public PersistResult persist(TrackerBundle bundle) { return new PersistResult(typeReport, notifications); } + private long[] preAllocateIds(Connection conn, TrackerBundle bundle, List dtos) + throws SQLException { + String sequenceName = sequenceName(); + if (sequenceName == null) { + return null; + } + int createCount = 0; + for (T dto : dtos) { + if (isNew(bundle, dto)) { + createCount++; + } + } + if (createCount == 0) { + return null; + } + return allocateIds(conn, sequenceName, createCount); + } + + private void assignId(V convertedDto, long id) { + if (convertedDto instanceof TrackedEntity te) { + te.setId(id); + } else { + throw new IllegalStateException( + "Pre-allocated id assignment not implemented for " + + convertedDto.getClass().getName() + + " -- sequenceName() returned non-null but assignId does not handle this type."); + } + } + private void stageInsert(V convertedDto, EntityWriteBatch batch) { if (convertedDto instanceof TrackedEntity te) { batch.stageInsert(te); @@ -396,6 +444,40 @@ protected static Set mergeNotifications( @SuppressWarnings("unchecked") protected abstract List getByType(TrackerBundle bundle); + /** + * Returns the PostgreSQL sequence used to allocate primary keys for this entity type, or {@code + * null} if id allocation should be left to Hibernate. When non-null, {@link #persist} pre-fetches + * one id per CREATE entity in a single round-trip and assigns the id before staging so the flush + * path can emit a multi-row INSERT. + */ + protected abstract String sequenceName(); + + /** + * Fetches {@code count} ids from {@code sequenceName} in a single round-trip. The sequence name + * is interpolated into the SQL (not a bind parameter) because PostgreSQL's {@code nextval} takes + * a {@code regclass}, and the value comes from {@link #sequenceName()} — controlled by us, not + * user input. + */ + private static long[] allocateIds(Connection conn, String sequenceName, int count) + throws SQLException { + long[] ids = new long[count]; + String sql = "select nextval('" + sequenceName + "') from generate_series(1, ?)"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, count); + try (ResultSet rs = ps.executeQuery()) { + int i = 0; + while (rs.next()) { + ids[i++] = rs.getLong(1); + } + if (i != count) { + throw new SQLException( + "Allocated " + i + " ids from " + sequenceName + ", expected " + count); + } + } + } + return ids; + } + // // // // // // // // // // // // // // // // // SHARED METHODS // @@ -446,12 +528,12 @@ protected void handleTrackedEntityAttributeValues( // already attached to the session, preventing a duplicate em.persist that would throw // EntityExistsException. The batch overlay catches the within-persister case (e.g. two // enrollments under the same TE both carrying the same attribute) where the second occurrence - // would otherwise produce a fresh instance with the same composite key. A transient TE - // (id == 0) has no DB rows yet -- the insert is still staged in the batch -- so binding it as - // a query parameter would throw TransientObjectException; skip the JPQL in that case and rely - // on the batch overlay alone. + // would otherwise produce a fresh instance with the same composite key. A TE staged for + // insert in this batch has no DB row yet -- whether or not its id is set (Phase 4a pre- + // allocates the id from the sequence) -- so the JPQL would always return empty and the lookup + // can be skipped. Map attributeValueById = - trackedEntity.getId() == 0 + batch.isStagedAsInsert(trackedEntity) ? new HashMap<>() : entityManager .createQuery( diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EnrollmentPersister.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EnrollmentPersister.java index b108aa862645..d9e727a04cf5 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EnrollmentPersister.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EnrollmentPersister.java @@ -29,6 +29,7 @@ */ package org.hisp.dhis.tracker.imports.bundle.persister; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.EnumSet; import java.util.List; import java.util.Set; @@ -62,11 +63,17 @@ public EnrollmentPersister( ReservedValueService reservedValueService, DataSource dataSource, FileResourceStore fileResourceStore, + ObjectMapper objectMapper, TrackedEntityProgramOwnerService trackedEntityProgramOwnerService) { - super(reservedValueService, dataSource, fileResourceStore); + super(reservedValueService, dataSource, fileResourceStore, objectMapper); this.trackedEntityProgramOwnerService = trackedEntityProgramOwnerService; } + @Override + protected String sequenceName() { + return null; + } + @Override protected void updateAttributes( TrackerPreheat preheat, diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EntityWriteBatch.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EntityWriteBatch.java index 566b868a9d07..caa4c252d237 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EntityWriteBatch.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/EntityWriteBatch.java @@ -29,10 +29,19 @@ */ package org.hisp.dhis.tracker.imports.bundle.persister; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.stream.Stream; +import org.hisp.dhis.program.UserInfoSnapshot; import org.hisp.dhis.tracker.model.Enrollment; import org.hisp.dhis.tracker.model.Relationship; import org.hisp.dhis.tracker.model.SingleEvent; @@ -45,13 +54,23 @@ * point. Mirrors {@link ChangeLogAccumulator} and shares the same mark/rollback contract for * per-entity error isolation in non-atomic mode. * - *

Current scope: inserts and updates for TrackedEntity, Enrollment, TrackerEvent, SingleEvent; - * inserts only for Relationship (updates are rejected upstream by {@link - * AbstractTrackerPersister}); inserts, updates, and deletes for TEAVs. The flush implementation - * delegates back to {@link EntityManager} so that Hibernate continues to drive persistence while - * the staging shape is in place. Phase 6 replaces {@link #flush(EntityManager)} with a - * connection-based implementation that emits multi-row INSERT / unnest UPDATE / tuple DELETE - * statements. + *

Scope: + * + *

    + *
  • TrackedEntity inserts are flushed via a multi-row JDBC INSERT against {@code trackedentity} + * -- ids are pre-allocated by {@link AbstractTrackerPersister} from {@code + * trackedentityinstance_sequence} in one round-trip. + *
  • TrackedEntity updates and all other entity types (Enrollment, TrackerEvent, SingleEvent, + * Relationship) still delegate to {@link EntityManager}; their Phase 6 JDBC replacements + * follow. + *
  • TEAVs continue to delegate to {@link EntityManager} until their Phase 6 turn. + *
+ * + *

Top-level entities are flushed before TEAVs so that ids are set (and rows present in the DB or + * in the Hibernate persistence context) before any TEAV that references them. Within top-level + * entities the order (TE inserts, TE updates, Enrollment, TrackerEvent, SingleEvent, Relationship) + * matches the persister call order enforced by {@code DefaultTrackerBundleService.commit()} and is + * FK-safe. * *

Each {@link AbstractTrackerPersister#persist} call creates its own batch, so in practice only * one top-level entity type list is populated per batch (the persister's own type) alongside any @@ -59,6 +78,26 @@ */ class EntityWriteBatch { + /** Cap on rows per multi-row INSERT statement, to keep query text manageable. */ + private static final int INSERT_BATCH_SIZE = 128; + + private static final int TRACKED_ENTITY_SRID = 4326; + + private static final String TRACKED_ENTITY_INSERT_PREFIX = + "insert into trackedentity (" + + "trackedentityid, uid, created, lastupdated," + + " createdatclient, lastupdatedatclient," + + " inactive, deleted, lastsynchronized, potentialduplicate," + + " organisationunitid, trackedentitytypeid," + + " geometry, createdbyuserinfo, lastupdatedbyuserinfo) values "; + + private static final String TRACKED_ENTITY_INSERT_ROW = + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ST_GeomFromText(?, " + + TRACKED_ENTITY_SRID + + "), ?::jsonb, ?::jsonb)"; + + private final ObjectMapper objectMapper; + private final List teInserts = new ArrayList<>(); private final List teUpdates = new ArrayList<>(); @@ -77,6 +116,10 @@ class EntityWriteBatch { private final List teavUpdates = new ArrayList<>(); private final List teavDeletes = new ArrayList<>(); + EntityWriteBatch(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + void stageInsert(TrackedEntity trackedEntity) { teInserts.add(trackedEntity); } @@ -141,6 +184,15 @@ Stream stagedFor(TrackedEntity trackedEntity) { .filter(v -> v.getTrackedEntity() == trackedEntity); } + /** + * Whether the given TrackedEntity is being inserted in this batch (no DB row yet). Used by the + * attribute-handling code to skip the "load existing TEAVs" JPQL query, which would always return + * empty for a fresh insert. + */ + boolean isStagedAsInsert(TrackedEntity trackedEntity) { + return teInserts.contains(trackedEntity); + } + Mark mark() { return new Mark( new Mark.EntityCounts(teInserts.size(), teUpdates.size()), @@ -167,21 +219,17 @@ void rollbackTo(Mark mark) { } /** - * Applies all staged writes through the provided {@link EntityManager}. Temporary delegating - * implementation; Phase 6 swaps this for JDBC multi-row statements that take a {@link - * java.sql.Connection}. - * - *

Top-level entities are flushed before TEAVs so that Hibernate assigns ids (and inserts the - * rows, when {@code em.flush()} runs) before any TEAV that references them. The top-level order - * (TE, Enrollment, TrackerEvent, SingleEvent, Relationship) matches the persister call order - * enforced by {@code DefaultTrackerBundleService.commit()} and is FK-safe. + * Applies all staged writes. TrackedEntity inserts go through a JDBC multi-row INSERT on {@code + * conn}; everything else still delegates to {@code entityManager}. The connection must be the one + * bound to the current Spring-managed transaction so that the JDBC INSERT and any subsequent + * Hibernate flush execute under the same commit. */ - void flush(EntityManager entityManager) { + void flush(EntityManager entityManager, Connection conn) throws SQLException { if (isEmpty()) { return; } - persistAll(entityManager, teInserts); + insertTrackedEntities(conn); mergeAll(entityManager, teUpdates); persistAll(entityManager, enrollmentInserts); mergeAll(entityManager, enrollmentUpdates); @@ -215,6 +263,95 @@ void flush(EntityManager entityManager) { teavDeletes.clear(); } + private void insertTrackedEntities(Connection conn) throws SQLException { + if (teInserts.isEmpty()) { + return; + } + for (int from = 0; from < teInserts.size(); from += INSERT_BATCH_SIZE) { + int to = Math.min(from + INSERT_BATCH_SIZE, teInserts.size()); + List chunk = teInserts.subList(from, to); + String sql = + buildMultiRowInsertSql( + TRACKED_ENTITY_INSERT_PREFIX, TRACKED_ENTITY_INSERT_ROW, chunk.size()); + try (PreparedStatement ps = conn.prepareStatement(sql)) { + int p = 1; + for (TrackedEntity te : chunk) { + p = bindTrackedEntityRow(ps, p, te); + } + ps.executeUpdate(); + } + } + } + + private int bindTrackedEntityRow(PreparedStatement ps, int p, TrackedEntity te) + throws SQLException { + if (te.getId() == 0) { + throw new SQLException( + "TrackedEntity " + + te.getUid() + + " has no pre-allocated id; AbstractTrackerPersister" + + " must pre-allocate ids when sequenceName() is non-null."); + } + ps.setLong(p++, te.getId()); + ps.setString(p++, te.getUid()); + ps.setTimestamp(p++, toTimestamp(te.getCreated())); + ps.setTimestamp(p++, toTimestamp(te.getLastUpdated())); + setNullableTimestamp(ps, p++, te.getCreatedAtClient()); + setNullableTimestamp(ps, p++, te.getLastUpdatedAtClient()); + ps.setBoolean(p++, te.isInactive()); + ps.setBoolean(p++, te.isDeleted()); + ps.setTimestamp(p++, toTimestamp(te.getLastSynchronized())); + ps.setBoolean(p++, te.isPotentialDuplicate()); + ps.setLong(p++, te.getOrganisationUnit().getId()); + ps.setLong(p++, te.getTrackedEntityType().getId()); + if (te.getGeometry() != null) { + ps.setString(p++, te.getGeometry().toText()); + } else { + ps.setNull(p++, Types.VARCHAR); + } + ps.setString(p++, toJson(te.getCreatedByUserInfo())); + ps.setString(p++, toJson(te.getLastUpdatedByUserInfo())); + return p; + } + + private static String buildMultiRowInsertSql( + String prefix, String rowPlaceholders, int rowCount) { + StringBuilder sb = + new StringBuilder(prefix.length() + rowPlaceholders.length() * rowCount + 16); + sb.append(prefix); + for (int i = 0; i < rowCount; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(rowPlaceholders); + } + return sb.toString(); + } + + private static Timestamp toTimestamp(Date date) { + return date != null ? new Timestamp(date.getTime()) : null; + } + + private static void setNullableTimestamp(PreparedStatement ps, int index, Date date) + throws SQLException { + if (date != null) { + ps.setTimestamp(index, new Timestamp(date.getTime())); + } else { + ps.setNull(index, Types.TIMESTAMP); + } + } + + private String toJson(UserInfoSnapshot info) throws SQLException { + if (info == null) { + return null; + } + try { + return objectMapper.writeValueAsString(info); + } catch (JsonProcessingException e) { + throw new SQLException("Failed to serialize UserInfoSnapshot to JSON", e); + } + } + private boolean isEmpty() { return teInserts.isEmpty() && teUpdates.isEmpty() diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/RelationshipPersister.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/RelationshipPersister.java index 3ae2cad6c3b5..7416e7b3539a 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/RelationshipPersister.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/RelationshipPersister.java @@ -29,6 +29,7 @@ */ package org.hisp.dhis.tracker.imports.bundle.persister; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.Set; import javax.sql.DataSource; @@ -54,8 +55,14 @@ public class RelationshipPersister public RelationshipPersister( ReservedValueService reservedValueService, DataSource dataSource, - FileResourceStore fileResourceStore) { - super(reservedValueService, dataSource, fileResourceStore); + FileResourceStore fileResourceStore, + ObjectMapper objectMapper) { + super(reservedValueService, dataSource, fileResourceStore, objectMapper); + } + + @Override + protected String sequenceName() { + return null; } @Override diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/SingleEventPersister.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/SingleEventPersister.java index 396d5d99eedd..7a96d8a52fc4 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/SingleEventPersister.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/SingleEventPersister.java @@ -33,6 +33,7 @@ import static org.hisp.dhis.changelog.ChangeLogType.DELETE; import static org.hisp.dhis.changelog.ChangeLogType.UPDATE; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -78,8 +79,14 @@ public class SingleEventPersister public SingleEventPersister( ReservedValueService reservedValueService, DataSource dataSource, - FileResourceStore fileResourceStore) { - super(reservedValueService, dataSource, fileResourceStore); + FileResourceStore fileResourceStore, + ObjectMapper objectMapper) { + super(reservedValueService, dataSource, fileResourceStore, objectMapper); + } + + @Override + protected String sequenceName() { + return null; } @Override diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackedEntityPersister.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackedEntityPersister.java index 1aed6c4483d8..eb17687c0b0f 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackedEntityPersister.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackedEntityPersister.java @@ -29,6 +29,7 @@ */ package org.hisp.dhis.tracker.imports.bundle.persister; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.List; import java.util.Set; @@ -55,8 +56,14 @@ public class TrackedEntityPersister public TrackedEntityPersister( ReservedValueService reservedValueService, DataSource dataSource, - FileResourceStore fileResourceStore) { - super(reservedValueService, dataSource, fileResourceStore); + FileResourceStore fileResourceStore, + ObjectMapper objectMapper) { + super(reservedValueService, dataSource, fileResourceStore, objectMapper); + } + + @Override + protected String sequenceName() { + return "trackedentityinstance_sequence"; } @Override diff --git a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackerEventPersister.java b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackerEventPersister.java index d4a894b3b1a6..fad4d0ecfa2b 100644 --- a/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackerEventPersister.java +++ b/dhis-2/dhis-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/persister/TrackerEventPersister.java @@ -33,6 +33,7 @@ import static org.hisp.dhis.changelog.ChangeLogType.DELETE; import static org.hisp.dhis.changelog.ChangeLogType.UPDATE; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -79,8 +80,14 @@ public class TrackerEventPersister public TrackerEventPersister( ReservedValueService reservedValueService, DataSource dataSource, - FileResourceStore fileResourceStore) { - super(reservedValueService, dataSource, fileResourceStore); + FileResourceStore fileResourceStore, + ObjectMapper objectMapper) { + super(reservedValueService, dataSource, fileResourceStore, objectMapper); + } + + @Override + protected String sequenceName() { + return null; } @Override