diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/mappers/EnrollmentMapper.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/mappers/EnrollmentMapper.java index 5a20500a75f1..69f97e79d888 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/mappers/EnrollmentMapper.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/mappers/EnrollmentMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022, University of Oslo + * Copyright (c) 2004-2026, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -65,6 +65,32 @@ public interface EnrollmentMapper extends PreheatMapper { @Mapping(target = "status") Enrollment map(Enrollment enrollment); + /** + * Same as {@link #map(Enrollment)} but without {@code notes}, which is a lazy collection. Mapping + * it would force a query to load it. Use this for enrollments that are only ever read for their + * identifiers (e.g. the synthetic enrollment preheated for WITHOUT_REGISTRATION programs), never + * converted or persisted. + */ + @Named("mapWithoutNotes") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id") + @Mapping(target = "uid") + @Mapping(target = "code") + @Mapping(target = "user") + @Mapping(target = "completedBy") + @Mapping(target = "completedDate") + @Mapping(target = "program", qualifiedByName = "program") + @Mapping(target = "trackedEntity") + @Mapping(target = "organisationUnit") + @Mapping(target = "created") + @Mapping(target = "occurredDate") + @Mapping(target = "enrollmentDate") + @Mapping(target = "deleted") + @Mapping(target = "createdByUserInfo") + @Mapping(target = "lastUpdatedByUserInfo") + @Mapping(target = "status") + Enrollment mapWithoutNotes(Enrollment enrollment); + @Named("program") @BeanMapping(ignoreByDefault = true) @Mapping(target = "id") diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplier.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplier.java index e26b40d99e51..081f957c8f51 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplier.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022, University of Oslo + * Copyright (c) 2004-2026, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -61,10 +61,13 @@ public void preheatAdd(TrackerObjects trackerObjects, TrackerPreheat preheat) { programsWithoutRegistration = programStore.getByType(ProgramType.WITHOUT_REGISTRATION); } if (!programsWithoutRegistration.isEmpty()) { + // This enrollment is only ever used as an FK reference for events (it is never converted + // or persisted), so its notes are never read: map without them to avoid an unnecessary + // lazy-load query per WITHOUT_REGISTRATION program on every import. List enrollments = - DetachUtils.detach( - EnrollmentMapper.INSTANCE, - enrollmentStore.getByPrograms(programsWithoutRegistration)); + enrollmentStore.getByPrograms(programsWithoutRegistration).stream() + .map(EnrollmentMapper.INSTANCE::mapWithoutNotes) + .collect(Collectors.toList()); enrollments.forEach( e -> { diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplierTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplierTest.java index 6b7df7c1ab95..86aeaf018390 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplierTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/supplier/EnrollmentSupplierTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022, University of Oslo + * Copyright (c) 2004-2026, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,6 +31,9 @@ import static org.hisp.dhis.program.ProgramType.WITH_REGISTRATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.Lists; @@ -149,6 +152,24 @@ void verifySupplier() { preheat.getEnrollmentsWithoutRegistration(programWithoutRegistration.getUid())); } + @Test + void verifySupplierDoesNotAccessNotesOfEnrollmentWithoutRegistration() { + // On a real (Hibernate-backed) Enrollment, calling getNotes() on an entity that hasn't + // had its notes collection initialized yet triggers a lazy-load query. This enrollment is + // only ever used by the importer as an FK reference for events, never converted or + // persisted, so its notes must never be read during preheat. + Enrollment spiedEnrollmentWithoutRegistration = spy(enrollmentWithoutRegistration); + when(enrollmentStore.getByPrograms(Lists.newArrayList(programWithoutRegistration))) + .thenReturn(List.of(spiedEnrollmentWithoutRegistration)); + preheat.put( + TrackerIdSchemeParam.UID, + Lists.newArrayList(programWithRegistration, programWithoutRegistration)); + + this.supplier.preheatAdd(params, preheat); + + verify(spiedEnrollmentWithoutRegistration, never()).getNotes(); + } + private void assertEnrollmentInPreheat(Enrollment expected, Enrollment actual) { assertEquals(expected.getUid(), actual.getUid()); assertEquals(expected.getProgram().getUid(), actual.getProgram().getUid());