Don't preserve history mode (#5306)

* No history mode

* Spotless

* Version history

* Fix changelog

* Address review comment
This commit is contained in:
James Agnew 2023-09-14 08:42:24 -04:00 committed by GitHub
parent d4a57fc123
commit de341a5bb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 489 additions and 12 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 5306
title: "A new option has been added to the JPA server JpaStorageOptions which prevents the
server from maintaining a version history. In this mode, when a new version of a resource
is added, the previous version is automatically expunged."

View File

@ -1465,12 +1465,43 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
boolean versionedTags =
getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED;
final ResourceHistoryTable historyEntry = theEntity.toHistory(versionedTags);
ResourceHistoryTable historyEntry = null;
long resourceVersion = theEntity.getVersion();
boolean reusingHistoryEntity = false;
if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) {
/*
* If we're not storing history, then just pull the current history
* table row and update it. Note that there is always a chance that
* this could return null if the current resourceVersion has been expunged
* in which case we'll still create a new one
*/
historyEntry = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(
theEntity.getResourceId(), resourceVersion - 1);
if (historyEntry != null) {
reusingHistoryEntity = true;
theEntity.populateHistoryEntityVersionAndDates(historyEntry);
if (versionedTags && theEntity.isHasTags()) {
for (ResourceTag next : theEntity.getTags()) {
historyEntry.addTag(next.getTag());
}
}
}
}
/*
* This should basically always be null unless resource history
* is disabled on this server. In that case, we'll just be reusing
* the previous version entity.
*/
if (historyEntry == null) {
historyEntry = theEntity.toHistory(versionedTags);
}
historyEntry.setEncoding(theChanged.getEncoding());
historyEntry.setResource(theChanged.getResourceBinary());
historyEntry.setResourceTextVc(theChanged.getResourceText());
ourLog.debug("Saving history entry {}", historyEntry.getIdDt());
ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId());
myResourceHistoryTableDao.save(historyEntry);
theEntity.setCurrentVersionEntity(historyEntry);
@ -1503,7 +1534,18 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
boolean haveSource = isNotBlank(source) && shouldStoreSource;
boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId;
if (haveSource || haveRequestId) {
ResourceHistoryProvenanceEntity provenance = new ResourceHistoryProvenanceEntity();
ResourceHistoryProvenanceEntity provenance = null;
if (reusingHistoryEntity) {
/*
* If version history is disabled, then we may be reusing
* a previous history entity. If that's the case, let's try
* to reuse the previous provenance entity too.
*/
provenance = historyEntry.getProvenance();
}
if (provenance == null) {
provenance = new ResourceHistoryProvenanceEntity();
}
provenance.setResourceHistoryTable(historyEntry);
provenance.setResourceTable(theEntity);
provenance.setPartitionId(theEntity.getPartitionId());

View File

@ -71,12 +71,21 @@ public class ResourceHistoryTag extends BaseTag implements Serializable {
@Column(name = "RES_ID", nullable = false)
private Long myResourceId;
public ResourceHistoryTag() {}
/**
* Constructor
*/
public ResourceHistoryTag() {
super();
}
/**
* Constructor
*/
public ResourceHistoryTag(
ResourceHistoryTable theResourceHistoryTable,
TagDefinition theTag,
PartitionablePartitionId theRequestPartitionId) {
this();
setTag(theTag);
setResource(theResourceHistoryTable);
setResourceId(theResourceHistoryTable.getResourceId());

View File

@ -874,13 +874,8 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
retVal.setResourceId(myId);
retVal.setResourceType(myResourceType);
retVal.setVersion(getVersion());
retVal.setTransientForcedId(getTransientForcedId());
retVal.setPublished(getPublishedDate());
retVal.setUpdated(getUpdatedDate());
retVal.setFhirVersion(getFhirVersion());
retVal.setDeleted(getDeleted());
retVal.setResourceTable(this);
retVal.setForcedId(getForcedId());
retVal.setPartitionId(getPartitionId());
@ -892,9 +887,23 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
}
}
populateHistoryEntityVersionAndDates(retVal);
return retVal;
}
/**
* Updates several temporal values in a {@link ResourceHistoryTable} entity which
* are pulled from this entity, including the resource version, and the
* creation, update, and deletion dates.
*/
public void populateHistoryEntityVersionAndDates(ResourceHistoryTable theResourceHistoryTable) {
theResourceHistoryTable.setVersion(getVersion());
theResourceHistoryTable.setPublished(getPublishedDate());
theResourceHistoryTable.setUpdated(getUpdatedDate());
theResourceHistoryTable.setDeleted(getDeleted());
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);

