Avoid redundant PID lookups in transaction processing (#3061)
* Avoid redundant PID lookups in transaction processing * Add changelog * Test fix
This commit is contained in:
parent
6ad0dffd00
commit
aed718bd6f
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
type: perf
|
||||||
|
issue: 3061
|
||||||
|
title: "When ingesting Synthea data (or simnilarly structured transaction bundles) into a JPA server, a redundant
|
||||||
|
resource ID lookup has been optimized out. This should particularly speed up larger sized transactions."
|
|
@ -336,7 +336,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
ResourcePersistentId persistentId = new ResourcePersistentId(updatedEntity.getResourceId());
|
ResourcePersistentId persistentId = new ResourcePersistentId(updatedEntity.getResourceId());
|
||||||
theTransactionDetails.addResolvedResourceId(id, persistentId);
|
theTransactionDetails.addResolvedResourceId(id, persistentId);
|
||||||
if (entity.getForcedId() != null) {
|
if (entity.getForcedId() != null) {
|
||||||
myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, updatedEntity.getResourceType(), updatedEntity.getForcedId().getForcedId());
|
myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, updatedEntity.getResourceType(), updatedEntity.getForcedId().getForcedId(), updatedEntity.getDeleted());
|
||||||
}
|
}
|
||||||
|
|
||||||
theResource.setId(entity.getIdDt());
|
theResource.setId(entity.getIdDt());
|
||||||
|
@ -355,9 +355,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResourcePersistentId resourcePersistentId = new ResourcePersistentId(entity.getResourceId());
|
||||||
|
resourcePersistentId.setAssociatedResourceId(entity.getIdType(myFhirContext));
|
||||||
|
|
||||||
|
theTransactionDetails.addResolvedResourceId(resourcePersistentId.getAssociatedResourceId(), resourcePersistentId);
|
||||||
|
|
||||||
|
// Pre-cache the match URL
|
||||||
if (theIfNoneExist != null) {
|
if (theIfNoneExist != null) {
|
||||||
// Pre-cache the match URL
|
myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theIfNoneExist, resourcePersistentId);
|
||||||
myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theIfNoneExist, new ResourcePersistentId(entity.getResourceId()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the version/last updated in the resource so that interceptors get
|
// Update the version/last updated in the resource so that interceptors get
|
||||||
|
@ -390,7 +395,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
if (updatedEntity.getForcedId() != null) {
|
if (updatedEntity.getForcedId() != null) {
|
||||||
forcedId = updatedEntity.getForcedId().getForcedId();
|
forcedId = updatedEntity.getForcedId().getForcedId();
|
||||||
}
|
}
|
||||||
myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, getResourceName(), forcedId);
|
myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, getResourceName(), forcedId, null);
|
||||||
|
|
||||||
ourLog.debug(msg);
|
ourLog.debug(msg);
|
||||||
return outcome;
|
return outcome;
|
||||||
|
|
|
@ -33,8 +33,11 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
|
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
|
||||||
|
import ca.uhn.fhir.jpa.util.MemoryCacheService;
|
||||||
import ca.uhn.fhir.mdm.util.CanonicalIdentifier;
|
import ca.uhn.fhir.mdm.util.CanonicalIdentifier;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||||
|
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
@ -74,7 +77,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
||||||
private DaoRegistry myDaoRegistry;
|
private DaoRegistry myDaoRegistry;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
|
public IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
|
||||||
IResourceLookup resolvedResource;
|
IResourceLookup resolvedResource;
|
||||||
String idPart = theSourceResourceId.getIdPart();
|
String idPart = theSourceResourceId.getIdPart();
|
||||||
try {
|
try {
|
||||||
|
@ -82,7 +85,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
||||||
ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource);
|
ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource);
|
||||||
} catch (ResourceNotFoundException e) {
|
} catch (ResourceNotFoundException e) {
|
||||||
|
|
||||||
Optional<ResourceTable> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(theType, theReference, idPart, theRequest);
|
Optional<ResourceTable> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(theType, theReference, idPart, theRequest, theTransactionDetails);
|
||||||
if (!createdTableOpt.isPresent()) {
|
if (!createdTableOpt.isPresent()) {
|
||||||
|
|
||||||
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
|
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
|
||||||
|
@ -118,7 +121,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
||||||
/**
|
/**
|
||||||
* @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
|
* @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
|
||||||
*/
|
*/
|
||||||
public <T extends IBaseResource> Optional<ResourceTable> createPlaceholderTargetIfConfiguredToDoSo(Class<T> theType, IBaseReference theReference, @Nullable String theIdToAssignToPlaceholder, RequestDetails theRequest) {
|
public <T extends IBaseResource> Optional<ResourceTable> createPlaceholderTargetIfConfiguredToDoSo(Class<T> theType, IBaseReference theReference, @Nullable String theIdToAssignToPlaceholder, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
|
||||||
ResourceTable valueOf = null;
|
ResourceTable valueOf = null;
|
||||||
|
|
||||||
if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
|
if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
|
||||||
|
@ -143,6 +146,10 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
||||||
} else {
|
} else {
|
||||||
valueOf = ((ResourceTable) placeholderResourceDao.create(newResource, theRequest).getEntity());
|
valueOf = ((ResourceTable) placeholderResourceDao.create(newResource, theRequest).getEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResourcePersistentId persistentId = new ResourcePersistentId(valueOf.getResourceId(), 1L);
|
||||||
|
persistentId.setAssociatedResourceId(valueOf.getIdType(myContext));
|
||||||
|
theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.ofNullable(valueOf);
|
return Optional.ofNullable(valueOf);
|
||||||
|
|
|
@ -40,6 +40,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import com.google.common.collect.MultimapBuilder;
|
import com.google.common.collect.MultimapBuilder;
|
||||||
|
import org.apache.commons.lang3.Functions;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||||
|
@ -339,6 +340,10 @@ public class IdHelperService {
|
||||||
*/
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, ResourcePersistentId theId) {
|
public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, ResourcePersistentId theId) {
|
||||||
|
if (theId.getAssociatedResourceId() != null) {
|
||||||
|
return theId.getAssociatedResourceId();
|
||||||
|
}
|
||||||
|
|
||||||
IIdType retVal = theCtx.getVersion().newIdType();
|
IIdType retVal = theCtx.getVersion().newIdType();
|
||||||
|
|
||||||
Optional<String> forcedId = translatePidIdToForcedIdWithCache(theId);
|
Optional<String> forcedId = translatePidIdToForcedIdWithCache(theId);
|
||||||
|
@ -630,7 +635,7 @@ public class IdHelperService {
|
||||||
/**
|
/**
|
||||||
* Pre-cache a PID-to-Resource-ID mapping for later retrieval by {@link #translatePidsToForcedIds(Set)} and related methods
|
* Pre-cache a PID-to-Resource-ID mapping for later retrieval by {@link #translatePidsToForcedIds(Set)} and related methods
|
||||||
*/
|
*/
|
||||||
public void addResolvedPidToForcedId(ResourcePersistentId theResourcePersistentId, @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, @Nullable String theForcedId) {
|
public void addResolvedPidToForcedId(ResourcePersistentId theResourcePersistentId, @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, @Nullable String theForcedId, @Nullable Date theDeletedAt) {
|
||||||
if (theForcedId != null) {
|
if (theForcedId != null) {
|
||||||
if (theResourcePersistentId.getAssociatedResourceId() == null) {
|
if (theResourcePersistentId.getAssociatedResourceId() == null) {
|
||||||
populateAssociatedResourceId(theResourceType, theForcedId, theResourcePersistentId);
|
populateAssociatedResourceId(theResourceType, theForcedId, theResourcePersistentId);
|
||||||
|
@ -642,6 +647,13 @@ public class IdHelperService {
|
||||||
} else {
|
} else {
|
||||||
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.empty());
|
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!myDaoConfig.isDeleteEnabled()) {
|
||||||
|
ResourceLookup lookup = new ResourceLookup(theResourceType, theResourcePersistentId.getIdAsLong(), theDeletedAt);
|
||||||
|
String nextKey = theResourcePersistentId.toString();
|
||||||
|
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, lookup);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
@ -39,6 +39,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
|
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
|
||||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||||
|
import ca.uhn.fhir.jpa.util.MemoryCacheService;
|
||||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
@ -315,6 +316,12 @@ public class SearchParamWithInlineReferencesExtractor {
|
||||||
myContext = theContext;
|
myContext = theContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MemoryCacheService myMemoryCacheService;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
|
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
|
||||||
* matching resource.
|
* matching resource.
|
||||||
|
@ -350,15 +357,19 @@ public class SearchParamWithInlineReferencesExtractor {
|
||||||
throw new InvalidRequestException(msg);
|
throw new InvalidRequestException(msg);
|
||||||
}
|
}
|
||||||
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
|
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
|
||||||
|
|
||||||
//Attempt to find the target reference before creating a placeholder
|
//Attempt to find the target reference before creating a placeholder
|
||||||
Set<ResourcePersistentId> matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theTransactionDetails, theRequest);
|
Set<ResourcePersistentId> matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theTransactionDetails, theRequest);
|
||||||
|
|
||||||
ResourcePersistentId match;
|
ResourcePersistentId match;
|
||||||
if (matches.isEmpty()) {
|
if (matches.isEmpty()) {
|
||||||
|
|
||||||
Optional<ResourceTable> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null, theRequest);
|
Optional<ResourceTable> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null, theRequest, theTransactionDetails);
|
||||||
if (placeholderOpt.isPresent()) {
|
if (placeholderOpt.isPresent()) {
|
||||||
match = new ResourcePersistentId(placeholderOpt.get().getResourceId());
|
match = new ResourcePersistentId(placeholderOpt.get().getResourceId());
|
||||||
|
match.setAssociatedResourceId(placeholderOpt.get().getIdType(myContext));
|
||||||
|
theTransactionDetails.addResolvedMatchUrl(nextIdText, match);
|
||||||
|
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match);
|
||||||
} else {
|
} else {
|
||||||
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
|
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
|
||||||
throw new ResourceNotFoundException(msg);
|
throw new ResourceNotFoundException(msg);
|
||||||
|
@ -373,6 +384,7 @@ public class SearchParamWithInlineReferencesExtractor {
|
||||||
|
|
||||||
IIdType newId = myIdHelperService.translatePidIdToForcedId(myContext, resourceTypeString, match);
|
IIdType newId = myIdHelperService.translatePidIdToForcedId(myContext, resourceTypeString, match);
|
||||||
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
|
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
|
||||||
|
|
||||||
nextRef.setReference(newId.getValue());
|
nextRef.setReference(newId.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,17 +38,23 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.in;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
@ -68,6 +74,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
||||||
myModelConfig.setRespectVersionsForSearchIncludes(new ModelConfig().isRespectVersionsForSearchIncludes());
|
myModelConfig.setRespectVersionsForSearchIncludes(new ModelConfig().isRespectVersionsForSearchIncludes());
|
||||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(true);
|
myFhirCtx.getParserOptions().setStripVersionsFromReferences(true);
|
||||||
myDaoConfig.setTagStorageMode(new DaoConfig().getTagStorageMode());
|
myDaoConfig.setTagStorageMode(new DaoConfig().getTagStorageMode());
|
||||||
|
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets());
|
||||||
|
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(new DaoConfig().isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -782,6 +790,121 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
||||||
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
|
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransactionWithMultipleCreates() {
|
||||||
|
myDaoConfig.setMassIngestionMode(true);
|
||||||
|
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||||
|
myDaoConfig.setDeleteEnabled(false);
|
||||||
|
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||||
|
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(true);
|
||||||
|
|
||||||
|
// First pass
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
|
||||||
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
|
// 1 lookup for the match URL only
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
runInTransaction(()->assertEquals(4, myResourceTableDao.count()));
|
||||||
|
|
||||||
|
// Run it again - This time even the match URL should be cached
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
|
||||||
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
runInTransaction(()->assertEquals(7, myResourceTableDao.count()));
|
||||||
|
|
||||||
|
// Once more for good measure
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
|
||||||
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
runInTransaction(()->assertEquals(10, myResourceTableDao.count()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Bundle createTransactionWithCreatesAndOneMatchUrl() {
|
||||||
|
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId(IdType.newRandomUuid());
|
||||||
|
p.setActive(true);
|
||||||
|
bb.addTransactionCreateEntry(p);
|
||||||
|
|
||||||
|
Encounter enc = new Encounter();
|
||||||
|
enc.setSubject(new Reference(p.getId()));
|
||||||
|
enc.addParticipant().setIndividual(new Reference("Practitioner?identifier=foo|bar"));
|
||||||
|
bb.addTransactionCreateEntry(enc);
|
||||||
|
|
||||||
|
enc = new Encounter();
|
||||||
|
enc.setSubject(new Reference(p.getId()));
|
||||||
|
enc.addParticipant().setIndividual(new Reference("Practitioner?identifier=foo|bar"));
|
||||||
|
bb.addTransactionCreateEntry(enc);
|
||||||
|
|
||||||
|
return (Bundle) bb.getBundle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransactionWithMultipleCreates_PreExistingMatchUrl() {
|
||||||
|
myDaoConfig.setMassIngestionMode(true);
|
||||||
|
myDaoConfig.setMatchUrlCacheEnabled(true);
|
||||||
|
myDaoConfig.setDeleteEnabled(false);
|
||||||
|
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
|
||||||
|
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(true);
|
||||||
|
|
||||||
|
Practitioner pract = new Practitioner();
|
||||||
|
pract.addIdentifier().setSystem("foo").setValue("bar");
|
||||||
|
myPractitionerDao.create(pract);
|
||||||
|
|
||||||
|
// First pass
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
|
||||||
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
|
// 1 lookup for the match URL only
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
runInTransaction(()->assertEquals(4, myResourceTableDao.count()));
|
||||||
|
|
||||||
|
// Run it again - This time even the match URL should be cached
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
|
||||||
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
runInTransaction(()->assertEquals(7, myResourceTableDao.count()));
|
||||||
|
|
||||||
|
// Once more for good measure
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
|
||||||
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countSelectQueries());
|
||||||
|
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
|
||||||
|
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||||
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
runInTransaction(()->assertEquals(10, myResourceTableDao.count()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransactionWithTwoCreates() {
|
public void testTransactionWithTwoCreates() {
|
||||||
|
|
||||||
|
@ -1194,7 +1317,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
||||||
myCaptureQueriesListener.clear();
|
myCaptureQueriesListener.clear();
|
||||||
mySystemDao.transaction(mySrd, bundleCreator.get());
|
mySystemDao.transaction(mySrd, bundleCreator.get());
|
||||||
myCaptureQueriesListener.logSelectQueries();
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
|
assertEquals(0, myCaptureQueriesListener.countSelectQueries());
|
||||||
assertEquals(3, myCaptureQueriesListener.countInsertQueries());
|
assertEquals(3, myCaptureQueriesListener.countInsertQueries());
|
||||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
|
||||||
|
@ -1744,7 +1867,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
||||||
|
|
||||||
// Lookup the two existing IDs to make sure they are legit
|
// Lookup the two existing IDs to make sure they are legit
|
||||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||||
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
||||||
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||||
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||||
|
|
|
@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.model.entity;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
|
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
|
||||||
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
||||||
import ca.uhn.fhir.jpa.model.search.ResourceTableRoutingBinder;
|
import ca.uhn.fhir.jpa.model.search.ResourceTableRoutingBinder;
|
||||||
|
@ -39,6 +40,7 @@ import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
|
||||||
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency;
|
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency;
|
||||||
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath;
|
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath;
|
||||||
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue;
|
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -709,17 +711,32 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
|
||||||
myForcedId = theForcedId;
|
myForcedId = theForcedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IdDt getIdDt() {
|
public IdDt getIdDt() {
|
||||||
|
IdDt retVal = new IdDt();
|
||||||
|
populateId(retVal);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IIdType getIdType(FhirContext theContext) {
|
||||||
|
IIdType retVal = theContext.getVersion().newIdType();
|
||||||
|
populateId(retVal);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateId(IIdType retVal) {
|
||||||
if (getTransientForcedId() != null) {
|
if (getTransientForcedId() != null) {
|
||||||
// Avoid a join query if possible
|
// Avoid a join query if possible
|
||||||
return new IdDt(getResourceType() + '/' + getTransientForcedId() + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
retVal.setValue(getResourceType() + '/' + getTransientForcedId() + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||||
} else if (getForcedId() == null) {
|
} else if (getForcedId() == null) {
|
||||||
Long id = this.getResourceId();
|
Long id = this.getResourceId();
|
||||||
return new IdDt(getResourceType() + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
retVal.setValue(getResourceType() + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||||
} else {
|
} else {
|
||||||
String forcedId = getForcedId().getForcedId();
|
String forcedId = getForcedId().getForcedId();
|
||||||
return new IdDt(getResourceType() + '/' + forcedId + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
retVal.setValue(getResourceType() + '/' + forcedId + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
@ -37,16 +38,17 @@ public interface IResourceLinkResolver {
|
||||||
* <p>
|
* <p>
|
||||||
* This method returns an {@link IResourceLookup} so as to avoid needing to resolve the entire resource.
|
* This method returns an {@link IResourceLookup} so as to avoid needing to resolve the entire resource.
|
||||||
*
|
*
|
||||||
* @param theRequestPartitionId The partition ID of the target resource
|
* @param theRequestPartitionId The partition ID of the target resource
|
||||||
* @param theSearchParam The param that is being indexed
|
* @param theSearchParam The param that is being indexed
|
||||||
* @param theSourcePath The path within the resource where this reference was found
|
* @param theSourcePath The path within the resource where this reference was found
|
||||||
* @param theSourceResourceId The ID of the resource containing the reference to the target being resolved
|
* @param theSourceResourceId The ID of the resource containing the reference to the target being resolved
|
||||||
* @param theTypeString The type of the resource being resolved
|
* @param theTypeString The type of the resource being resolved
|
||||||
* @param theType The resource type of the target
|
* @param theType The resource type of the target
|
||||||
* @param theReference The reference being resolved
|
* @param theReference The reference being resolved
|
||||||
* @param theRequest The incoming request, if any
|
* @param theRequest The incoming request, if any
|
||||||
|
* @param theTransactionDetails
|
||||||
*/
|
*/
|
||||||
IResourceLookup findTargetResource(RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest);
|
IResourceLookup findTargetResource(RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails);
|
||||||
|
|
||||||
void validateTypeOrThrowException(Class<? extends IBaseResource> theType);
|
void validateTypeOrThrowException(Class<? extends IBaseResource> theType);
|
||||||
|
|
||||||
|
|
|
@ -377,7 +377,7 @@ public class SearchParamExtractorService {
|
||||||
* if the reference is invalid
|
* if the reference is invalid
|
||||||
*/
|
*/
|
||||||
myResourceLinkResolver.validateTypeOrThrowException(type);
|
myResourceLinkResolver.validateTypeOrThrowException(type);
|
||||||
resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull(theRequestPartitionId, theEntity, transactionDate, theRuntimeSearchParam, path, thePathAndRef, nextId, typeString, type, nextReference, theRequest);
|
resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull(theRequestPartitionId, theEntity, transactionDate, theRuntimeSearchParam, path, thePathAndRef, nextId, typeString, type, nextReference, theRequest, theTransactionDetails);
|
||||||
if (resourceLink == null) {
|
if (resourceLink == null) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -402,7 +402,17 @@ public class SearchParamExtractorService {
|
||||||
theParams.myLinks.add(resourceLink);
|
theParams.myLinks.add(resourceLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(@Nonnull RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
|
private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(@Nonnull RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
|
||||||
|
|
||||||
|
ResourcePersistentId resolvedResourceId = theTransactionDetails.getResolvedResourceId(theNextId);
|
||||||
|
if (resolvedResourceId != null) {
|
||||||
|
String targetResourceType = theNextId.getResourceType();
|
||||||
|
Long targetResourcePid = resolvedResourceId.getIdAsLong();
|
||||||
|
String targetResourceIdPart = theNextId.getIdPart();
|
||||||
|
Long targetVersion = theNextId.getVersionIdPartAsLong();
|
||||||
|
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* We keep a cache of resolved target resources. This is good since for some resource types, there
|
* We keep a cache of resolved target resources. This is good since for some resource types, there
|
||||||
* are multiple search parameters that map to the same element path within a resource (e.g.
|
* are multiple search parameters that map to the same element path within a resource (e.g.
|
||||||
|
@ -415,7 +425,7 @@ public class SearchParamExtractorService {
|
||||||
targetRequestPartitionId = RequestPartitionId.allPartitions();
|
targetRequestPartitionId = RequestPartitionId.allPartitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
IResourceLookup targetResource = myResourceLinkResolver.findTargetResource(targetRequestPartitionId, nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theReference, theRequest);
|
IResourceLookup targetResource = myResourceLinkResolver.findTargetResource(targetRequestPartitionId, nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theReference, theRequest, theTransactionDetails);
|
||||||
|
|
||||||
if (targetResource == null) {
|
if (targetResource == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -117,7 +117,7 @@ public class TransactionDetails {
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public ResourcePersistentId getResolvedResourceId(IIdType theId) {
|
public ResourcePersistentId getResolvedResourceId(IIdType theId) {
|
||||||
String idValue = theId.toVersionless().getValue();
|
String idValue = theId.toUnqualifiedVersionless().getValue();
|
||||||
return myResolvedResourceIds.get(idValue);
|
return myResolvedResourceIds.get(idValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue