Allow JPA server to restore resources and link to them in a single

transaction
This commit is contained in:
James Agnew 2018-11-02 16:45:21 -04:00 committed by Eeva Turkka
parent cc9c8dfa80
commit ba8795ed0f
6 changed files with 658 additions and 54 deletions

View File

@ -1253,6 +1253,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
IBaseResource oldResource = toResource(entity, false); IBaseResource oldResource = toResource(entity, false);
/*
* Mark the entity as not deleted - This is also done in the actual updateInternal()
* method later on so it usually doesn't matter whether we do it here, but in the
* case of a transaction with multiple PUTs we don't get there until later so
* having this here means that a transaction can have a reference in one
* resource to another resource in the same transaction that is being
* un-deleted by the transaction. Wacky use case, sure. But it's real.
*
* See SystemProviderR4Test#testTransactionReSavesPreviouslyDeletedResources
* for a test that needs this.
*/
entity.setDeleted(null);
/* /*
* If we aren't indexing, that means we're doing this inside a transaction. * If we aren't indexing, that means we're doing this inside a transaction.
* The transaction will do the actual storage to the database a bit later on, * The transaction will do the actual storage to the database a bit later on,

View File

@ -1,12 +1,25 @@
package ca.uhn.fhir.jpa.provider; package ca.uhn.fhir.jpa.provider;
import static org.hamcrest.Matchers.*; import ca.uhn.fhir.context.FhirContext;
import static org.junit.Assert.*; import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test;
import ca.uhn.fhir.jpa.rp.dstu2.*;
import java.io.InputStream; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import java.nio.charset.StandardCharsets; import ca.uhn.fhir.model.dstu2.resource.*;
import java.util.concurrent.TimeUnit; import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
import ca.uhn.fhir.model.primitive.DecimalDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
@ -17,46 +30,32 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.Test;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ca.uhn.fhir.context.FhirContext; import java.io.IOException;
import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test; import java.io.InputStream;
import ca.uhn.fhir.jpa.rp.dstu2.*; import java.nio.charset.StandardCharsets;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.model.dstu2.resource.*;
import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; import static org.hamcrest.Matchers.*;
import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; import static org.junit.Assert.*;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.util.TestUtil;
public class SystemProviderDstu2Test extends BaseJpaDstu2Test { public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu2Test.class);
private static RestfulServer myRestServer; private static RestfulServer myRestServer;
private static IGenericClient ourClient; private static IGenericClient ourClient;
private static FhirContext ourCtx; private static FhirContext ourCtx;
private static CloseableHttpClient ourHttpClient; private static CloseableHttpClient ourHttpClient;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu2Test.class);
private static Server ourServer; private static Server ourServer;
private static String ourServerBase; private static String ourServerBase;
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@Before @Before
public void beforeStartServer() throws Exception { public void beforeStartServer() throws Exception {
if (myRestServer == null) { if (myRestServer == null) {
@ -72,9 +71,23 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
OrganizationResourceProvider organizationRp = new OrganizationResourceProvider(); OrganizationResourceProvider organizationRp = new OrganizationResourceProvider();
organizationRp.setDao(myOrganizationDao); organizationRp.setDao(myOrganizationDao);
LocationResourceProvider locationRp = new LocationResourceProvider();
locationRp.setDao(myLocationDao);
BinaryResourceProvider binaryRp = new BinaryResourceProvider();
binaryRp.setDao(myBinaryDao);
DiagnosticReportResourceProvider diagnosticReportRp = new DiagnosticReportResourceProvider();
diagnosticReportRp.setDao(myDiagnosticReportDao);
DiagnosticOrderResourceProvider diagnosticOrderRp = new DiagnosticOrderResourceProvider();
diagnosticOrderRp.setDao(myDiagnosticOrderDao);
PractitionerResourceProvider practitionerRp = new PractitionerResourceProvider();
practitionerRp.setDao(myPractitionerDao);
RestfulServer restServer = new RestfulServer(ourCtx); RestfulServer restServer = new RestfulServer(ourCtx);
restServer.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(10)); restServer.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(10));
restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp, binaryRp, locationRp, diagnosticReportRp, diagnosticOrderRp, practitionerRp);
restServer.setPlainProviders(mySystemProvider); restServer.setPlainProviders(mySystemProvider);
@ -179,7 +192,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
} }
} }
@Transactional(propagation=Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Test @Test
public void testSuggestKeywords() throws Exception { public void testSuggestKeywords() throws Exception {
@ -269,6 +282,44 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
assertEquals("get-resource-counts", op.getCode()); assertEquals("get-resource-counts", op.getCode());
} }
@Test
public void testTransactionReSavesPreviouslyDeletedResources() throws IOException {
for (int i = 0; i < 10; i++) {
ourLog.info("** Beginning pass {}", i);
Bundle input = myFhirCtx.newJsonParser().parseResource(Bundle.class, IOUtils.toString(getClass().getResourceAsStream("/dstu2/createdeletebundle.json"), Charsets.UTF_8));
ourClient.transaction().withBundle(input).execute();
myPatientDao.read(new IdType("Patient/Patient1063259"));
deleteAllOfType("Binary");
deleteAllOfType("Location");
deleteAllOfType("DiagnosticReport");
deleteAllOfType("Observation");
deleteAllOfType("DiagnosticOrder");
deleteAllOfType("Practitioner");
deleteAllOfType("Patient");
deleteAllOfType("Organization");
try {
myPatientDao.read(new IdType("Patient/Patient1063259"));
fail();
} catch (ResourceGoneException e) {
// good
}
}
}
private void deleteAllOfType(String theType) {
BundleUtil.toListOfResources(myFhirCtx, ourClient.search().forResource(theType).execute())
.forEach(t -> {
ourClient.delete().resourceById(t.getIdElement()).execute();
});
}
@Test @Test
public void testTransactionFromBundle() throws Exception { public void testTransactionFromBundle() throws Exception {
InputStream bundleRes = SystemProviderDstu2Test.class.getResourceAsStream("/transaction_link_patient_eve.xml"); InputStream bundleRes = SystemProviderDstu2Test.class.getResourceAsStream("/transaction_link_patient_eve.xml");
@ -372,7 +423,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
@Test @Test
public void testTransactionSearch() throws Exception { public void testTransactionSearch() throws Exception {
for (int i = 0; i < 20; i ++) { for (int i = 0; i < 20; i++) {
Patient p = new Patient(); Patient p = new Patient();
p.addName().addFamily("PATIENT_" + i); p.addName().addFamily("PATIENT_" + i);
myPatientDao.create(p, mySrd); myPatientDao.create(p, mySrd);
@ -385,7 +436,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(1, resp.getEntry().size()); assertEquals(1, resp.getEntry().size());
Bundle respSub = (Bundle)resp.getEntry().get(0).getResource(); Bundle respSub = (Bundle) resp.getEntry().get(0).getResource();
assertEquals("self", respSub.getLink().get(0).getRelation()); assertEquals("self", respSub.getLink().get(0).getRelation());
assertEquals(ourServerBase + "/Patient", respSub.getLink().get(0).getUrl()); assertEquals(ourServerBase + "/Patient", respSub.getLink().get(0).getUrl());
assertEquals("next", respSub.getLink().get(1).getRelation()); assertEquals("next", respSub.getLink().get(1).getRelation());
@ -396,7 +447,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
@Test @Test
public void testTransactionCount() throws Exception { public void testTransactionCount() throws Exception {
for (int i = 0; i < 20; i ++) { for (int i = 0; i < 20; i++) {
Patient p = new Patient(); Patient p = new Patient();
p.addName().addFamily("PATIENT_" + i); p.addName().addFamily("PATIENT_" + i);
myPatientDao.create(p, mySrd); myPatientDao.create(p, mySrd);
@ -409,7 +460,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(1, resp.getEntry().size()); assertEquals(1, resp.getEntry().size());
Bundle respSub = (Bundle)resp.getEntry().get(0).getResource(); Bundle respSub = (Bundle) resp.getEntry().get(0).getResource();
assertEquals(20, respSub.getTotal().intValue()); assertEquals(20, respSub.getTotal().intValue());
assertEquals(0, respSub.getEntry().size()); assertEquals(0, respSub.getEntry().size());
} }
@ -423,9 +474,16 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test {
ourLog.info(output); ourLog.info(output);
assertEquals(200, http.getStatusLine().getStatusCode()); assertEquals(200, http.getStatusLine().getStatusCode());
} finally { } finally {
IOUtils.closeQuietly(http);; IOUtils.closeQuietly(http);
;
} }
} }
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
} }

View File

@ -3,9 +3,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test;
import ca.uhn.fhir.jpa.rp.r4.ObservationResourceProvider; import ca.uhn.fhir.jpa.rp.r4.*;
import ca.uhn.fhir.jpa.rp.r4.OrganizationResourceProvider;
import ca.uhn.fhir.jpa.rp.r4.PatientResourceProvider;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
@ -17,8 +15,10 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ResultSeverityEnum;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
@ -42,6 +42,7 @@ import org.junit.*;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -88,8 +89,21 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
OrganizationResourceProvider organizationRp = new OrganizationResourceProvider(); OrganizationResourceProvider organizationRp = new OrganizationResourceProvider();
organizationRp.setDao(myOrganizationDao); organizationRp.setDao(myOrganizationDao);
LocationResourceProvider locationRp = new LocationResourceProvider();
locationRp.setDao(myLocationDao);
BinaryResourceProvider binaryRp = new BinaryResourceProvider();
binaryRp.setDao(myBinaryDao);
DiagnosticReportResourceProvider diagnosticReportRp = new DiagnosticReportResourceProvider();
diagnosticReportRp.setDao(myDiagnosticReportDao);
ServiceRequestResourceProvider diagnosticOrderRp = new ServiceRequestResourceProvider();
diagnosticOrderRp.setDao(myServiceRequestDao);
PractitionerResourceProvider practitionerRp = new PractitionerResourceProvider();
practitionerRp.setDao(myPractitionerDao);
RestfulServer restServer = new RestfulServer(ourCtx); RestfulServer restServer = new RestfulServer(ourCtx);
restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp, locationRp, binaryRp, diagnosticReportRp, diagnosticOrderRp, practitionerRp);
restServer.setPlainProviders(mySystemProvider); restServer.setPlainProviders(mySystemProvider);
@ -385,6 +399,45 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus());
} }
@Test
public void testTransactionReSavesPreviouslyDeletedResources() throws IOException {
for (int i = 0; i < 10; i++) {
ourLog.info("** Beginning pass {}", i);
Bundle input = myFhirCtx.newJsonParser().parseResource(Bundle.class, IOUtils.toString(getClass().getResourceAsStream("/r4/createdeletebundle.json"), Charsets.UTF_8));
ourClient.transaction().withBundle(input).execute();
myPatientDao.read(new IdType("Patient/Patient1063259"));
deleteAllOfType("Binary");
deleteAllOfType("Location");
deleteAllOfType("DiagnosticReport");
deleteAllOfType("Observation");
deleteAllOfType("ServiceRequest");
deleteAllOfType("Practitioner");
deleteAllOfType("Patient");
deleteAllOfType("Organization");
try {
myPatientDao.read(new IdType("Patient/Patient1063259"));
fail();
} catch (ResourceGoneException e) {
// good
}
}
}
private void deleteAllOfType(String theType) {
BundleUtil.toListOfResources(myFhirCtx, ourClient.search().forResource(theType).execute())
.forEach(t -> {
ourClient.delete().resourceById(t.getIdElement()).execute();
});
}
@Test @Test
public void testTransactionDeleteWithDuplicateDeletes() throws Exception { public void testTransactionDeleteWithDuplicateDeletes() throws Exception {
myDaoConfig.setAllowInlineMatchUrlReferences(true); myDaoConfig.setAllowInlineMatchUrlReferences(true);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,37 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "Patient/Patient1063259",
"resource": {
"resourceType": "Patient",
"id": "Patient1063259",
"identifier": [
{
"system": "http://www.foo.com/fhir/identifier-type/EnterpriseId",
"value": "1063259"
}
]
},
"request": {
"method": "PUT",
"url": "Patient/Patient1063259"
}
},
{
"fullUrl": "DiagnosticReport/ReportSPATH",
"resource": {
"resourceType": "DiagnosticReport",
"id": "ReportSPATH",
"subject": {
"reference": "Patient/Patient1063259"
}
},
"request": {
"method": "PUT",
"url": "DiagnosticReport/ReportSPATH"
}
}
]
}

View File

@ -104,6 +104,11 @@
The JPA server version migrator tool now runs in a multithreaded way, allowing it to The JPA server version migrator tool now runs in a multithreaded way, allowing it to
upgrade th database faster when migration tasks require data updates. upgrade th database faster when migration tasks require data updates.
</action> </action>
<action type="fix">
A bug in the JPA server was fixed: When a resource was previously deleted,
a transaction could not be posted that both restored the deleted resource but
also contained references to the now-restored resource.
</action>
</release> </release>
<release version="3.5.0" date="2018-09-17"> <release version="3.5.0" date="2018-09-17">