View File

@ -19,7 +19,9 @@ import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider;
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryProvenanceDao;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboStringUniqueDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityDao;
@ -292,6 +294,12 @@ public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuil
@Autowired
protected IResourceHistoryTableDao myResourceHistoryTableDao;
@Autowired
protected IResourceTagDao myResourceTagDao;
@Autowired
protected IResourceHistoryTagDao myResourceHistoryTagDao;
@Autowired
protected IResourceHistoryProvenanceDao myResourceHistoryProvenanceDao;
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired
@Qualifier("myCoverageDaoR5")
@ -315,8 +323,6 @@ public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuil
@Qualifier("myResourceProvidersR5")
protected ResourceProviderFactory myResourceProviders;
@Autowired
protected IResourceTagDao myResourceTagDao;
@Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;

View File

@ -0,0 +1,352 @@
package ca.uhn.fhir.jpa.dao.r5;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CodeType;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Meta;
import org.hl7.fhir.r5.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.annotation.Nonnull;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
public class FhirResourceDaoR5HistoryDisabledTest extends BaseJpaR5Test {
@BeforeEach
public void beforeEach() {
myStorageSettings.setResourceDbHistoryEnabled(false);
}
@AfterEach
public void afterEach() {
JpaStorageSettings defaults = new JpaStorageSettings();
myStorageSettings.setResourceDbHistoryEnabled(defaults.isResourceDbHistoryEnabled());
myStorageSettings.setTagStorageMode(defaults.getTagStorageMode());
myStorageSettings.setStoreMetaSourceInformation(defaults.getStoreMetaSourceInformation());
}
@Test
public void testPatch() {
// Setup
Patient p = new Patient();
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
// Test
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent op = patch.addParameter().setName("operation");
op.addPart().setName("type").setValue(new CodeType("replace"));
op.addPart().setName("path").setValue(new CodeType("Patient.active"));
op.addPart().setName("value").setValue(new BooleanType(false));
IIdType id2 = myPatientDao.patch(id1, null, PatchTypeEnum.FHIR_PATCH_JSON, null, patch, mySrd).getId();
// Verify
runInTransaction(() -> assertEquals(1, myResourceHistoryTableDao.count()));
assertEquals("2", id2.getVersionIdPart());
assertDoesNotThrow(() -> myPatientDao.read(id2, mySrd));
assertDoesNotThrow(() -> myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd));
assertThrows(ResourceNotFoundException.class, () -> myPatientDao.read(id2.withVersion("1"), mySrd));
p = myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd);
assertFalse(p.getActive());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
p = myPatientDao.read(id2.withVersion("2"), mySrd);
assertFalse(p.getActive());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
}
@Test
public void testUpdate() {
// Setup
Patient p = new Patient();
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
// Test
p = new Patient();
p.setId(id1);
p.addIdentifier().setValue("foo");
IIdType id2 = myPatientDao.update(p, mySrd).getId();
// Verify
runInTransaction(() -> assertEquals(1, myResourceHistoryTableDao.count()));
assertEquals("2", id2.getVersionIdPart());
assertDoesNotThrow(() -> myPatientDao.read(id2, mySrd));
assertDoesNotThrow(() -> myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd));
assertThrows(ResourceNotFoundException.class, () -> myPatientDao.read(id2.withVersion("1"), mySrd));
p = myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd);
assertEquals("foo", p.getIdentifier().get(0).getValue());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
p = myPatientDao.read(id2.withVersion("2"), mySrd);
assertEquals("foo", p.getIdentifier().get(0).getValue());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
}
@Test
public void testUpdate_InTransaction() {
// Setup
Patient p = new Patient();
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
// Test
p = new Patient();
p.setId(id1);
p.addIdentifier().setValue("foo");
BundleBuilder bb = new BundleBuilder(myFhirContext);
bb.addTransactionUpdateEntry(p);
Bundle outcome = mySystemDao.transaction(mySrd, bb.getBundleTyped());
IIdType id2 = new IdType(outcome.getEntry().get(0).getResponse().getLocation());
// Verify
runInTransaction(() -> assertEquals(1, myResourceHistoryTableDao.count()));
assertEquals("2", id2.getVersionIdPart());
assertDoesNotThrow(() -> myPatientDao.read(id2, mySrd));
assertDoesNotThrow(() -> myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd));
assertThrows(ResourceNotFoundException.class, () -> myPatientDao.read(id2.withVersion("1"), mySrd));
p = myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd);
assertEquals("foo", p.getIdentifier().get(0).getValue());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
p = myPatientDao.read(id2.withVersion("2"), mySrd);
assertEquals("foo", p.getIdentifier().get(0).getValue());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
}
@Test
public void testUpdate_CurrentVersionWasExpunged() {
// Setup
Patient p = new Patient();
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
runInTransaction(() -> myResourceHistoryTableDao.deleteAll());
// Test
p = new Patient();
p.setId(id1);
p.addIdentifier().setValue("foo");
IIdType id2 = myPatientDao.update(p, mySrd).getId();
// Verify
runInTransaction(() -> assertEquals(1, myResourceHistoryTableDao.count()));
assertEquals("2", id2.getVersionIdPart());
assertDoesNotThrow(() -> myPatientDao.read(id2, mySrd));
assertDoesNotThrow(() -> myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd));
assertThrows(ResourceNotFoundException.class, () -> myPatientDao.read(id2.withVersion("1"), mySrd));
p = myPatientDao.read(id2.toUnqualifiedVersionless(), mySrd);
assertEquals("foo", p.getIdentifier().get(0).getValue());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
p = myPatientDao.read(id2.withVersion("2"), mySrd);
assertEquals("foo", p.getIdentifier().get(0).getValue());
assertEquals("2", p.getIdElement().getVersionIdPart());
assertEquals("2", p.getMeta().getVersionId());
}
@Test
public void testUpdate_VersionedTagsMode_TagsAreCarriedForward() {
// Setup
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.VERSIONED);
Patient p = new Patient();
p.getMeta().addTag().setSystem("http://foo").setCode("bar1");
p.getMeta().addTag().setSystem("http://foo").setCode("bar2");
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
runInTransaction(()-> {
assertEquals(2, myResourceTagDao.count());
assertEquals(2, myResourceHistoryTagDao.count());
});
// Test
p = new Patient();
p.setId(id1);
p.getMeta().addTag().setSystem("http://foo").setCode("bar3");
p.addIdentifier().setValue("foo");
DaoMethodOutcome outcome = myPatientDao.update(p, mySrd);
// Verify
assertThat(toTagTokens(outcome.getResource()), containsInAnyOrder(
"http://foo|bar1", "http://foo|bar2", "http://foo|bar3"
));
p = myPatientDao.read(outcome.getId(), mySrd);
assertThat(toTagTokens(p), containsInAnyOrder(
"http://foo|bar1", "http://foo|bar2", "http://foo|bar3"
));
ourLog.info("Tag tokens: {}", toTagTokens(p));
runInTransaction(()-> {
assertEquals(3, myResourceTagDao.count());
assertEquals(3, myResourceHistoryTagDao.count());
});
}
@Test
public void testUpdate_VersionedTagsMode_TagsCanBeDeleted() {
// Setup
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.VERSIONED);
Patient p = new Patient();
p.getMeta().addTag().setSystem("http://foo").setCode("bar1");
p.getMeta().addTag().setSystem("http://foo").setCode("bar2");
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
runInTransaction(()-> {
assertEquals(2, myResourceTagDao.count());
assertEquals(2, myResourceHistoryTagDao.count());
});
// Test
Meta meta = new Meta();
meta.addTag().setSystem("http://foo").setCode("bar2");
myPatientDao.metaDeleteOperation(id1.toVersionless(), meta, mySrd);
// Verify
p = myPatientDao.read(id1.toVersionless(), mySrd);
assertThat(toTagTokens(p), containsInAnyOrder(
"http://foo|bar1"
));
ourLog.info("Tag tokens: {}", toTagTokens(p));
runInTransaction(()-> {
assertEquals(1, myResourceTagDao.count());
assertEquals(1, myResourceHistoryTagDao.count());
});
}
@Test
public void testUpdate_NonVersionedTagsMode_TagsAreCarriedForward() {
// Setup
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED);
Patient p = new Patient();
p.getMeta().addTag().setSystem("http://foo").setCode("bar1");
p.getMeta().addTag().setSystem("http://foo").setCode("bar2");
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
runInTransaction(()-> {
assertEquals(2, myResourceTagDao.count());
assertEquals(0, myResourceHistoryTagDao.count());
});
// Test
p = new Patient();
p.setId(id1);
p.getMeta().addTag().setSystem("http://foo").setCode("bar3");
p.addIdentifier().setValue("foo");
DaoMethodOutcome outcome = myPatientDao.update(p, mySrd);
// Verify
assertThat(toTagTokens(outcome.getResource()), containsInAnyOrder(
"http://foo|bar1", "http://foo|bar2", "http://foo|bar3"
));
p = myPatientDao.read(outcome.getId(), mySrd);
assertThat(toTagTokens(p), containsInAnyOrder(
"http://foo|bar1", "http://foo|bar2", "http://foo|bar3"
));
ourLog.info("Tag tokens: {}", toTagTokens(p));
runInTransaction(()-> {
assertEquals(3, myResourceTagDao.count());
assertEquals(0, myResourceHistoryTagDao.count());
});
}
@Test
public void testUpdate_NonVersionedTagsMode_TagsCanBeDeleted() {
// Setup
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED);
Patient p = new Patient();
p.getMeta().addTag().setSystem("http://foo").setCode("bar1");
p.getMeta().addTag().setSystem("http://foo").setCode("bar2");
p.setActive(true);
IIdType id1 = myPatientDao.create(p, mySrd).getId();
runInTransaction(()-> {
assertEquals(2, myResourceTagDao.count());
assertEquals(0, myResourceHistoryTagDao.count());
});
// Test
Meta meta = new Meta();
meta.addTag().setSystem("http://foo").setCode("bar2");
myPatientDao.metaDeleteOperation(id1.toVersionless(), meta, mySrd);
// Verify
p = myPatientDao.read(id1.toVersionless(), mySrd);
assertThat(toTagTokens(p), containsInAnyOrder(
"http://foo|bar1"
));
ourLog.info("Tag tokens: {}", toTagTokens(p));
runInTransaction(()-> {
assertEquals(1, myResourceTagDao.count());
assertEquals(0, myResourceHistoryTagDao.count());
});
}
@Test
public void testUpdate_ProvenanceIsUpdatedInPlace() {
// Setup
myStorageSettings.setStoreMetaSourceInformation(JpaStorageSettings.StoreMetaSourceInformationEnum.SOURCE_URI_AND_REQUEST_ID);
Patient p = new Patient();
p.getMeta().setSource("source-1");
p.setActive(true);
when(mySrd.getRequestId()).thenReturn("request-id-1");
IIdType id1 = myPatientDao.create(p, mySrd).getId();
runInTransaction(()-> assertEquals(1, myResourceHistoryProvenanceDao.count()));
// Test
p = new Patient();
p.setId(id1);
p.addIdentifier().setValue("foo");
p.getMeta().setSource("source-2");
p.setActive(true);
when(mySrd.getRequestId()).thenReturn("request-id-2");
DaoMethodOutcome outcome = myPatientDao.update(p, mySrd);
// Verify
assertEquals("source-2#request-id-2", ((Patient)outcome.getResource()).getMeta().getSource());
p = myPatientDao.read(outcome.getId(), mySrd);
assertEquals("source-2#request-id-2", p.getMeta().getSource());
runInTransaction(()-> assertEquals(1, myResourceHistoryProvenanceDao.count()));
}
@Nonnull
private static List<String> toTagTokens(IBaseResource resource) {
List<String> tags = resource.getMeta()
.getTag()
.stream()
.map(t -> t.getSystem() + "|" + t.getCode())
.toList();
return tags;
}
}

