Repository navigation
BED-39: prevent Hibernate premature flush #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package org.openmrs.module.bedmanagement.aop; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.anyString; | ||
| import static org.mockito.Mockito.never; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.verifyNoInteractions; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.Date; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.extension.ExtendWith; | ||
| import org.mockito.Mock; | ||
| import org.mockito.junit.jupiter.MockitoExtension; | ||
| import org.openmrs.Encounter; | ||
| import org.openmrs.User; | ||
| import org.openmrs.module.bedmanagement.entity.BedPatientAssignment; | ||
| import org.openmrs.module.bedmanagement.service.BedManagementService; | ||
|
|
||
| /** | ||
| * Unit tests for {@link EncounterWithBedPatientAssignmentSaveHandler}. | ||
| * <p> | ||
| * The handler runs inside the OpenMRS save pipeline, so the bed assignments it touches are already | ||
| * persistent and attached to the current Hibernate session. It must mutate them in place and let | ||
| * the surrounding transaction flush the changes. It must NOT call the {@code @Transactional} | ||
| * {@code saveBedPatientAssignment} / {@code deleteBedPatientAssignment} service methods: their | ||
| * commit forced an early flush of the whole session, which could trip over unrelated, | ||
| * not-yet-populated entities (e.g. a {@code VisitAttribute} whose NOT NULL {@code valueReference} | ||
| * had not been serialized yet) and abort the save with a {@code DataIntegrityViolationException}. | ||
| */ | ||
| @ExtendWith(MockitoExtension.class) | ||
| public class EncounterWithBedPatientAssignmentSaveHandlerUnitTest { | ||
|
|
||
| @Mock | ||
| private BedManagementService bedManagementService; | ||
|
|
||
| private EncounterWithBedPatientAssignmentSaveHandler handler; | ||
|
|
||
| private final User user = new User(); | ||
|
|
||
| @BeforeEach | ||
| public void setUp() { | ||
| handler = new EncounterWithBedPatientAssignmentSaveHandler(bedManagementService); | ||
| } | ||
|
|
||
| @Test | ||
| public void handleShouldUpdateAssignmentStartDatetimeInPlaceWithoutCallingTransactionalSave() { | ||
| Date encounterDatetime = new Date(); | ||
|
Check warning on line 53 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java
|
||
| Encounter encounter = new Encounter(); | ||
| encounter.setEncounterId(1001); | ||
| encounter.setUuid("enc-uuid"); | ||
| encounter.setEncounterDatetime(encounterDatetime); | ||
|
|
||
| BedPatientAssignment bpa = new BedPatientAssignment(); | ||
| when(bedManagementService.getBedPatientAssignmentByEncounter("enc-uuid", true)) | ||
| .thenReturn(Collections.singletonList(bpa)); | ||
|
|
||
| handler.handle(encounter, user, new Date(), null); | ||
|
Check warning on line 63 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java
|
||
|
|
||
| assertEquals(encounterDatetime, bpa.getStartDatetime()); | ||
| // the fix: the assignment is mutated in place, never re-saved through the | ||
| // transactional service | ||
| verify(bedManagementService, never()).saveBedPatientAssignment(any()); | ||
| } | ||
|
|
||
| @Test | ||
| public void handleShouldVoidAssignmentsInPlaceWhenEncounterVoidedWithoutCallingTransactionalDelete() { | ||
| Encounter encounter = new Encounter(); | ||
| encounter.setEncounterId(1001); | ||
| encounter.setUuid("enc-uuid"); | ||
| encounter.setEncounterDatetime(new Date()); | ||
|
Check warning on line 76 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java
|
||
| encounter.setVoided(true); | ||
|
|
||
| BedPatientAssignment bpa = new BedPatientAssignment(); | ||
| when(bedManagementService.getBedPatientAssignmentByEncounter("enc-uuid", true)) | ||
| .thenReturn(Collections.singletonList(bpa)); | ||
|
|
||
| Date voidDate = new Date(); | ||
|
Check warning on line 83 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java
|
||
| handler.handle(encounter, user, voidDate, null); | ||
|
|
||
| assertTrue(bpa.getVoided()); | ||
| assertEquals("encounter voided", bpa.getVoidReason()); | ||
| assertEquals(user, bpa.getVoidedBy()); | ||
| assertEquals(voidDate, bpa.getDateVoided()); | ||
| // the fix: the assignment is voided in place, never routed through the | ||
| // transactional service | ||
| verify(bedManagementService, never()).deleteBedPatientAssignment(any(), anyString()); | ||
| verify(bedManagementService, never()).saveBedPatientAssignment(any()); | ||
| } | ||
|
|
||
| @Test | ||
| public void handleShouldDoNothingForTransientEncounter() { | ||
| Encounter encounter = new Encounter(); // no encounterId -> not yet persisted | ||
|
|
||
| handler.handle(encounter, user, new Date(), null); | ||
|
Check warning on line 100 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java
|
||
|
|
||
| verifyNoInteractions(bedManagementService); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what date is passed in here, I would think this should always be the current datetime? (maybe it already is)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct @mogoodrich :
Yes, for a SaveHandler the date is always the current datetime
SaveHandlers are driven by RequiredDataAdvice (the MethodBeforeAdvice on save* service methods). The save path goes through the 3-arg recursivelyHandle(Class, OpenmrsObject, String) overload, whose bytecode is unambiguous:
2: invokestatic Context.getAuthenticatedUser() // -> user
5: new java/util/Date
9: invokespecial java/util/Date."":()V // -> new Date() == "now"
17: invokestatic recursivelyHandle(Class, OpenmrsObject, User, Date, String, Set)
That new Date() is then handed straight to each handler via RequiredDataHandler.handle(object, user, date, reason) (the #150 interface call at offset 71). So the date your handler receives is a fresh new Date() captured at the start of the save operation — i.e. the current datetime.
That makes bpa.setDateVoided(date) equivalent to the original deleteBedPatientAssignment's bpa.setDateVoided(new Date()) — just sourced from the timestamp the framework already created for this save, so the void instant lines up with the rest of that operation's audit metadata. No behavior change, and no risk of it being null.
One nuance worth knowing (doesn't affect us)
The other branches in RequiredDataAdvice — the Voidable/Retireable paths that drive VoidHandler/RetireHandler — behave differently: they pass the object's existing getDateVoided() / getDateRetired() if it's already set, and only fall back to new Date() otherwise (visible at offsets 253–303 and 384–434). So for those handler types, the date is "whenever it was voided," not necessarily now.
But our class is a SaveHandler, which is only ever invoked through the save path → always new Date(). So your instinct is correct: for this handler it's always the current datetime.
No code change needed — the date parameter is the right thing to use here.