View File

@ -326,6 +326,10 @@ public class JpaStorageSettings extends StorageSettings {
* Applies to MDM links.
*/
private boolean myNonResourceDbHistoryEnabled = true;
/**
* Since 7.0.0
*/
private boolean myResourceHistoryDbEnabled = true;
/**
* Constructor
@ -2302,9 +2306,50 @@ public class JpaStorageSettings extends StorageSettings {
myJobFastTrackingEnabled = theJobFastTrackingEnabled;
}
/**
* If set to {@literal false} (default is {@literal true}), the server will not
* preserve resource history and will delete previous versions of resources when
* a resource is updated.
* <p>
* Note that this does not make the server completely version-less. Resources will
* still have a version number which increases every time a resource is modified,
* operations such as vread and history will still be supported, and features
* such as ETags and ETag-aware updates will still work. Disabling this setting
* simply means that when a resource is updated, the previous version of the
* resource will be expunged. This could be done in order to conserve space, or
* in cases where there is no business value to storing previous versions of
* resources.
* </p>
*
* @since 7.0.0
*/
public boolean isResourceDbHistoryEnabled() {
return myResourceHistoryDbEnabled;
}
/**
* If set to {@literal false} (default is {@literal true}), the server will not
* preserve resource history and will delete previous versions of resources when
* a resource is updated.
* <p>
* Note that this does not make the server completely version-less. Resources will
* still have a version number which increases every time a resource is modified,
* operations such as vread and history will still be supported, and features
* such as ETags and ETag-aware updates will still work. Disabling this setting
* simply means that when a resource is updated, the previous version of the
* resource will be expunged. This could be done in order to conserve space, or
* in cases where there is no business value to storing previous versions of
* resources.
* </p>
*
* @since 7.0.0
*/
public void setResourceDbHistoryEnabled(boolean theResourceHistoryEnabled) {
myResourceHistoryDbEnabled = theResourceHistoryEnabled;
}
/**
* This setting controls whether MdmLink and other non-resource DB history is enabled.
* This setting controls whether non-resource DB history is enabled
* <p/>
* By default, this is enabled unless explicitly disabled.
*
@ -2315,6 +2360,14 @@ public class JpaStorageSettings extends StorageSettings {
return myNonResourceDbHistoryEnabled;
}
/**
* This setting controls whether MdmLink and other non-resource DB history is enabled.
* <p/>
* By default, this is enabled unless explicitly disabled.
*
* @param theNonResourceDbHistoryEnabled Whether non-resource DB history is enabled (default is true);
* @since 6.6.0
*/
public void setNonResourceDbHistoryEnabled(boolean theNonResourceDbHistoryEnabled) {
myNonResourceDbHistoryEnabled = theNonResourceDbHistoryEnabled;
}