From b934abb297a3d1cb7a981312fc59846762ea8e88 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 14 Jun 2021 13:08:19 -0400 Subject: [PATCH] Impropve transaction Performance (#2717) * Work on changes * Work on perf * Work on testing * Work on perf * Work on perf * Work on fix * Work on perf * Ongoing work * Add changelog * Additional docs * Test fixes * Address review comments * Test fix --- .../5_5_0/2717-add-tag-versioning-mode.yaml | 7 + .../2717-transaction-write-pre-caching.yaml | 6 + .../ca/uhn/fhir/jpa/api/config/DaoConfig.java | 76 +- .../api/dao/IFhirResourceDaoSubscription.java | 3 +- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 104 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 72 +- .../jpa/dao/BaseTransactionProcessor.java | 31 +- .../dao/FhirResourceDaoSubscriptionDstu2.java | 4 +- .../fhir/jpa/dao/MatchResourceUrlService.java | 65 +- .../fhir/jpa/dao/TransactionProcessor.java | 295 +++++- .../FhirResourceDaoSubscriptionDstu3.java | 4 +- .../fhir/jpa/dao/index/IdHelperService.java | 123 ++- ...rchParamWithInlineReferencesExtractor.java | 10 +- .../dao/r4/FhirResourceDaoSubscriptionR4.java | 6 +- .../dao/r5/FhirResourceDaoSubscriptionR5.java | 6 +- .../jpa/sp/SearchParamPresenceSvcImpl.java | 3 +- .../CircularQueueCaptureQueriesListener.java | 3 + .../java/ca/uhn/fhir/jpa/util/SqlQuery.java | 5 - .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 25 +- .../jpa/dao/TransactionProcessorTest.java | 12 + .../jpa/dao/dstu2/FhirSystemDaoDstu2Test.java | 16 +- .../jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 107 --- .../jpa/dao/r4/BasePartitioningR4Test.java | 2 + .../dao/r4/FhirResourceDaoR4CreateTest.java | 2 +- .../r4/FhirResourceDaoR4QueryCountTest.java | 369 ++++++- ...ourceDaoR4SearchCustomSearchParamTest.java | 2 + .../FhirResourceDaoR4SearchOptimizedTest.java | 5 +- .../jpa/dao/r4/FhirResourceDaoR4TagsTest.java | 192 ++++ .../jpa/dao/r4/FhirResourceDaoR4Test.java | 2 +- .../fhir/jpa/dao/r4/FhirSystemDaoR4Test.java | 32 +- .../jpa/dao/r4/PartitioningSqlR4Test.java | 133 ++- .../stresstest/GiantTransactionPerfTest.java | 7 + .../transaction-perf-bundle-smallchanges.json | 904 ++++++++++++++++++ .../resources/r4/transaction-perf-bundle.json | 904 ++++++++++++++++++ .../ResourceIndexedSearchParamToken.java | 4 +- .../fhir/jpa/model/entity/ResourceTable.java | 4 +- .../server/storage/ResourcePersistentId.java | 12 +- .../server/storage/TransactionDetails.java | 41 +- 38 files changed, 3217 insertions(+), 381 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-add-tag-versioning-mode.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-transaction-write-pre-caching.yaml create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TagsTest.java create mode 100644 hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle-smallchanges.json create mode 100644 hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle.json diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-add-tag-versioning-mode.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-add-tag-versioning-mode.yaml new file mode 100644 index 00000000000..89dc13e02f1 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-add-tag-versioning-mode.yaml @@ -0,0 +1,7 @@ +--- +type: perf +issue: 2717 +title: "A new setting has been added to the DaoConfig called Tag Versioning Mode. This setting controls whether a single collection of + tags/profiles/security labels is maintained across all versions of a single resource, or whether each version of the + resource maintains its own independent collection. Previously each version always maintained an independent collection, + which is useful sometimes, but is often not useful and can affect performance." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-transaction-write-pre-caching.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-transaction-write-pre-caching.yaml new file mode 100644 index 00000000000..866fb5a28ba --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2717-transaction-write-pre-caching.yaml @@ -0,0 +1,6 @@ +--- +type: perf +issue: 2717 +title: "FHIR transactions in the JPA server that perform writes will now aggressively pre-fetch as many entities + as possible at the very start of transaction processing. This can drastically reduce the number of + round-trips, especially as the number of resources in a transaction gets bigger." diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index 6a390358484..a60afe0dcab 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -84,6 +84,10 @@ public class DaoConfig { */ public static final boolean DEFAULT_ENABLE_TASKS = true; public static final int DEFAULT_MAXIMUM_INCLUDES_TO_LOAD_PER_PAGE = 1000; + /** + * @since 5.5.0 + */ + public static final TagStorageModeEnum DEFAULT_TAG_STORAGE_MODE = TagStorageModeEnum.VERSIONED; /** * Default value for {@link #setMaximumSearchResultCountInTransaction(Integer)} * @@ -129,6 +133,7 @@ public class DaoConfig { private SearchTotalModeEnum myDefaultTotalMode = null; private int myEverythingIncludesFetchPageSize = 50; private int myBulkImportMaxRetryCount = 10; + private TagStorageModeEnum myTagStorageMode = DEFAULT_TAG_STORAGE_MODE; /** * update setter javadoc if default changes */ @@ -219,7 +224,7 @@ public class DaoConfig { /** * @since 5.4.0 */ - private boolean myMatchUrlCache; + private boolean myMatchUrlCacheEnabled; /** * @since 5.5.0 */ @@ -266,6 +271,26 @@ public class DaoConfig { } } + /** + * Sets the tag storage mode for the server. Default is {@link TagStorageModeEnum#VERSIONED}. + * + * @since 5.5.0 + */ + @Nonnull + public TagStorageModeEnum getTagStorageMode() { + return myTagStorageMode; + } + + /** + * Sets the tag storage mode for the server. Default is {@link TagStorageModeEnum#VERSIONED}. + * + * @since 5.5.0 + */ + public void setTagStorageMode(@Nonnull TagStorageModeEnum theTagStorageMode) { + Validate.notNull(theTagStorageMode, "theTagStorageMode must not be null"); + myTagStorageMode = theTagStorageMode; + } + /** * Specifies the maximum number of times that a chunk will be retried during bulk import * processes before giving up. @@ -421,9 +446,25 @@ public class DaoConfig { * Default is false * * @since 5.4.0 + * @deprecated Deprecated in 5.5.0. Use {@link #isMatchUrlCacheEnabled()} instead (the name of this method is misleading) */ + @Deprecated public boolean getMatchUrlCache() { - return myMatchUrlCache; + return myMatchUrlCacheEnabled; + } + + /** + * If enabled, resolutions for match URLs (e.g. conditional create URLs, conditional update URLs, etc) will be + * cached in an in-memory cache. This cache can have a noticeable improvement on write performance on servers + * where conditional operations are frequently performed, but note that this cache will not be + * invalidated based on updates to resources so this may have detrimental effects. + *

+ * Default is false + * + * @since 5.5.0 + */ + public boolean isMatchUrlCacheEnabled() { + return getMatchUrlCache(); } /** @@ -435,9 +476,25 @@ public class DaoConfig { * Default is false * * @since 5.4.0 + * @deprecated Deprecated in 5.5.0. Use {@link #setMatchUrlCacheEnabled(boolean)} instead (the name of this method is misleading) */ + @Deprecated public void setMatchUrlCache(boolean theMatchUrlCache) { - myMatchUrlCache = theMatchUrlCache; + myMatchUrlCacheEnabled = theMatchUrlCache; + } + + /** + * If enabled, resolutions for match URLs (e.g. conditional create URLs, conditional update URLs, etc) will be + * cached in an in-memory cache. This cache can have a noticeable improvement on write performance on servers + * where conditional operations are frequently performed, but note that this cache will not be + * invalidated based on updates to resources so this may have detrimental effects. + *

+ * Default is false + * + * @since 5.5.0 + */ + public void setMatchUrlCacheEnabled(boolean theMatchUrlCache) { + setMatchUrlCache(theMatchUrlCache); } /** @@ -2548,4 +2605,17 @@ public class DaoConfig { ANY } + public enum TagStorageModeEnum { + + /** + * A separate set of tags is stored for each resource version + */ + VERSIONED, + + /** + * A single set of tags is shared by all resource versions + */ + NON_VERSIONED + + } } diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoSubscription.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoSubscription.java index ca35c19b724..99e3f1133b6 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoSubscription.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoSubscription.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.api.dao; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -26,6 +27,6 @@ import org.hl7.fhir.instance.model.api.IIdType; public interface IFhirResourceDaoSubscription extends IFhirResourceDao { - Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest); + Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest, TransactionDetails theTransactionDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index eabc69b082c..9021c1a4ce5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -58,7 +58,6 @@ import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.util.AddRemoveCount; -import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; @@ -84,6 +83,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.MetaUtil; @@ -137,6 +137,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -185,6 +186,7 @@ public abstract class BaseHapiFhirDao extends BaseStora private static final Logger ourLog = LoggerFactory.getLogger(BaseHapiFhirDao.class); private static final Map ourRetrievalContexts = new HashMap<>(); private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; + private static final String TRANSACTION_DETAILS_CACHE_KEY_EXISTING_SEARCH_PARAMS = BaseHapiFhirDao.class.getName() + "_EXISTING_SEARCH_PARAMS"; private static boolean ourValidationDisabledForUnitTest; private static boolean ourDisableIncrementOnUpdateForUnitTest = false; @@ -394,6 +396,7 @@ public abstract class BaseHapiFhirDao extends BaseStora @Autowired public void setContext(FhirContext theContext) { + super.myFhirContext = theContext; myContext = theContext; } @@ -668,6 +671,10 @@ public abstract class BaseHapiFhirDao extends BaseStora // Don't check existing - We'll rely on the SHA256 hash only + } else if (theEntity.getVersion() == 1L && theEntity.getCurrentVersionEntity() == null) { + + // No previous version if this is the first version + } else { ResourceHistoryTable currentHistoryVersion = theEntity.getCurrentVersionEntity(); if (currentHistoryVersion == null) { @@ -791,27 +798,23 @@ public abstract class BaseHapiFhirDao extends BaseStora res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); IDao.RESOURCE_PID.put(res, theEntity.getResourceId()); - Collection tags = theTagList; - - if (theEntity.isHasTags()) { - for (BaseTag next : tags) { - switch (next.getTag().getTagType()) { - case PROFILE: - res.getMeta().addProfile(next.getTag().getCode()); - break; - case SECURITY_LABEL: - IBaseCoding sec = res.getMeta().addSecurity(); - sec.setSystem(next.getTag().getSystem()); - sec.setCode(next.getTag().getCode()); - sec.setDisplay(next.getTag().getDisplay()); - break; - case TAG: - IBaseCoding tag = res.getMeta().addTag(); - tag.setSystem(next.getTag().getSystem()); - tag.setCode(next.getTag().getCode()); - tag.setDisplay(next.getTag().getDisplay()); - break; - } + for (BaseTag next : theTagList) { + switch (next.getTag().getTagType()) { + case PROFILE: + res.getMeta().addProfile(next.getTag().getCode()); + break; + case SECURITY_LABEL: + IBaseCoding sec = res.getMeta().addSecurity(); + sec.setSystem(next.getTag().getSystem()); + sec.setCode(next.getTag().getCode()); + sec.setDisplay(next.getTag().getDisplay()); + break; + case TAG: + IBaseCoding tag = res.getMeta().addTag(); + tag.setSystem(next.getTag().getSystem()); + tag.setCode(next.getTag().getCode()); + tag.setDisplay(next.getTag().getDisplay()); + break; } } @@ -912,7 +915,7 @@ public abstract class BaseHapiFhirDao extends BaseStora // 1. get resource, it's encoding and the tags if any byte[] resourceBytes; ResourceEncodingEnum resourceEncoding; - Collection myTagList; + Collection tagList = Collections.emptyList(); long version; String provenanceSourceUri = null; String provenanceRequestId = null; @@ -921,10 +924,14 @@ public abstract class BaseHapiFhirDao extends BaseStora ResourceHistoryTable history = (ResourceHistoryTable) theEntity; resourceBytes = history.getResource(); resourceEncoding = history.getEncoding(); - if (history.isHasTags()) { - myTagList = history.getTags(); + if (getConfig().getTagStorageMode() == DaoConfig.TagStorageModeEnum.VERSIONED) { + if (history.isHasTags()) { + tagList = history.getTags(); + } } else { - myTagList = Collections.emptyList(); + if (history.getResourceTable().isHasTags()) { + tagList = history.getResourceTable().getTags(); + } } version = history.getVersion(); if (history.getProvenance() != null) { @@ -948,9 +955,9 @@ public abstract class BaseHapiFhirDao extends BaseStora resourceBytes = history.getResource(); resourceEncoding = history.getEncoding(); if (resource.isHasTags()) { - myTagList = resource.getTags(); + tagList = resource.getTags(); } else { - myTagList = Collections.emptyList(); + tagList = Collections.emptyList(); } version = history.getVersion(); if (history.getProvenance() != null) { @@ -966,9 +973,9 @@ public abstract class BaseHapiFhirDao extends BaseStora provenanceRequestId = view.getProvenanceRequestId(); provenanceSourceUri = view.getProvenanceSourceUri(); if (theTagList == null) - myTagList = new HashSet<>(); + tagList = new HashSet<>(); else - myTagList = theTagList; + tagList = theTagList; } else { // something wrong return null; @@ -980,7 +987,7 @@ public abstract class BaseHapiFhirDao extends BaseStora // 3. Use the appropriate custom type if one is specified in the context Class resourceType = theResourceType; if (myContext.hasDefaultTypeForProfile()) { - for (BaseTag nextTag : myTagList) { + for (BaseTag nextTag : tagList) { if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) { String profile = nextTag.getTag().getCode(); if (isNotBlank(profile)) { @@ -1030,10 +1037,10 @@ public abstract class BaseHapiFhirDao extends BaseStora // 5. fill MetaData if (retVal instanceof IResource) { IResource res = (IResource) retVal; - retVal = populateResourceMetadataHapi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version); + retVal = populateResourceMetadataHapi(resourceType, theEntity, tagList, theForHistoryOperation, res, version); } else { IAnyResource res = (IAnyResource) retVal; - retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version); + retVal = populateResourceMetadataRi(resourceType, theEntity, tagList, theForHistoryOperation, res, version); } // 6. Handle source (provenance) @@ -1152,14 +1159,22 @@ public abstract class BaseHapiFhirDao extends BaseStora changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); } else { + // CREATE or UPDATE - existingParams = new ResourceIndexedSearchParams(entity); + + IdentityHashMap existingSearchParams = theTransactionDetails.getOrCreateUserData(TRANSACTION_DETAILS_CACHE_KEY_EXISTING_SEARCH_PARAMS, () -> new IdentityHashMap<>()); + existingParams = existingSearchParams.get(entity); + if (existingParams == null) { + existingParams = new ResourceIndexedSearchParams(entity); + existingSearchParams.put(entity, existingParams); + } entity.setDeleted(null); - if (thePerformIndexing) { + // TODO: is this IF statement always true? Try removing it + if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) { newParams = new ResourceIndexedSearchParams(); - mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, theTransactionDetails, entity, theResource, existingParams, theRequest); + mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing); changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); @@ -1175,7 +1190,7 @@ public abstract class BaseHapiFhirDao extends BaseStora // to match a resource and then update it in a way that it no longer // matches. We could certainly make this configurable though in the // future. - if (entity.getVersion() <= 1L && entity.getCreatedByMatchUrl() != null) { + if (entity.getVersion() <= 1L && entity.getCreatedByMatchUrl() != null && thePerformIndexing) { verifyMatchUrlForConditionalCreate(theResource, entity.getCreatedByMatchUrl(), entity, newParams); } @@ -1205,7 +1220,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } - if (thePerformIndexing && changed != null && !changed.isChanged() && !theForceUpdate && myConfig.isSuppressUpdatesWithNoChange()) { + if (thePerformIndexing && changed != null && !changed.isChanged() && !theForceUpdate && myConfig.isSuppressUpdatesWithNoChange() && (entity.getVersion() > 1 || theUpdateVersion)) { ourLog.debug("Resource {} has not changed", entity.getIdDt().toUnqualified().getValue()); if (theResource != null) { updateResourceMetadata(entity, theResource); @@ -1245,7 +1260,8 @@ public abstract class BaseHapiFhirDao extends BaseStora * Create history entry */ if (theCreateNewHistoryEntry) { - final ResourceHistoryTable historyEntry = entity.toHistory(); + boolean versionedTags = getConfig().getTagStorageMode() == DaoConfig.TagStorageModeEnum.VERSIONED; + final ResourceHistoryTable historyEntry = entity.toHistory(versionedTags); historyEntry.setEncoding(changed.getEncoding()); historyEntry.setResource(changed.getResource()); @@ -1575,6 +1591,11 @@ public abstract class BaseHapiFhirDao extends BaseStora // nothing yet } + @VisibleForTesting + public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) { + myConfig = theDaoConfig; + } + private class AddTagDefinitionToCacheAfterCommitSynchronization implements TransactionSynchronization { private final TagDefinition myTagDefinition; @@ -1726,11 +1747,6 @@ public abstract class BaseHapiFhirDao extends BaseStora ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest; } - @VisibleForTesting - public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) { - myConfig = theDaoConfig; - } - /** * Do not call this method outside of unit tests */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index b334a1dfa24..379bf1becda 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome; import ca.uhn.fhir.jpa.dao.expunge.DeleteExpungeService; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.entity.BaseHasResource; @@ -57,7 +58,6 @@ import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; -import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.dstu2.resource.ListResource; @@ -91,6 +91,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.util.ObjectUtil; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.ReflectionUtil; @@ -257,9 +258,10 @@ public abstract class BaseHapiFhirResourceDao extends B entity.setResourceType(toResourceName(theResource)); entity.setPartitionId(theRequestPartitionId); entity.setCreatedByMatchUrl(theIfNoneExist); + entity.setVersion(1); if (isNotBlank(theIfNoneExist)) { - Set match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theRequest); + Set match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theTransactionDetails, theRequest); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); throw new PreconditionFailedException(msg); @@ -338,9 +340,17 @@ public abstract class BaseHapiFhirResourceDao extends B doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams); // Perform actual DB update - ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, false, thePerformIndexing); + ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, false, theTransactionDetails, false, thePerformIndexing); + + IIdType id = myFhirContext.getVersion().newIdType().setValue(updatedEntity.getIdDt().toUnqualifiedVersionless().getValue()); + ResourcePersistentId persistentId = new ResourcePersistentId(updatedEntity.getResourceId()); + theTransactionDetails.addResolvedResourceId(id, persistentId); + if (entity.getForcedId() != null) { + myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, updatedEntity.getResourceType(), updatedEntity.getForcedId().getForcedId()); + } theResource.setId(entity.getIdDt()); + if (serverAssignedId) { switch (getConfig().getResourceClientIdStrategy()) { case NOT_ALLOWED: @@ -357,16 +367,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (theIfNoneExist != null) { // Pre-cache the match URL - myMatchResourceUrlService.matchUrlResolved(theIfNoneExist, new ResourcePersistentId(entity.getResourceId())); - } - - /* - * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), - * we'll manually increase the version. This is important because we want the updated version number - * to be reflected in the resource shared with interceptors - */ - if (!thePerformIndexing) { - incrementId(theResource, entity, theResource.getIdElement()); + myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theIfNoneExist, new ResourcePersistentId(entity.getResourceId())); } // Update the version/last updated in the resource so that interceptors get @@ -399,9 +400,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (updatedEntity.getForcedId() != null) { forcedId = updatedEntity.getForcedId().getForcedId(); } - if (myIdHelperService != null) { - myIdHelperService.addResolvedPidToForcedId(new ResourcePersistentId(updatedEntity.getResourceId()), theRequestPartitionId, getResourceName(), forcedId); - } + myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, getResourceName(), forcedId); ourLog.debug(msg); return outcome; @@ -443,7 +442,7 @@ public abstract class BaseHapiFhirResourceDao extends B validateIdPresentForDelete(theId); validateDeleteEnabled(); - final ResourceTable entity = readEntityLatestVersion(theId, theRequestDetails); + final ResourceTable entity = readEntityLatestVersion(theId, theRequestDetails, theTransactionDetails); if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) { throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version"); } @@ -913,7 +912,7 @@ public abstract class BaseHapiFhirResourceDao extends B throw new ResourceNotFoundException(theResourceId); } - ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest); + ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest, transactionDetails); if (latestVersion.getVersion() != entity.getVersion()) { doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails); } else { @@ -948,7 +947,7 @@ public abstract class BaseHapiFhirResourceDao extends B throw new ResourceNotFoundException(theResourceId); } - ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest); + ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest, transactionDetails); if (latestVersion.getVersion() != entity.getVersion()) { doMetaDelete(theMetaDel, entity, theRequest, transactionDetails); } else { @@ -1007,14 +1006,14 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) { - return myTransactionService.execute(theRequest, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest)); + return myTransactionService.execute(theRequest, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest, new TransactionDetails())); } - private DaoMethodOutcome doPatch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) { + private DaoMethodOutcome doPatch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest, TransactionDetails theTransactionDetails) { ResourceTable entityToUpdate; if (isNotBlank(theConditionalUrl)) { - Set match = myMatchResourceUrlService.processMatchUrl(theConditionalUrl, myResourceType, theRequest); + Set match = myMatchResourceUrlService.processMatchUrl(theConditionalUrl, myResourceType, theTransactionDetails, theRequest); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "PATCH", theConditionalUrl, match.size()); throw new PreconditionFailedException(msg); @@ -1027,7 +1026,7 @@ public abstract class BaseHapiFhirResourceDao extends B } } else { - entityToUpdate = readEntityLatestVersion(theId, theRequest); + entityToUpdate = readEntityLatestVersion(theId, theRequest, theTransactionDetails); if (theId.hasVersionIdPart()) { if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) { throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch"); @@ -1064,7 +1063,7 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public void start() { assert getConfig() != null; - + ourLog.debug("Starting resource DAO for type: {}", getResourceName()); myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class); myTxTemplate = new TransactionTemplate(myPlatformTransactionManager); @@ -1252,15 +1251,19 @@ public abstract class BaseHapiFhirResourceDao extends B } @Nonnull - protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequestDetails) { + protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, getResourceName()); - return readEntityLatestVersion(theId, requestPartitionId); + return readEntityLatestVersion(theId, requestPartitionId, theTransactionDetails); } @Nonnull - private ResourceTable readEntityLatestVersion(IIdType theId, @Nullable RequestPartitionId theRequestPartitionId) { + private ResourceTable readEntityLatestVersion(IIdType theId, @Nullable RequestPartitionId theRequestPartitionId, TransactionDetails theTransactionDetails) { validateResourceTypeAndThrowInvalidRequestException(theId); + if (theTransactionDetails.isResolvedResourceIdEmpty(theId.toUnqualifiedVersionless())) { + throw new ResourceNotFoundException(theId); + } + ResourcePersistentId persistentId = myIdHelperService.resolveResourcePersistentIds(theRequestPartitionId, getResourceName(), theId.getIdPart()); ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId.getId()); if (entity == null) { @@ -1569,7 +1572,7 @@ public abstract class BaseHapiFhirResourceDao extends B IIdType resourceId; if (isNotBlank(theMatchUrl)) { - Set match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theRequest); + Set match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theTransactionDetails, theRequest); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size()); throw new PreconditionFailedException(msg); @@ -1582,7 +1585,7 @@ public abstract class BaseHapiFhirResourceDao extends B // Pre-cache the match URL if (outcome.getPersistentId() != null) { - myMatchResourceUrlService.matchUrlResolved(theMatchUrl, outcome.getPersistentId()); + myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theMatchUrl, outcome.getPersistentId()); } return outcome; @@ -1610,7 +1613,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (!create) { try { - entity = readEntityLatestVersion(resourceId, requestPartitionId); + entity = readEntityLatestVersion(resourceId, requestPartitionId, theTransactionDetails); } catch (ResourceNotFoundException e) { create = true; } @@ -1692,6 +1695,8 @@ public abstract class BaseHapiFhirResourceDao extends B @Override @Transactional(propagation = Propagation.SUPPORTS) public MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequest) { + TransactionDetails transactionDetails = new TransactionDetails(); + if (theRequest != null) { ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, theResource, null, theId); notifyInterceptors(RestOperationTypeEnum.VALIDATE, requestDetails); @@ -1701,7 +1706,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (theId == null || theId.hasIdPart() == false) { throw new InvalidRequestException("No ID supplied. ID is required when validating with mode=DELETE"); } - final ResourceTable entity = readEntityLatestVersion(theId, theRequest); + final ResourceTable entity = readEntityLatestVersion(theId, theRequest, transactionDetails); // Validate that there are no resources pointing to the candidate that // would prevent deletion @@ -1799,6 +1804,11 @@ public abstract class BaseHapiFhirResourceDao extends B } } + @VisibleForTesting + public void setIdHelperSvcForUnitTest(IdHelperService theIdHelperService) { + myIdHelperService = theIdHelperService; + } + private static class IdChecker implements IValidatorModule { private final ValidationModeEnum myMode; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index c78b2522ff2..7fed32b7455 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -145,6 +145,20 @@ public abstract class BaseTransactionProcessor { @Autowired private InMemoryResourceMatcher myInMemoryResourceMatcher; + @VisibleForTesting + public void setDaoConfig(DaoConfig theDaoConfig) { + myDaoConfig = theDaoConfig; + } + + public ITransactionProcessorVersionAdapter getVersionAdapter() { + return myVersionAdapter; + } + + @VisibleForTesting + public void setVersionAdapter(ITransactionProcessorVersionAdapter theVersionAdapter) { + myVersionAdapter = theVersionAdapter; + } + @PostConstruct public void start() { ourLog.trace("Starting transaction processor"); @@ -287,11 +301,6 @@ public abstract class BaseTransactionProcessor { } } - @VisibleForTesting - public void setVersionAdapter(ITransactionProcessorVersionAdapter theVersionAdapter) { - myVersionAdapter = theVersionAdapter; - } - @VisibleForTesting public void setTxManager(PlatformTransactionManager theTxManager) { myTxManager = theTxManager; @@ -582,8 +591,8 @@ public abstract class BaseTransactionProcessor { myModelConfig = theModelConfig; } - private Map doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set theAllIds, - Map theIdSubstitutions, Map theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) { + protected Map doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set theAllIds, + Map theIdSubstitutions, Map theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) { theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts( Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, @@ -1067,7 +1076,7 @@ public abstract class BaseTransactionProcessor { if (!nextId.hasIdPart()) { if (resourceReference.getResource() != null) { IIdType targetId = resourceReference.getResource().getIdElement(); - if (targetId.getValue() == null) { + if (targetId.getValue() == null || targetId.getValue().startsWith("#")) { // This means it's a contained resource continue; } else if (theIdSubstitutions.containsValue(targetId)) { @@ -1258,7 +1267,6 @@ public abstract class BaseTransactionProcessor { return dao; } - private String toResourceName(Class theResourceType) { return myContext.getResourceType(theResourceType); } @@ -1318,11 +1326,6 @@ public abstract class BaseTransactionProcessor { return null; } - @VisibleForTesting - public void setDaoConfig(DaoConfig theDaoConfig) { - myDaoConfig = theDaoConfig; - } - public interface ITransactionProcessorVersionAdapter { void setResponseStatus(BUNDLEENTRY theBundleEntry, String theStatus); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java index 7de8da80775..e7bbf79a09c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java @@ -51,8 +51,8 @@ public class FhirResourceDaoSubscriptionDstu2 extends BaseHapiFhirResourceDao Set processMatchUrl(String theMatchUrl, Class theResourceType, RequestDetails theRequest) { - if (myDaoConfig.getMatchUrlCache()) { - ResourcePersistentId existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl); - if (existing != null) { - return Collections.singleton(existing); + public Set processMatchUrl(String theMatchUrl, Class theResourceType, TransactionDetails theTransactionDetails, RequestDetails theRequest) { + String resourceType = myContext.getResourceType(theResourceType); + String matchUrl = massageForStorage(resourceType, theMatchUrl); + + ResourcePersistentId resolvedInTransaction = theTransactionDetails.getResolvedMatchUrls().get(matchUrl); + if (resolvedInTransaction != null) { + if (resolvedInTransaction == TransactionDetails.NOT_FOUND) { + return Collections.emptySet(); + } else { + return Collections.singleton(resolvedInTransaction); } } + ResourcePersistentId resolvedInCache = processMatchUrlUsingCacheOnly(resourceType, matchUrl); + if (resolvedInCache != null) { + return Collections.singleton(resolvedInCache); + } + RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType); - SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef); + SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(matchUrl, resourceDef); if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) { - throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters"); + throw new InvalidRequestException("Invalid match URL[" + matchUrl + "] - URL has no search parameters"); } paramMap.setLoadSynchronousUpTo(2); Set retVal = search(paramMap, theResourceType, theRequest); - if (myDaoConfig.getMatchUrlCache() && retVal.size() == 1) { + if (myDaoConfig.isMatchUrlCacheEnabled() && retVal.size() == 1) { ResourcePersistentId pid = retVal.iterator().next(); - myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl, pid); + myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, pid); } return retVal; } + private String massageForStorage(String theResourceType, String theMatchUrl) { + Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank"); + int questionMarkIdx = theMatchUrl.indexOf("?"); + if (questionMarkIdx > 0) { + return theMatchUrl; + } + if (questionMarkIdx == 0) { + return theResourceType + theMatchUrl; + } + return theResourceType + "?" + theMatchUrl; + } + + @Nullable + public ResourcePersistentId processMatchUrlUsingCacheOnly(String theResourceType, String theMatchUrl) { + ResourcePersistentId existing = null; + if (myDaoConfig.getMatchUrlCache()) { + String matchUrl = massageForStorage(theResourceType, theMatchUrl); + existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl); + } + return existing; + } + public Set search(SearchParameterMap theParamMap, Class theResourceType, RequestDetails theRequest) { StopWatch sw = new StopWatch(); IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceType); @@ -113,11 +149,14 @@ public class MatchResourceUrlService { } - public void matchUrlResolved(String theMatchUrl, ResourcePersistentId theResourcePersistentId) { + public void matchUrlResolved(TransactionDetails theTransactionDetails, String theResourceType, String theMatchUrl, ResourcePersistentId theResourcePersistentId) { Validate.notBlank(theMatchUrl); Validate.notNull(theResourcePersistentId); - if (myDaoConfig.getMatchUrlCache()) { - myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl, theResourcePersistentId); + String matchUrl = massageForStorage(theResourceType, theMatchUrl); + theTransactionDetails.addResolvedMatchUrl(matchUrl, theResourcePersistentId); + if (myDaoConfig.isMatchUrlCacheEnabled()) { + myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, theResourcePersistentId); } } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index 399aad64093..85d1f850442 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -20,12 +20,31 @@ package ca.uhn.fhir.jpa.dao; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; +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.param.TokenParam; import ca.uhn.fhir.util.StopWatch; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; -import org.hibernate.Session; import org.hibernate.internal.SessionImpl; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,17 +54,48 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContextType; import javax.persistence.PersistenceException; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.dao.index.IdHelperService.EMPTY_PREDICATE_ARRAY; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + public class TransactionProcessor extends BaseTransactionProcessor { + public static final Pattern SINGLE_PARAMETER_MATCH_URL_PATTERN = Pattern.compile("^[^?]+[?][a-z0-9-]+=[^&,]+$"); private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class); @PersistenceContext(type = PersistenceContextType.TRANSACTION) private EntityManager myEntityManager; @Autowired(required = false) private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect; + @Autowired + private IdHelperService myIdHelperService; + @Autowired + private PartitionSettings myPartitionSettings; + @Autowired + private DaoConfig myDaoConfig; + @Autowired + private FhirContext myFhirContext; + @Autowired + private MatchResourceUrlService myMatchResourceUrlService; + @Autowired + private MatchUrlService myMatchUrlService; + @Autowired + private IRequestPartitionHelperSvc myRequestPartitionSvc; + public void setEntityManagerForUnitTest(EntityManager theEntityManager) { myEntityManager = theEntityManager; @@ -58,6 +108,225 @@ public class TransactionProcessor extends BaseTransactionProcessor { Validate.notNull(myEntityManager); } + @VisibleForTesting + public void setFhirContextForUnitTest(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + @Override + protected Map doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set theAllIds, + Map theIdSubstitutions, Map theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) { + + ITransactionProcessorVersionAdapter versionAdapter = getVersionAdapter(); + RequestPartitionId requestPartitionId = null; + if (!myPartitionSettings.isPartitioningEnabled()) { + requestPartitionId = RequestPartitionId.allPartitions(); + } else { + // If all entries in the transaction point to the exact same partition, we'll try and do a pre-fetch + Set requestPartitionIdsForAllEntries = new HashSet<>(); + for (IBase nextEntry : theEntries) { + IBaseResource resource = versionAdapter.getResource(nextEntry); + if (resource != null) { + RequestPartitionId requestPartition = myRequestPartitionSvc.determineReadPartitionForRequest(theRequest, myFhirContext.getResourceType(resource)); + requestPartitionIdsForAllEntries.add(requestPartition); + } + } + if (requestPartitionIdsForAllEntries.size() == 1) { + requestPartitionId = requestPartitionIdsForAllEntries.iterator().next(); + } + } + + if (requestPartitionId != null) { + + Set foundIds = new HashSet<>(); + List idsToPreFetch = new ArrayList<>(); + + /* + * Pre-Fetch any resources that are referred to normally by ID, e.g. + * regular FHIR updates within the transaction. + */ + List idsToPreResolve = new ArrayList<>(); + for (IBase nextEntry : theEntries) { + IBaseResource resource = versionAdapter.getResource(nextEntry); + if (resource != null) { + String fullUrl = versionAdapter.getFullUrl(nextEntry); + boolean isPlaceholder = defaultString(fullUrl).startsWith("urn:"); + if (!isPlaceholder) { + if (resource.getIdElement().hasIdPart() && resource.getIdElement().hasResourceType()) { + idsToPreResolve.add(resource.getIdElement()); + } + } + } + } + List outcome = myIdHelperService.resolveResourcePersistentIdsWithCache(requestPartitionId, idsToPreResolve); + for (ResourcePersistentId next : outcome) { + foundIds.add(next.getAssociatedResourceId().toUnqualifiedVersionless().getValue()); + theTransactionDetails.addResolvedResourceId(next.getAssociatedResourceId(), next); + if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY || !next.getAssociatedResourceId().isIdPartValidLong()) { + idsToPreFetch.add(next.getIdAsLong()); + } + } + for (IIdType next : idsToPreResolve) { + if (!foundIds.contains(next.toUnqualifiedVersionless().getValue())) { + theTransactionDetails.addResolvedResourceId(next.toUnqualifiedVersionless(), null); + } + } + + /* + * Pre-resolve any conditional URLs we can + */ + List searchParameterMapsToResolve = new ArrayList<>(); + for (IBase nextEntry : theEntries) { + IBaseResource resource = versionAdapter.getResource(nextEntry); + if (resource != null) { + String verb = versionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); + String requestUrl = versionAdapter.getEntryRequestUrl(nextEntry); + String requestIfNoneExist = versionAdapter.getEntryIfNoneExist(nextEntry); + String resourceType = myFhirContext.getResourceType(resource); + if ("PUT".equals(verb) && requestUrl != null && requestUrl.contains("?")) { + ResourcePersistentId cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(resourceType, requestUrl); + if (cachedId != null) { + idsToPreFetch.add(cachedId.getIdAsLong()); + } else if (SINGLE_PARAMETER_MATCH_URL_PATTERN.matcher(requestUrl).matches()) { + RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(resource); + SearchParameterMap matchUrlSearchMap = myMatchUrlService.translateMatchUrl(requestUrl, resourceDefinition); + searchParameterMapsToResolve.add(new MatchUrlToResolve(requestUrl, matchUrlSearchMap, resourceDefinition)); + } + } else if ("POST".equals(verb) && requestIfNoneExist != null && requestIfNoneExist.contains("?")) { + ResourcePersistentId cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(resourceType, requestIfNoneExist); + if (cachedId != null) { + idsToPreFetch.add(cachedId.getIdAsLong()); + } else if (SINGLE_PARAMETER_MATCH_URL_PATTERN.matcher(requestIfNoneExist).matches()) { + RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(resource); + SearchParameterMap matchUrlSearchMap = myMatchUrlService.translateMatchUrl(requestIfNoneExist, resourceDefinition); + searchParameterMapsToResolve.add(new MatchUrlToResolve(requestIfNoneExist, matchUrlSearchMap, resourceDefinition)); + } + } + + } + } + if (searchParameterMapsToResolve.size() > 0) { + CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(ResourceIndexedSearchParamToken.class); + Root from = cq.from(ResourceIndexedSearchParamToken.class); + List orPredicates = new ArrayList<>(); + + for (MatchUrlToResolve next : searchParameterMapsToResolve) { + Collection>> values = next.myMatchUrlSearchMap.values(); + if (values.size() == 1) { + List> andList = values.iterator().next(); + IQueryParameterType param = andList.get(0).get(0); + + if (param instanceof TokenParam) { + TokenParam tokenParam = (TokenParam) param; + Predicate hashPredicate = null; + if (isNotBlank(tokenParam.getValue()) && isNotBlank(tokenParam.getSystem())) { + next.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue(myPartitionSettings, requestPartitionId, next.myResourceDefinition.getName(), next.myMatchUrlSearchMap.keySet().iterator().next(), tokenParam.getSystem(), tokenParam.getValue()); + hashPredicate = cb.equal(from.get("myHashSystemAndValue").as(Long.class), next.myHashSystemAndValue); + } else if (isNotBlank(tokenParam.getValue())) { + next.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue(myPartitionSettings, requestPartitionId, next.myResourceDefinition.getName(), next.myMatchUrlSearchMap.keySet().iterator().next(), tokenParam.getValue()); + hashPredicate = cb.equal(from.get("myHashValue").as(Long.class), next.myHashValue); + } + + if (hashPredicate != null) { + + if (myPartitionSettings.isPartitioningEnabled() && !myPartitionSettings.isIncludePartitionInSearchHashes()) { + if (requestPartitionId.isDefaultPartition()) { + Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue").as(Integer.class)); + hashPredicate = cb.and(hashPredicate, partitionIdCriteria); + } else if (!requestPartitionId.isAllPartitions()) { + Predicate partitionIdCriteria = from.get("myPartitionIdValue").as(Integer.class).in(requestPartitionId.getPartitionIds()); + hashPredicate = cb.and(hashPredicate, partitionIdCriteria); + } + } + + orPredicates.add(hashPredicate); + } + } + } + + } + + if (orPredicates.size() > 1) { + cq.where(cb.or(orPredicates.toArray(EMPTY_PREDICATE_ARRAY))); + + TypedQuery query = myEntityManager.createQuery(cq); + List results = query.getResultList(); + for (ResourceIndexedSearchParamToken nextResult : results) { + + for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { + if (nextSearchParameterMap.myHashSystemAndValue != null && nextSearchParameterMap.myHashSystemAndValue.equals(nextResult.getHashSystemAndValue())) { + idsToPreFetch.add(nextResult.getResourcePid()); + myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, nextSearchParameterMap.myResourceDefinition.getName(), nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); + theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); + nextSearchParameterMap.myResolved = true; + } + if (nextSearchParameterMap.myHashValue != null && nextSearchParameterMap.myHashValue.equals(nextResult.getHashValue())) { + idsToPreFetch.add(nextResult.getResourcePid()); + myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, nextSearchParameterMap.myResourceDefinition.getName(), nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); + theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); + nextSearchParameterMap.myResolved = true; + } + + } + + } + + for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { + // No matches + if (!nextSearchParameterMap.myResolved) { + theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, TransactionDetails.NOT_FOUND); + } + } + + } + } + + + /* + * Pre-fetch the resources we're touching in this transaction in mass - this reduced the + * number of database round trips. + * + * The thresholds below are kind of arbitrary. It's not + * actually guaranteed that this pre-fetching will help (e.g. if a Bundle contains + * a bundle of NOP conditional creates for example, the pre-fetching is actually loading + * more data than would otherwise be loaded). + * + * However, for realistic average workloads, this should reduce the number of round trips. + */ + if (idsToPreFetch.size() > 2) { + List loadedResourceTableEntries = preFetchIndexes(idsToPreFetch, "forcedId", "myForcedId"); + + if (loadedResourceTableEntries.stream().filter(t -> t.isParamsStringPopulated()).count() > 1) { + preFetchIndexes(idsToPreFetch, "string", "myParamsString"); + } + if (loadedResourceTableEntries.stream().filter(t -> t.isParamsTokenPopulated()).count() > 1) { + preFetchIndexes(idsToPreFetch, "token", "myParamsToken"); + } + if (loadedResourceTableEntries.stream().filter(t -> t.isParamsDatePopulated()).count() > 1) { + preFetchIndexes(idsToPreFetch, "date", "myParamsDate"); + } + if (loadedResourceTableEntries.stream().filter(t -> t.isParamsDatePopulated()).count() > 1) { + preFetchIndexes(idsToPreFetch, "quantity", "myParamsQuantity"); + } + if (loadedResourceTableEntries.stream().filter(t -> t.isHasLinks()).count() > 1) { + preFetchIndexes(idsToPreFetch, "resourceLinks", "myResourceLinks"); + } + + } + + } + + return super.doTransactionWriteOperations(theRequest, theActionName, theTransactionDetails, theAllIds, theIdSubstitutions, theIdToPersistedOutcome, theResponse, theOriginalRequestOrder, theEntries, theTransactionStopWatch); + } + + private List preFetchIndexes(List ids, String typeDesc, String fieldName) { + TypedQuery query = myEntityManager.createQuery("FROM ResourceTable r LEFT JOIN FETCH r." + fieldName + " WHERE r.myId IN ( :IDS )", ResourceTable.class); + query.setParameter("IDS", ids); + List indexFetchOutcome = query.getResultList(); + ourLog.debug("Pre-fetched {} {}} indexes", indexFetchOutcome.size(), typeDesc); + return indexFetchOutcome; + } @Override protected void flushSession(Map theIdToPersistedOutcome) { @@ -86,5 +355,29 @@ public class TransactionProcessor extends BaseTransactionProcessor { } } + @VisibleForTesting + public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { + myPartitionSettings = thePartitionSettings; + } + @VisibleForTesting + public void setIdHelperServiceForUnitTest(IdHelperService theIdHelperService) { + myIdHelperService = theIdHelperService; + } + + private static class MatchUrlToResolve { + + private final String myRequestUrl; + private final SearchParameterMap myMatchUrlSearchMap; + private final RuntimeResourceDefinition myResourceDefinition; + public boolean myResolved; + private Long myHashValue; + private Long myHashSystemAndValue; + + public MatchUrlToResolve(String theRequestUrl, SearchParameterMap theMatchUrlSearchMap, RuntimeResourceDefinition theResourceDefinition) { + myRequestUrl = theRequestUrl; + myMatchUrlSearchMap = theMatchUrlSearchMap; + myResourceDefinition = theResourceDefinition; + } + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java index 2ecae9e3ec7..add98b15d65 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java @@ -48,8 +48,8 @@ public class FhirResourceDaoSubscriptionDstu3 extends BaseHapiFhirResourceDao @@ -181,71 +195,80 @@ public class IdHelperService { return Collections.emptyList(); } - List retVal = new ArrayList<>(); + List retVal = new ArrayList<>(theIds.size()); - if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) { - theIds - .stream() - .filter(IdHelperService::isValidPid) - .map(IIdType::getIdPartAsLong) - .map(ResourcePersistentId::new) - .forEach(retVal::add); + Set idsToCheck = new HashSet<>(theIds.size()); + for (IIdType nextId : theIds) { + if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) { + if (nextId.isIdPartValidLong()) { + retVal.add(new ResourcePersistentId(nextId.getIdPartAsLong()).setAssociatedResourceId(nextId)); + continue; + } + } + + String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getIdPart()); + ResourcePersistentId cachedId = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key); + if (cachedId != null) { + retVal.add(cachedId); + continue; + } + + idsToCheck.add(nextId); } - ListMultimap typeToIds = organizeIdsByResourceType(theIds); + if (idsToCheck.size() > 0) { + CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = cb.createQuery(ForcedId.class); + Root from = criteriaQuery.from(ForcedId.class); - for (Map.Entry> nextEntry : typeToIds.asMap().entrySet()) { - String nextResourceType = nextEntry.getKey(); - Collection nextIds = nextEntry.getValue(); - if (isBlank(nextResourceType)) { + List predicates = new ArrayList<>(idsToCheck.size()); + for (IIdType next : idsToCheck) { - List views = myForcedIdDao.findByForcedId(nextIds); - views.forEach(t -> retVal.add(new ResourcePersistentId(t))); + List andPredicates = new ArrayList<>(3); - } else { - -// String partitionIdStringForKey = RequestPartitionId.stringifyForKey(theRequestPartitionId); - for (Iterator idIterator = nextIds.iterator(); idIterator.hasNext(); ) { - String nextId = idIterator.next(); - String key = toForcedIdToPidKey(theRequestPartitionId, nextResourceType, nextId); - ResourcePersistentId nextCachedPid = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key); - if (nextCachedPid != null) { - idIterator.remove(); - retVal.add(nextCachedPid); - } + if (isNotBlank(next.getResourceType())) { + Predicate typeCriteria = cb.equal(from.get("myResourceType").as(String.class), next.getResourceType()); + andPredicates.add(typeCriteria); } - if (nextIds.size() > 0) { + Predicate idCriteria = cb.equal(from.get("myForcedId").as(String.class), next.getIdPart()); + andPredicates.add(idCriteria); - Collection views; - if (theRequestPartitionId.isAllPartitions()) { - views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds); - } else { - if (theRequestPartitionId.isDefaultPartition()) { - views = myForcedIdDao.findByTypeAndForcedIdInPartitionNull(nextResourceType, nextIds); - } else if (theRequestPartitionId.hasDefaultPartitionId()) { - views = myForcedIdDao.findByTypeAndForcedIdInPartitionIdsOrNullPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionIds()); - } else { - views = myForcedIdDao.findByTypeAndForcedIdInPartitionIds(nextResourceType, nextIds, theRequestPartitionId.getPartitionIds()); - } - } - for (Object[] nextView : views) { - String forcedId = (String) nextView[0]; - Long pid = (Long) nextView[1]; - ResourcePersistentId persistentId = new ResourcePersistentId(pid); - retVal.add(persistentId); - - String key = toForcedIdToPidKey(theRequestPartitionId, nextResourceType, forcedId); - myMemoryCacheService.put(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, persistentId); - } + if (theRequestPartitionId.isDefaultPartition()) { + Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue").as(Integer.class)); + andPredicates.add(partitionIdCriteria); + } else if (!theRequestPartitionId.isAllPartitions()) { + Predicate partitionIdCriteria = from.get("myPartitionIdValue").as(Integer.class).in(theRequestPartitionId.getPartitionIds()); + andPredicates.add(partitionIdCriteria); } + predicates.add(cb.and(andPredicates.toArray(EMPTY_PREDICATE_ARRAY))); } + + criteriaQuery.where(cb.or(predicates.toArray(EMPTY_PREDICATE_ARRAY))); + + TypedQuery query = myEntityManager.createQuery(criteriaQuery); + List results = query.getResultList(); + for (ForcedId nextId : results) { + ResourcePersistentId persistentId = new ResourcePersistentId(nextId.getResourceId()); + populateAssociatedResourceId(nextId.getResourceType(), nextId.getForcedId(), persistentId); + retVal.add(persistentId); + + String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getForcedId()); + myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, persistentId); + } + } return retVal; } + private void populateAssociatedResourceId(String nextResourceType, String forcedId, ResourcePersistentId persistentId) { + IIdType resourceId = myFhirCtx.getVersion().newIdType(); + resourceId.setValue(nextResourceType + "/" + forcedId); + persistentId.setAssociatedResourceId(resourceId); + } + /** * Given a persistent ID, returns the associated resource ID */ @@ -501,6 +524,10 @@ public class IdHelperService { */ public void addResolvedPidToForcedId(ResourcePersistentId theResourcePersistentId, @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, @Nullable String theForcedId) { if (theForcedId != null) { + if (theResourcePersistentId.getAssociatedResourceId() == null) { + populateAssociatedResourceId(theResourceType, theForcedId, theResourcePersistentId); + } + myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourcePersistentId.getIdAsLong(), Optional.of(theForcedId)); String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theForcedId); myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theResourcePersistentId); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java index db78254e769..379d19f5bd4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java @@ -111,8 +111,8 @@ public class SearchParamWithInlineReferencesExtractor { mySearchParamRegistry = theSearchParamRegistry; } - public void populateFromResource(ResourceIndexedSearchParams theParams, TransactionDetails theTransactionDetails, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest) { - extractInlineReferences(theResource, theRequest); + public void populateFromResource(ResourceIndexedSearchParams theParams, TransactionDetails theTransactionDetails, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest, boolean theFailOnInvalidReference) { + extractInlineReferences(theResource, theTransactionDetails, theRequest); RequestPartitionId partitionId; if (myPartitionSettings.isPartitioningEnabled()) { @@ -121,7 +121,7 @@ public class SearchParamWithInlineReferencesExtractor { partitionId = RequestPartitionId.allPartitions(); } - mySearchParamExtractorService.extractFromResource(partitionId, theRequest, theParams, theEntity, theResource, theTransactionDetails, true); + mySearchParamExtractorService.extractFromResource(partitionId, theRequest, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference); Set> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet(); if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) { @@ -245,7 +245,7 @@ public class SearchParamWithInlineReferencesExtractor { * 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. */ - public void extractInlineReferences(IBaseResource theResource, RequestDetails theRequest) { + public void extractInlineReferences(IBaseResource theResource, TransactionDetails theTransactionDetails, RequestDetails theRequest) { if (!myDaoConfig.isAllowInlineMatchUrlReferences()) { return; } @@ -277,7 +277,7 @@ public class SearchParamWithInlineReferencesExtractor { } Class matchResourceType = matchResourceDef.getImplementingClass(); //Attempt to find the target reference before creating a placeholder - Set matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theRequest); + Set matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theTransactionDetails, theRequest); ResourcePersistentId match; if (matches.isEmpty()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSubscriptionR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSubscriptionR4.java index 5c095bd72c0..a0c7eab2a16 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSubscriptionR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSubscriptionR4.java @@ -48,8 +48,8 @@ public class FhirResourceDaoSubscriptionR4 extends BaseHapiFhirResourceDao theParamNameToPresence) { + public AddRemoveCount + updatePresence(ResourceTable theResource, Map theParamNameToPresence) { AddRemoveCount retVal = new AddRemoveCount(); if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { return retVal; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java index de46d0f1990..34a0e10da8a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java @@ -378,6 +378,9 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe b.append(new InstantType(new Date(theQuery.getQueryTimestamp())).getValueAsString()); b.append(" took ").append(StopWatch.formatMillis(theQuery.getElapsedTime())); b.append(" on Thread: ").append(theQuery.getThreadName()); + if (theQuery.getSize() > 1) { + b.append("\nExecution Count: ").append(theQuery.getSize()).append(" (parameters shown are for first execution)"); + } b.append("\nSQL:\n").append(formattedSql); if (theQuery.getStackTrace() != null) { b.append("\nStack:\n "); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SqlQuery.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SqlQuery.java index 3c1f9a7ddea..f588f278c93 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SqlQuery.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SqlQuery.java @@ -26,7 +26,6 @@ import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.regex.Pattern; import static org.apache.commons.lang3.StringUtils.trim; @@ -93,11 +92,7 @@ public class SqlQuery { } } - if (mySize > 1) { - retVal += "\nsize: " + mySize + "\n"; - } return trim(retVal); - } public StackTraceElement[] getStackTrace() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 18acae82b23..ae31250f280 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -11,14 +11,17 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.BaseConfig; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSetConcept; import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; @@ -162,6 +165,10 @@ public abstract class BaseJpaTest extends BaseTest { private IValidationSupport myJpaPersistedValidationSupport; @Autowired private FhirInstanceValidator myFhirInstanceValidator; + @Autowired + private IResourceTableDao myResourceTableDao; + @Autowired + private IResourceHistoryTableDao myResourceHistoryTableDao; @AfterEach public void afterPerformCleanup() { @@ -242,6 +249,22 @@ public abstract class BaseJpaTest extends BaseTest { }); } + protected int logAllResources() { + return runInTransaction(() -> { + List resources = myResourceTableDao.findAll(); + ourLog.info("Resources:\n * {}", resources.stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + return resources.size(); + }); + } + + protected int logAllResourceVersions() { + return runInTransaction(() -> { + List resources = myResourceTableDao.findAll(); + ourLog.info("Resources Versions:\n * {}", resources.stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + return resources.size(); + }); + } + protected void logAllDateIndexes() { runInTransaction(() -> { ourLog.info("Date indexes:\n * {}", myResourceIndexedSearchParamDateDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); @@ -630,7 +653,7 @@ public abstract class BaseJpaTest extends BaseTest { throw new Error(theE); } } - if (sw.getMillis() >= 16000) { + if (sw.getMillis() >= 16000 || theList.size() > theTarget) { String describeResults = theList .stream() .map(t -> { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java index 97824dfa84a..18af1ef70f5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java @@ -5,9 +5,13 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hibernate.Session; @@ -56,6 +60,14 @@ public class TransactionProcessorTest { private ModelConfig myModelConfig; @MockBean private InMemoryResourceMatcher myInMemoryResourceMatcher; + @MockBean + private IdHelperService myIdHelperService; + @MockBean + private PartitionSettings myPartitionSettings; + @MockBean + private MatchUrlService myMatchUrlService; + @MockBean + private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; @MockBean(answer = Answers.RETURNS_DEEP_STUBS) private SessionImpl mySession; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java index e4803f648d5..80476f79ed8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java @@ -429,19 +429,11 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest { p.addIdentifier().setSystem("urn:system").setValue(methodName); request.addEntry().setResource(p).getRequest().setMethod(HTTPVerbEnum.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName); - try { - myCaptureQueriesListener.clear(); - mySystemDao.transaction(mySrd, request); - myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, request); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - runInTransaction(()->{ - ourLog.info("Tokens:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); - }); - - fail(); - } catch (InvalidRequestException e) { - assertEquals(e.getMessage(), "Unable to process Transaction - Request would cause multiple resources to match URL: \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateWithDuplicateMatchUrl01\". Does transaction request contain duplicates?"); - } + assertEquals(1, logAllResources()); } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index 6778d18624a..a879980e9f5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -1008,29 +1008,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { assertThat(oo.getIssue().get(0).getDiagnostics(), containsString("Unknown search parameter")); } - @Test - public void testTransactionCreateWithDuplicateMatchUrl01() { - String methodName = "testTransactionCreateWithDuplicateMatchUrl01"; - Bundle request = new Bundle(); - - Patient p; - p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName); - - p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue(methodName); - request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName); - - try { - mySystemDao.transaction(mySrd, request); - fail(); - } catch (InvalidRequestException e) { - assertEquals(e.getMessage(), - "Unable to process Transaction - Request would cause multiple resources to match URL: \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateWithDuplicateMatchUrl01\". Does transaction request contain duplicates?"); - } - } - @Test public void testTransactionCreateWithDuplicateMatchUrl02() { String methodName = "testTransactionCreateWithDuplicateMatchUrl02"; @@ -1127,27 +1104,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } } - @Test - public void testTransactionCreateWithLinks() { - Bundle request = new Bundle(); - request.setType(BundleType.TRANSACTION); - - Observation o = new Observation(); - o.setId("A"); - o.setStatus(ObservationStatus.AMENDED); - - request.addEntry() - .setResource(o) - .getRequest().setUrl("A").setMethod(HTTPVerb.PUT); - - try { - mySystemDao.transaction(mySrd, request); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Invalid match URL[A] - URL has no search parameters", e.getMessage()); - } - } - @Test public void testTransactionCreateWithPutUsingAbsoluteUrl() { String methodName = "testTransactionCreateWithPutUsingAbsoluteUrl"; @@ -1699,69 +1655,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } - @Test - public void testTransactionDoubleConditionalCreateOnlyCreatesOne() { - Bundle inputBundle = new Bundle(); - inputBundle.setType(Bundle.BundleType.TRANSACTION); - - Encounter enc1 = new Encounter(); - enc1.addIdentifier().setSystem("urn:foo").setValue("12345"); - inputBundle - .addEntry() - .setResource(enc1) - .getRequest() - .setMethod(HTTPVerb.POST) - .setIfNoneExist("Encounter?identifier=urn:foo|12345"); - Encounter enc2 = new Encounter(); - enc2.addIdentifier().setSystem("urn:foo").setValue("12345"); - inputBundle - .addEntry() - .setResource(enc2) - .getRequest() - .setMethod(HTTPVerb.POST) - .setIfNoneExist("Encounter?identifier=urn:foo|12345"); - - try { - mySystemDao.transaction(mySrd, inputBundle); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", - e.getMessage()); - } - } - - @Test - public void testTransactionDoubleConditionalUpdateOnlyCreatesOne() { - Bundle inputBundle = new Bundle(); - inputBundle.setType(Bundle.BundleType.TRANSACTION); - - Encounter enc1 = new Encounter(); - enc1.addIdentifier().setSystem("urn:foo").setValue("12345"); - inputBundle - .addEntry() - .setResource(enc1) - .getRequest() - .setMethod(HTTPVerb.PUT) - .setUrl("Encounter?identifier=urn:foo|12345"); - Encounter enc2 = new Encounter(); - enc2.addIdentifier().setSystem("urn:foo").setValue("12345"); - inputBundle - .addEntry() - .setResource(enc2) - .getRequest() - .setMethod(HTTPVerb.PUT) - .setUrl("Encounter?identifier=urn:foo|12345"); - - try { - mySystemDao.transaction(mySrd, inputBundle); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", - e.getMessage()); - } - - } - @Test public void testTransactionFailsWithDuplicateIds() { Bundle request = new Bundle(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java index 5212ba9e40d..80ee91e8b78 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java @@ -68,6 +68,8 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest { myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setMassIngestionMode(new DaoConfig().isMassIngestionMode()); + myDaoConfig.setMatchUrlCacheEnabled(new DaoConfig().getMatchUrlCache()); } @BeforeEach diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index 27bb3f38b6d..ca13037bc3d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -231,7 +231,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId)); assertEquals(true, p.getActive()); - // Not create a client assigned numeric ID + // Now create a client assigned numeric ID p = new Patient(); p.setId("Patient/" + newId); p.addName().setFamily("FAM"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index baf324bd080..d420c556889 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -1,6 +1,5 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum; import ca.uhn.fhir.jpa.model.entity.ModelConfig; @@ -8,7 +7,6 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.SqlQuery; -import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -28,6 +26,7 @@ import org.hl7.fhir.r4.model.Narrative; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.ServiceRequest; import org.hl7.fhir.r4.model.StringType; @@ -36,9 +35,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.util.Iterator; +import java.util.Date; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -65,6 +63,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths()); myModelConfig.setRespectVersionsForSearchIncludes(new ModelConfig().isRespectVersionsForSearchIncludes()); myFhirCtx.getParserOptions().setStripVersionsFromReferences(true); + myDaoConfig.setTagStorageMode(new DaoConfig().getTagStorageMode()); } @BeforeEach @@ -553,7 +552,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); assertEquals(1, myObservationDao.search(map).size().intValue()); - // Resolve forced ID, Perform search, load result + // (not resolve forced ID), Perform search, load result assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertNoPartitionSelectors(); assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); @@ -567,7 +566,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); assertEquals(1, myObservationDao.search(map).size().intValue()); myCaptureQueriesListener.logAllQueriesForCurrentThread(); - // Resolve forced ID, Perform search, load result (this time we reuse the cached forced-id resolution) + // (not resolve forced ID), Perform search, load result (this time we reuse the cached forced-id resolution) assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); @@ -595,7 +594,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); assertEquals(1, myObservationDao.search(map).size().intValue()); myCaptureQueriesListener.logAllQueriesForCurrentThread(); - // Resolve forced ID, Perform search, load result + // (not Resolve forced ID), Perform search, load result assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); @@ -691,11 +690,235 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); } + @Test + public void testTransactionWithTwoCreates() { + + BundleBuilder bb = new BundleBuilder(myFhirCtx); + + Patient pt = new Patient(); + pt.setId(IdType.newRandomUuid()); + pt.addIdentifier().setSystem("http://foo").setValue("123"); + bb.addTransactionCreateEntry(pt); + + Patient pt2 = new Patient(); + pt2.setId(IdType.newRandomUuid()); + pt2.addIdentifier().setSystem("http://foo").setValue("456"); + bb.addTransactionCreateEntry(pt2); + + runInTransaction(() -> { + assertEquals(0, myResourceTableDao.count()); + }); + + ourLog.info("About to start transaction"); + + myCaptureQueriesListener.clear(); + Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) bb.getBundle()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(0, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(3, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + runInTransaction(() -> { + assertEquals(2, myResourceTableDao.count()); + }); + } + + @Test + public void testTransactionWithMultipleUpdates() { + + AtomicInteger counter = new AtomicInteger(0); + Supplier input = () -> { + BundleBuilder bb = new BundleBuilder(myFhirCtx); + + Patient pt = new Patient(); + pt.setId("Patient/A"); + pt.addIdentifier().setSystem("http://foo").setValue("123"); + bb.addTransactionUpdateEntry(pt); + + Observation obsA = new Observation(); + obsA.setId("Observation/A"); + obsA.getCode().addCoding().setSystem("http://foo").setCode("bar"); + obsA.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsA.setEffective(new DateTimeType(new Date())); + obsA.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsA); + + Observation obsB = new Observation(); + obsB.setId("Observation/B"); + obsB.getCode().addCoding().setSystem("http://foo").setCode("bar"); + obsB.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsB.setEffective(new DateTimeType(new Date())); + obsB.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsB); + + return (Bundle) bb.getBundle(); + }; + + ourLog.info("About to start transaction"); + + myCaptureQueriesListener.clear(); + Bundle outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(1, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(6, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Run a second time + */ + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(10, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Third time with mass ingestion mode enabled + */ + myDaoConfig.setMassIngestionMode(true); + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(7, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + } + + + @Test + public void testTransactionWithMultipleConditionalUpdates() { + + AtomicInteger counter = new AtomicInteger(0); + Supplier input = () -> { + BundleBuilder bb = new BundleBuilder(myFhirCtx); + + Patient pt = new Patient(); + pt.setId(IdType.newRandomUuid()); + pt.addIdentifier().setSystem("http://foo").setValue("123"); + bb.addTransactionCreateEntry(pt).conditional("Patient?identifier=http://foo|123"); + + Observation obsA = new Observation(); + obsA.getSubject().setReference(pt.getId()); + obsA.getCode().addCoding().setSystem("http://foo").setCode("bar1"); + obsA.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsA.setEffective(new DateTimeType(new Date())); + obsA.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsA).conditional("Observation?code=http://foo|bar1"); + + Observation obsB = new Observation(); + obsB.getSubject().setReference(pt.getId()); + obsB.getCode().addCoding().setSystem("http://foo").setCode("bar2"); + obsB.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsB.setEffective(new DateTimeType(new Date())); + obsB.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsB).conditional("Observation?code=http://foo|bar2"); + + Observation obsC = new Observation(); + obsC.getSubject().setReference(pt.getId()); + obsC.getCode().addCoding().setSystem("http://foo").setCode("bar3"); + obsC.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsC.setEffective(new DateTimeType(new Date())); + obsC.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsC).conditional("Observation?code=bar3"); + + Observation obsD = new Observation(); + obsD.getSubject().setReference(pt.getId()); + obsD.getCode().addCoding().setSystem("http://foo").setCode("bar4"); + obsD.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsD.setEffective(new DateTimeType(new Date())); + obsD.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsD).conditional("Observation?code=bar4"); + + return (Bundle) bb.getBundle(); + }; + + ourLog.info("About to start transaction"); + + myCaptureQueriesListener.clear(); + Bundle outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(1, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(6, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(1, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Run a second time + */ + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(11, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Third time with mass ingestion mode enabled + */ + myDaoConfig.setMassIngestionMode(true); + myDaoConfig.setMatchUrlCache(true); + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(6, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Fourth time with mass ingestion mode enabled + */ + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(5, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + } + + @Test public void testTransactionWithConditionalCreate_MatchUrlCacheEnabled() { myDaoConfig.setMatchUrlCache(true); - Supplier bundleCreator = ()-> { + Supplier bundleCreator = () -> { BundleBuilder bb = new BundleBuilder(myFhirCtx); Patient pt = new Patient(); @@ -719,7 +942,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(5, myCaptureQueriesListener.countInsertQueries()); assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); - runInTransaction(()->{ + runInTransaction(() -> { List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); assertThat(types, containsInAnyOrder("Patient", "Observation")); }); @@ -733,7 +956,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(3, myCaptureQueriesListener.countInsertQueries()); assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); - runInTransaction(()->{ + runInTransaction(() -> { List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); assertThat(types, containsInAnyOrder("Patient", "Observation", "Observation")); }); @@ -746,7 +969,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(3, myCaptureQueriesListener.countInsertQueries()); assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); - runInTransaction(()->{ + runInTransaction(() -> { List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); assertThat(types, containsInAnyOrder("Patient", "Observation", "Observation", "Observation")); }); @@ -756,7 +979,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { @Test public void testTransactionWithConditionalCreate_MatchUrlCacheNotEnabled() { - Supplier bundleCreator = ()-> { + Supplier bundleCreator = () -> { BundleBuilder bb = new BundleBuilder(myFhirCtx); Patient pt = new Patient(); @@ -781,7 +1004,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(5, myCaptureQueriesListener.countInsertQueries()); assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); - runInTransaction(()->{ + runInTransaction(() -> { List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); assertThat(types, containsInAnyOrder("Patient", "Observation")); }); @@ -800,7 +1023,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertThat(matchUrlQuery, containsString("t0.HASH_SYS_AND_VALUE = '-4132452001562191669'")); assertThat(matchUrlQuery, containsString("limit '2'")); - runInTransaction(()->{ + runInTransaction(() -> { List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); assertThat(types, containsInAnyOrder("Patient", "Observation", "Observation")); }); @@ -813,7 +1036,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(3, myCaptureQueriesListener.countInsertQueries()); assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); - runInTransaction(()->{ + runInTransaction(() -> { List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); assertThat(types, containsInAnyOrder("Patient", "Observation", "Observation", "Observation")); }); @@ -855,14 +1078,14 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logInsertQueriesForCurrentThread(); assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); - assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Pass 2 input = new Bundle(); - patient = new Patient(); + patient = new Patient(); patient.setId("Patient/A"); patient.setActive(true); input.addEntry() @@ -872,7 +1095,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { .setMethod(Bundle.HTTPVerb.PUT) .setUrl("Patient/A"); - observation = new Observation(); + observation = new Observation(); observation.setId(IdType.newRandomUuid()); observation.addReferenceRange().setText("A"); input.addEntry() @@ -883,7 +1106,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { .setUrl("Observation"); myCaptureQueriesListener.clear(); - output = mySystemDao.transaction(mySrd, input); + output = mySystemDao.transaction(mySrd, input); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(output)); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); @@ -891,7 +1114,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logInsertQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); - assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); @@ -1013,7 +1236,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Do the same a second time - Deletes are enabled so we expect to have to resolve the @@ -1051,7 +1274,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } @@ -1102,7 +1325,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Do the same a second time - Deletes are enabled so we expect to have to resolve the @@ -1140,7 +1363,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } @@ -1193,9 +1416,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - // See notes in testTransactionWithMultiplePreExistingReferences_Numeric_DeletesDisabled below - myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Do the same a second time - Deletes are enabled so we expect to have to resolve the @@ -1233,7 +1454,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); - assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } @@ -1283,10 +1504,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - // TODO: We have 2 updates here that are caused by Hibernate deciding to flush its action queue half way through - // the transaction because a read is about to take place. I think these are unnecessary but I don't see a simple - // way of getting rid of them. Hopefully these can be optimized out later - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Do the same a second time - Deletes are enabled so we expect to have to resolve the @@ -1324,8 +1542,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - // Similar to the note above - No idea why this update is here, it's basically a NO-OP - assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } @@ -1398,9 +1615,9 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { // Lookup the two existing IDs to make sure they are legit myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Do the same a second time @@ -1457,9 +1674,9 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { // Lookup the two existing IDs to make sure they are legit myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } @@ -1491,17 +1708,30 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(8, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); - assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // Do the same a second time + input = new Bundle(); + for (int i = 0; i < 5; i++) { + Patient patient = new Patient(); + patient.getMeta().addProfile("http://example.com/profile"); + patient.getMeta().addTag().setSystem("http://example.com/tags").setCode("tag-1"); + patient.getMeta().addTag().setSystem("http://example.com/tags").setCode("tag-2"); + input.addEntry() + .setResource(patient) + .getRequest() + .setMethod(Bundle.HTTPVerb.POST) + .setUrl("Patient"); + } + myCaptureQueriesListener.clear(); mySystemDao.transaction(mySrd, input); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(5, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } @@ -1556,24 +1786,63 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); mySystemDao.transaction(new SystemRequestDetails(), supplier.get()); -// myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); - assertEquals(13, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(9, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); + assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); myCaptureQueriesListener.clear(); mySystemDao.transaction(new SystemRequestDetails(), supplier.get()); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(11, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(7, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); - - // assertEquals(15, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); - // assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - // assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); - // assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } + + @Test + public void testMassIngestionMode_TransactionWithChanges_2() throws IOException { + myDaoConfig.setDeleteEnabled(false); + myDaoConfig.setMatchUrlCache(true); + myDaoConfig.setMassIngestionMode(true); + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myModelConfig.setRespectVersionsForSearchIncludes(true); + myDaoConfig.setTagStorageMode(DaoConfig.TagStorageModeEnum.NON_VERSIONED); + myModelConfig.setAutoVersionReferenceAtPaths( + "ExplanationOfBenefit.patient", + "ExplanationOfBenefit.insurance.coverage" + ); + + // Pre-cache tag definitions + Patient patient = new Patient(); + patient.getMeta().addProfile("http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"); + patient.getMeta().addProfile("http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization"); + patient.getMeta().addProfile("http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner"); + patient.getMeta().addProfile("http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Professional-NonClinician"); + patient.getMeta().addProfile("http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Coverage"); + patient.setActive(true); + myPatientDao.create(patient); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(new SystemRequestDetails(), loadResourceFromClasspath(Bundle.class, "r4/transaction-perf-bundle.json")); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(10, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); + + // Now a copy that has differences in the EOB and Patient resources + myCaptureQueriesListener.clear(); + mySystemDao.transaction(new SystemRequestDetails(), loadResourceFromClasspath(Bundle.class, "r4/transaction-perf-bundle-smallchanges.json")); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); + assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); + + } + + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index bdc57e62ce0..4498cad3b7e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -402,9 +402,11 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test SearchParameterMap params = new SearchParameterMap(); params.add("myDoctor", new ReferenceParam("A")); + myCaptureQueriesListener.clear(); IBundleProvider outcome = myPatientDao.search(params); List ids = toUnqualifiedVersionlessIdValues(outcome); ourLog.info("IDS: " + ids); + myCaptureQueriesListener.logSelectQueries(); assertThat(ids, Matchers.contains(pid.getValue())); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java index 90497508a1a..a91c38bdfe8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java @@ -38,6 +38,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.stream.Collectors; @@ -1196,12 +1197,12 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test { // Forced ID resolution resultingQueryNotFormatted = queries.get(0); assertThat(resultingQueryNotFormatted, containsString("RESOURCE_TYPE='Organization'")); - assertThat(resultingQueryNotFormatted, containsString("FORCED_ID in ('ORG0' , 'ORG1' , 'ORG2' , 'ORG3' , 'ORG4')")); + assertThat(resultingQueryNotFormatted, containsString("forcedid0_.RESOURCE_TYPE='Organization' and forcedid0_.FORCED_ID='ORG1' or forcedid0_.RESOURCE_TYPE='Organization' and forcedid0_.FORCED_ID='ORG2'")); // The search itself resultingQueryNotFormatted = queries.get(1); assertEquals(1, StringUtils.countMatches(resultingQueryNotFormatted, "Patient.managingOrganization"), resultingQueryNotFormatted); - assertThat(resultingQueryNotFormatted, matchesPattern(".*TARGET_RESOURCE_ID IN \\('[0-9]+','[0-9]+','[0-9]+','[0-9]+','[0-9]+'\\).*")); + assertThat(resultingQueryNotFormatted.toUpperCase(Locale.US), matchesPattern(".*TARGET_RESOURCE_ID IN \\('[0-9]+','[0-9]+','[0-9]+','[0-9]+','[0-9]+'\\).*")); // Ensure that the search actually worked assertEquals(5, search.size().intValue()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TagsTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TagsTest.java new file mode 100644 index 00000000000..f47d25d3c29 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TagsTest.java @@ -0,0 +1,192 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings({"unchecked", "deprecation", "Duplicates"}) +public class FhirResourceDaoR4TagsTest extends BaseJpaR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4TagsTest.class); + + @AfterEach + public final void after() { + myDaoConfig.setTagStorageMode(DaoConfig.DEFAULT_TAG_STORAGE_MODE); + } + + + @Test + public void testStoreAndRetrieveNonVersionedTags_Read() { + initializeNonVersioned(); + + // Read + + Patient patient; + patient = myPatientDao.read(new IdType("Patient/A"), mySrd); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + + } + + @Test + public void testStoreAndRetrieveVersionedTags_Read() { + initializeVersioned(); + + // Read + + Patient patient; + patient = myPatientDao.read(new IdType("Patient/A"), mySrd); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + + } + + @Test + public void testStoreAndRetrieveVersionedTags_VRead() { + initializeVersioned(); + + Patient patient = myPatientDao.read(new IdType("Patient/A/_history/1"), mySrd); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile1")); + assertThat(toTags(patient).toString(), toTags(patient), contains("http://tag1|vtag1|dtag1")); + + patient = myPatientDao.read(new IdType("Patient/A/_history/2"), mySrd); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + + } + + @Test + public void testStoreAndRetrieveNonVersionedTags_VRead() { + initializeNonVersioned(); + + Patient patient = myPatientDao.read(new IdType("Patient/A/_history/1"), mySrd); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + + patient = myPatientDao.read(new IdType("Patient/A/_history/2"), mySrd); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + + } + + @Test + public void testStoreAndRetrieveVersionedTags_History() { + initializeVersioned(); + + IBundleProvider history = myPatientDao.history(null, null, mySrd); + + // Version 1 + Patient patient = (Patient) history.getResources(0, 999).get(1); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile1")); + assertThat(toTags(patient).toString(), toTags(patient), contains("http://tag1|vtag1|dtag1")); + + // Version 2 + patient = (Patient) history.getResources(0, 999).get(0); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + } + + + @Test + public void testStoreAndRetrieveNonVersionedTags_History() { + initializeNonVersioned(); + + IBundleProvider history = myPatientDao.history(null, null, mySrd); + + // Version 1 + Patient patient = (Patient) history.getResources(0, 999).get(1); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + + // Version 2 + patient = (Patient) history.getResources(0, 999).get(0); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + } + + + @Test + public void testStoreAndRetrieveVersionedTags_Search() { + initializeVersioned(); + + IBundleProvider search = myPatientDao.search(new SearchParameterMap()); + + Patient patient = (Patient) search.getResources(0, 999).get(0); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + } + + + @Test + public void testStoreAndRetrieveNonVersionedTags_Search() { + initializeNonVersioned(); + + IBundleProvider search = myPatientDao.search(new SearchParameterMap()); + + Patient patient = (Patient) search.getResources(0, 999).get(0); + assertThat(toProfiles(patient).toString(), toProfiles(patient), contains("http://profile2")); + assertThat(toTags(patient).toString(), toTags(patient), containsInAnyOrder("http://tag1|vtag1|dtag1", "http://tag2|vtag2|dtag2")); + } + + + + private void initializeNonVersioned() { + myDaoConfig.setTagStorageMode(DaoConfig.TagStorageModeEnum.NON_VERSIONED); + + Patient patient = new Patient(); + patient.setId("Patient/A"); + patient.getMeta().addProfile("http://profile1"); + patient.getMeta().addTag("http://tag1", "vtag1", "dtag1"); + patient.setActive(true); + myPatientDao.update(patient, mySrd); + + patient = new Patient(); + patient.setId("Patient/A"); + patient.getMeta().addProfile("http://profile2"); + patient.getMeta().addTag("http://tag2", "vtag2", "dtag2"); + patient.setActive(false); + assertEquals("2", myPatientDao.update(patient, mySrd).getId().getVersionIdPart()); + } + + private void initializeVersioned() { + myDaoConfig.setTagStorageMode(DaoConfig.TagStorageModeEnum.VERSIONED); + + Patient patient = new Patient(); + patient.setId("Patient/A"); + patient.getMeta().addProfile("http://profile1"); + patient.getMeta().addTag("http://tag1", "vtag1", "dtag1"); + patient.setActive(true); + myPatientDao.update(patient, mySrd); + + patient = new Patient(); + patient.setId("Patient/A"); + patient.getMeta().addProfile("http://profile2"); + patient.getMeta().addTag("http://tag2", "vtag2", "dtag2"); + patient.setActive(false); + assertEquals("2", myPatientDao.update(patient, mySrd).getId().getVersionIdPart()); + } + + @Nonnull + private List toTags(Patient patient) { + return patient.getMeta().getTag().stream().map(t -> t.getSystem() + "|" + t.getCode() + "|" + t.getDisplay()).collect(Collectors.toList()); + } + + @Nonnull + private List toProfiles(Patient patient) { + return patient.getMeta().getProfile().stream().map(t -> t.getValue()).collect(Collectors.toList()); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index d63231787fd..100f9988c01 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -279,7 +279,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { table.setIndexStatus(null); table.setDeleted(new Date()); table = myResourceTableDao.saveAndFlush(table); - ResourceHistoryTable newHistory = table.toHistory(); + ResourceHistoryTable newHistory = table.toHistory(true); ResourceHistoryTable currentHistory = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(table.getId(), 1L); newHistory.setEncoding(currentHistory.getEncoding()); newHistory.setResource(currentHistory.getResource()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index ee2ef1e107e..24bd38d0eb3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -1706,13 +1706,9 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { p.addIdentifier().setSystem("urn:system").setValue(methodName); request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setIfNoneExist("Patient?identifier=urn%3Asystem%7C" + methodName); - try { - mySystemDao.transaction(mySrd, request); - fail(); - } catch (InvalidRequestException e) { - assertEquals(e.getMessage(), - "Unable to process Transaction - Request would cause multiple resources to match URL: \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateWithDuplicateMatchUrl01\". Does transaction request contain duplicates?"); - } + mySystemDao.transaction(mySrd, request); + assertEquals(1, logAllResources()); + assertEquals(1, logAllResourceVersions()); } @Test @@ -1828,7 +1824,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { mySystemDao.transaction(mySrd, request); fail(); } catch (InvalidRequestException e) { - assertEquals("Invalid match URL[A] - URL has no search parameters", e.getMessage()); + assertEquals("Invalid match URL[Observation?A] - URL has no search parameters", e.getMessage()); } } @@ -2406,13 +2402,9 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { .setMethod(HTTPVerb.POST) .setIfNoneExist("Encounter?identifier=urn:foo|12345"); - try { - mySystemDao.transaction(mySrd, inputBundle); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", - e.getMessage()); - } + mySystemDao.transaction(mySrd, inputBundle); + assertEquals(1, logAllResources()); + assertEquals(1, logAllResourceVersions()); } @Test @@ -2437,14 +2429,10 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { .setMethod(HTTPVerb.PUT) .setUrl("Encounter?identifier=urn:foo|12345"); - try { - mySystemDao.transaction(mySrd, inputBundle); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", - e.getMessage()); - } + mySystemDao.transaction(mySrd, inputBundle); + assertEquals(1, logAllResources()); + assertEquals(1, logAllResourceVersions()); } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index 7350f9c9761..51d4a82ce87 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -39,11 +39,13 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.BundleBuilder; import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Observation; @@ -51,6 +53,7 @@ import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.SearchParameter; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -60,6 +63,9 @@ import org.slf4j.LoggerFactory; import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; @@ -609,6 +615,8 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { createUniqueCompositeSp(); createRequestId(); + addReadPartition(myPartitionId); + addReadPartition(myPartitionId); addCreatePartition(myPartitionId, myPartitionDate); addCreatePartition(myPartitionId, myPartitionDate); @@ -2548,7 +2556,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true)); String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); - assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID IN ('1')"), searchSql); + assertEquals(1, StringUtils.countMatches(searchSql.toUpperCase(Locale.US), "PARTITION_ID IN ('1')"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); // Same query, different partition @@ -2583,7 +2591,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID IS NULL"), searchSql); + assertEquals(1, StringUtils.countMatches(searchSql.toUpperCase(Locale.US), "PARTITION_ID IS NULL"), searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); // Same query, different partition @@ -2599,6 +2607,127 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { } + @Test + public void testTransaction_MultipleConditionalUpdates() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + + AtomicInteger counter = new AtomicInteger(0); + Supplier input = () -> { + BundleBuilder bb = new BundleBuilder(myFhirCtx); + + Patient pt = new Patient(); + pt.setId(IdType.newRandomUuid()); + pt.addIdentifier().setSystem("http://foo").setValue("123"); + bb.addTransactionCreateEntry(pt).conditional("Patient?identifier=http://foo|123"); + + Observation obsA = new Observation(); + obsA.getSubject().setReference(pt.getId()); + obsA.getCode().addCoding().setSystem("http://foo").setCode("bar1"); + obsA.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsA.setEffective(new DateTimeType(new Date())); + obsA.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsA).conditional("Observation?code=http://foo|bar1"); + + Observation obsB = new Observation(); + obsB.getSubject().setReference(pt.getId()); + obsB.getCode().addCoding().setSystem("http://foo").setCode("bar2"); + obsB.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsB.setEffective(new DateTimeType(new Date())); + obsB.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsB).conditional("Observation?code=http://foo|bar2"); + + Observation obsC = new Observation(); + obsC.getSubject().setReference(pt.getId()); + obsC.getCode().addCoding().setSystem("http://foo").setCode("bar3"); + obsC.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsC.setEffective(new DateTimeType(new Date())); + obsC.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsC).conditional("Observation?code=bar3"); + + Observation obsD = new Observation(); + obsD.getSubject().setReference(pt.getId()); + obsD.getCode().addCoding().setSystem("http://foo").setCode("bar4"); + obsD.setValue(new Quantity(null, 1, "http://unitsofmeasure.org", "kg", "kg")); + obsD.setEffective(new DateTimeType(new Date())); + obsD.addNote().setText("Foo " + counter.incrementAndGet()); // changes every time + bb.addTransactionUpdateEntry(obsD).conditional("Observation?code=bar4"); + + return (Bundle)bb.getBundle(); + }; + + ourLog.info("About to start transaction"); + + for (int i = 0; i < 20; i++) { + addReadPartition(1); + } + for (int i = 0; i < 8; i++) { + addCreatePartition(1, null); + } + + // Pre-fetch the partition ID from the partition lookup table + createPatient(withPartition(1), withActiveTrue()); + + myCaptureQueriesListener.clear(); + Bundle outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(1, myCaptureQueriesListener.countSelectQueries()); + assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), containsString("resourcein0_.HASH_SYS_AND_VALUE='-4132452001562191669' and (resourcein0_.PARTITION_ID in ('1'))")); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(6, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(1, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Run a second time + */ + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(11, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Third time with mass ingestion mode enabled + */ + myDaoConfig.setMassIngestionMode(true); + myDaoConfig.setMatchUrlCache(true); + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(6, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + + /* + * Fourth time with mass ingestion mode enabled + */ + + myCaptureQueriesListener.clear(); + outcome = mySystemDao.transaction(mySrd, input.get()); + ourLog.info("Resp: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + myCaptureQueriesListener.logSelectQueries(); + assertEquals(5, myCaptureQueriesListener.countSelectQueries()); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(1, myCaptureQueriesListener.countInsertQueries()); + myCaptureQueriesListener.logUpdateQueries(); + assertEquals(2, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + } + + @Test public void testUpdate_ResourcePreExistsInWrongPartition() { IIdType patientId = createPatient(withPutPartition(null), withId("ONE"), withBirthdate("2020-01-01")); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java index 83a6ac4392f..323b00df0f8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.jpa.dao.JpaResourceDao; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; import ca.uhn.fhir.jpa.dao.r4.FhirSystemDaoR4; import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; @@ -131,6 +132,8 @@ public class GiantTransactionPerfTest { private MockResourceHistoryTableDao myResourceHistoryTableDao; private SearchParamPresenceSvcImpl mySearchParamPresenceSvc; private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; + @Mock + private IdHelperService myIdHelperService; @AfterEach public void afterEach() { @@ -172,6 +175,9 @@ public class GiantTransactionPerfTest { myTransactionProcessor.setModelConfig(myDaoConfig.getModelConfig()); myTransactionProcessor.setHapiTransactionService(myHapiTransactionService); myTransactionProcessor.setDaoRegistry(myDaoRegistry); + myTransactionProcessor.setPartitionSettingsForUnitTest(myPartitionSettings); + myTransactionProcessor.setIdHelperServiceForUnitTest(myIdHelperService); + myTransactionProcessor.setFhirContextForUnitTest(myCtx); myTransactionProcessor.start(); mySystemDao = new FhirSystemDaoR4(); @@ -248,6 +254,7 @@ public class GiantTransactionPerfTest { myEobDao.setSearchParamPresenceSvc(mySearchParamPresenceSvc); myEobDao.setDaoSearchParamSynchronizer(myDaoSearchParamSynchronizer); myEobDao.setDaoConfigForUnitTest(myDaoConfig); + myEobDao.setIdHelperSvcForUnitTest(myIdHelperService); myEobDao.start(); myDaoRegistry.setResourceDaos(Lists.newArrayList(myEobDao)); diff --git a/hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle-smallchanges.json b/hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle-smallchanges.json new file mode 100644 index 00000000000..8cc029b5a47 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle-smallchanges.json @@ -0,0 +1,904 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "ExplanationOfBenefit", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Professional-NonClinician" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType", + "code": "payerid" + } + ] + }, + "system": "https://hl7.org/fhir/sid/payerid", + "value": "37525500673" + }, + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType", + "code": "uc" + } + ] + }, + "system": "https://hl7.org/fhir/sid/claimid", + "value": "26723516" + } + ], + "status": "active", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/claim-type", + "code": "professional" + } + ] + }, + "use": "claim", + "patient": { + "reference": "Patient/d16f4424-9703-23bf-8331-3fc4bceb0c21" + }, + "billablePeriod": { + "start": "2018-01-09", + "end": "2018-01-09" + }, + "created": "2018-01-08T00:00:00-08:00", + "insurer": { + "reference": "Organization/b77d3b98-03d8-1f0a-07b7-30b636c6ea9b" + }, + "provider": { + "reference": "Organization/e03b46ec-94df-0849-49eb-f5bba0c024c2" + }, + "payee": { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/payeetype", + "code": "provider" + } + ], + "text": "Claim paid to VENDOR" + } + }, + "facility": { + "reference": "Location/11651884-37d2-eede-e1b9-059afd90811a" + }, + "outcome": "complete", + "disposition": "DENIED", + "careTeam": [ + { + "sequence": 1, + "provider": { + "reference": "Practitioner/d2fc93e1-e1f8-c6d3-6c2c-9301f0e02c7c" + }, + "responsible": true, + "role": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole", + "code": "performing" + } + ] + } + } + ], + "supportingInfo": [ + { + "sequence": 1, + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType", + "code": "clmrecvddate" + } + ] + }, + "timingDate": "2018-01-08" + }, + { + "sequence": 2, + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType", + "code": "billingnetworkcontractingstatus" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBPayerAdjudicationStatus", + "code": "contracted" + } + ] + } + } + ], + "diagnosis": [ + { + "sequence": 1, + "diagnosisCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "M47.012", + "display": "ANT SPINAL ART COMPRESSION SYND CERVICAL REGION" + } + ], + "text": "ANT SPINAL ART COMPRESSION SYND CERVICAL REGION" + }, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype", + "code": "principal" + } + ] + } + ] + } + ], + "procedure": [ + { + "sequence": 1, + "date": "2018-01-08T00:00:00-08:00", + "procedureCodeableConcept": { + "coding": [ + { + "system": "http://www.ama-assn.org/go/cpt", + "code": "L0454", + "display": "TLSO FLEX PREFAB SACROCOC-T9" + } + ], + "text": "TLSO FLEXIBLE SC JUNCT TO T-9 PREFAB CUSTOM FIT" + } + } + ], + "insurance": [ + { + "focal": true, + "coverage": { + "reference": "urn:uuid:a8430b1b-1f26-44ea-8866-a605ebb48f21" + } + } + ], + "item": [ + { + "sequence": 1, + "diagnosisSequence": [ + 1 + ], + "procedureSequence": [ + 1 + ], + "productOrService": { + "coding": [ + { + "system": "http://www.ama-assn.org/go/cpt", + "code": "L0454", + "display": "TLSO FLEX PREFAB SACROCOC-T9" + } + ], + "text": "TLSO FLEXIBLE SC JUNCT TO T-9 PREFAB CUSTOM FIT" + }, + "modifier": [ + { + "coding": [ + { + "system": "http://www.ama-assn.org/go/cpt", + "code": "NU", + "display": "NEW EQUIPMENT" + } + ], + "text": "NEW EQUIPMENT" + } + ], + "servicedPeriod": { + "start": "2018-01-08", + "end": "2018-01-08" + }, + "locationCodeableConcept": { + "coding": [ + { + "system": "https://www.cms.gov/Medicare/Coding/place-of-service-codes/Place_of_Service_Code_Set", + "code": "11" + } + ] + }, + "quantity": { + "value": 1, + "unit": "Units", + "system": "http://unitsofmeasure.org", + "code": "[arb'U]" + }, + "net": { + "value": 704.26, + "currency": "USD" + }, + "noteNumber": [ + 1, + 2 + ], + "adjudication": [ + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "submitted" + } + ] + }, + "amount": { + "value": 704.26, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "benefit" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "copay" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "deductible" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "coinsurance" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "memberliability" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "noncovered" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "priorpayerpaid" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "paidtoprovider" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBPayerAdjudicationStatus", + "code": "outofnetwork" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + } + ] + } + ], + "total": [ + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "submitted" + } + ] + }, + "amount": { + "value": 704.26, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "benefit" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "copay" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "deductible" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "coinsurance" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "memberliability" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "noncovered" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "priorpayerpaid" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "paidtoprovider" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + } + ], + "payment": { + "date": "2021-01-22", + "amount": { + "value": 0, + "currency": "USD" + } + }, + "processNote": [ + { + "number": 1, + "type": "display", + "text": "AUD02: DENY, NOT AUTHORIZED, PROVIDER LIABILITY" + }, + { + "number": 2, + "type": "display", + "text": "BED08: DENY, PROCEDURE NOT COVERED" + } + ] + }, + "request": { + "method": "PUT", + "url": "ExplanationOfBenefit?identifier=37525500673" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "d16f4424-9703-23bf-8331-3fc4bceb0c21", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "https://healthy.kaiserpermanente.org/front-door", + "value": "1000116-GA" + } + ], + "name": [ + { + "use": "usual", + "text": "Gaabcseven Testing", + "family": "Testing", + "given": [ + "Gaabcsix" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "662-123-3456", + "use": "home" + } + ], + "gender": "male", + "birthDate": "1961-01-01", + "address": [ + { + "use": "home", + "type": "postal", + "line": [ + "TEST ADDRESS AVE, APT 234" + ], + "city": "ATLANTA", + "state": "GA", + "postalCode": "30301" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/d16f4424-9703-23bf-8331-3fc4bceb0c21" + } + }, + { + "fullUrl": "urn:uuid:a8430b1b-1f26-44ea-8866-a605ebb48f21", + "resource": { + "resourceType": "Coverage", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Coverage" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "FILL" + } + ] + }, + "system": "https://hl7.org/fhir/sid/coverageid", + "value": "1000116-GA-10159" + } + ], + "status": "active", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "HMO", + "display": "health maintenance organization policy" + } + ], + "text": "COMMERCIAL HMO-HMO-Amb Accum" + }, + "subscriberId": "1000116", + "beneficiary": { + "reference": "Patient/d16f4424-9703-23bf-8331-3fc4bceb0c21" + }, + "relationship": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/subscriber-relationship", + "code": "self", + "display": "Self" + } + ], + "text": "The Beneficiary is the Subscriber" + }, + "period": { + "start": "2017-01-01" + }, + "payor": [ + { + "reference": "Organization/b77d3b98-03d8-1f0a-07b7-30b636c6ea9b" + } + ], + "class": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/coverage-class", + "code": "group", + "display": "Group" + } + ], + "text": "An employee group" + }, + "value": "10159", + "name": "10159-100 STATE DEPTS, DFACS, HEALTH-NON-MEDICARE" + } + ] + }, + "request": { + "method": "PUT", + "url": "Coverage?identifier=1000116-GA-10159" + } + }, + { + "resource": { + "resourceType": "Organization", + "id": "e03b46ec-94df-0849-49eb-f5bba0c024c2", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType", + "code": "npi" + } + ] + }, + "system": "http://hl7.org/fhir/sid/us-npi" + }, + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "TAX" + } + ] + }, + "system": "urn:oid:2.16.840.1.113883.4.4", + "value": "330057155" + } + ], + "active": true, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/organization-type", + "code": "prov" + } + ] + } + ], + "name": "APRIA HEALTHCARE LLC", + "address": [ + { + "use": "work", + "type": "physical", + "line": [ + "2508 SOLUTIONS CENTER" + ], + "city": "CHICAGO", + "state": "IL", + "postalCode": "60677-2005", + "country": "USA" + } + ] + }, + "request": { + "method": "PUT", + "url": "Organization/e03b46ec-94df-0849-49eb-f5bba0c024c2" + } + }, + { + "resource": { + "resourceType": "Organization", + "id": "b77d3b98-03d8-1f0a-07b7-30b636c6ea9b", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization" + ] + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "FILL" + } + ] + }, + "system": "https://hl7.org/fhir/sid/organizationid", + "value": "NATLTAP GA-KFHP-GA" + } + ], + "active": true, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/organization-type", + "code": "pay", + "display": "Payer" + } + ] + } + ], + "name": "KAISER FOUNDATION HEALTHPLAN, INC", + "telecom": [ + { + "system": "phone", + "value": "1-888-865-5813", + "use": "work" + } + ], + "address": [ + { + "use": "work", + "type": "postal", + "line": [ + "NATIONAL CLAIMS ADMINISTRATION GEORGIA", + "PO Box 629028" + ], + "city": "El Dorado Hills", + "state": "CA", + "postalCode": "95762-9028" + } + ] + }, + "request": { + "method": "PUT", + "url": "Organization/b77d3b98-03d8-1f0a-07b7-30b636c6ea9b" + } + }, + { + "resource": { + "resourceType": "Practitioner", + "id": "d2fc93e1-e1f8-c6d3-6c2c-9301f0e02c7c", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner" + ] + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "NPI" + } + ] + }, + "system": "http://hl7.org/fhir/sid/us-npi", + "value": "PIN2001487498" + } + ], + "name": [ + { + "use": "usual", + "text": "APRIA HEALTHCARE LLC", + "family": "APRIA HEALTHCARE LLC" + } + ], + "address": [ + { + "use": "work", + "line": [ + "805 MARATHON PARKWAY", + "SUITE 160" + ], + "city": "LAWRENCEVILLE", + "state": "GA", + "postalCode": "30046" + } + ] + }, + "request": { + "method": "PUT", + "url": "Practitioner/d2fc93e1-e1f8-c6d3-6c2c-9301f0e02c7c" + } + }, + { + "resource": { + "resourceType": "Location", + "id": "11651884-37d2-eede-e1b9-059afd90811a", + "meta": { + "lastUpdated": "2021-06-07" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "NPI" + } + ] + }, + "value": "PIN12120678" + } + ], + "status": "active", + "name": "APRIA HEALTHCARE INC-30013", + "mode": "kind", + "type": [ + { + "coding": [ + { + "system": "https://www.cms.gov/Medicare/Coding/place-of-service-codes/Place_of_Service_Code_Set", + "code": "99" + } + ] + } + ], + "address": { + "use": "work", + "type": "physical", + "line": [ + "594 SIGMAN RD STE 100" + ], + "city": "CONYERS", + "state": "GA", + "postalCode": "30013-1365" + } + }, + "request": { + "method": "PUT", + "url": "Location/11651884-37d2-eede-e1b9-059afd90811a" + } + } + ] +} diff --git a/hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle.json b/hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle.json new file mode 100644 index 00000000000..8f3a8929fa1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/r4/transaction-perf-bundle.json @@ -0,0 +1,904 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "ExplanationOfBenefit", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Professional-NonClinician" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType", + "code": "payerid" + } + ] + }, + "system": "https://hl7.org/fhir/sid/payerid", + "value": "37525500673" + }, + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType", + "code": "uc" + } + ] + }, + "system": "https://hl7.org/fhir/sid/claimid", + "value": "26723516" + } + ], + "status": "active", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/claim-type", + "code": "professional" + } + ] + }, + "use": "claim", + "patient": { + "reference": "Patient/d16f4424-9703-23bf-8331-3fc4bceb0c21" + }, + "billablePeriod": { + "start": "2018-01-08", + "end": "2018-01-08" + }, + "created": "2018-01-08T00:00:00-08:00", + "insurer": { + "reference": "Organization/b77d3b98-03d8-1f0a-07b7-30b636c6ea9b" + }, + "provider": { + "reference": "Organization/e03b46ec-94df-0849-49eb-f5bba0c024c2" + }, + "payee": { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/payeetype", + "code": "provider" + } + ], + "text": "Claim paid to VENDOR" + } + }, + "facility": { + "reference": "Location/11651884-37d2-eede-e1b9-059afd90811a" + }, + "outcome": "complete", + "disposition": "DENIED", + "careTeam": [ + { + "sequence": 1, + "provider": { + "reference": "Practitioner/d2fc93e1-e1f8-c6d3-6c2c-9301f0e02c7c" + }, + "responsible": true, + "role": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole", + "code": "performing" + } + ] + } + } + ], + "supportingInfo": [ + { + "sequence": 1, + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType", + "code": "clmrecvddate" + } + ] + }, + "timingDate": "2018-01-08" + }, + { + "sequence": 2, + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType", + "code": "billingnetworkcontractingstatus" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBPayerAdjudicationStatus", + "code": "contracted" + } + ] + } + } + ], + "diagnosis": [ + { + "sequence": 1, + "diagnosisCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "M47.012", + "display": "ANT SPINAL ART COMPRESSION SYND CERVICAL REGION" + } + ], + "text": "ANT SPINAL ART COMPRESSION SYND CERVICAL REGION" + }, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype", + "code": "principal" + } + ] + } + ] + } + ], + "procedure": [ + { + "sequence": 1, + "date": "2018-01-08T00:00:00-08:00", + "procedureCodeableConcept": { + "coding": [ + { + "system": "http://www.ama-assn.org/go/cpt", + "code": "L0454", + "display": "TLSO FLEX PREFAB SACROCOC-T9" + } + ], + "text": "TLSO FLEXIBLE SC JUNCT TO T-9 PREFAB CUSTOM FIT" + } + } + ], + "insurance": [ + { + "focal": true, + "coverage": { + "reference": "urn:uuid:a8430b1b-1f26-44ea-8866-a605ebb48f21" + } + } + ], + "item": [ + { + "sequence": 1, + "diagnosisSequence": [ + 1 + ], + "procedureSequence": [ + 1 + ], + "productOrService": { + "coding": [ + { + "system": "http://www.ama-assn.org/go/cpt", + "code": "L0454", + "display": "TLSO FLEX PREFAB SACROCOC-T9" + } + ], + "text": "TLSO FLEXIBLE SC JUNCT TO T-9 PREFAB CUSTOM FIT" + }, + "modifier": [ + { + "coding": [ + { + "system": "http://www.ama-assn.org/go/cpt", + "code": "NU", + "display": "NEW EQUIPMENT" + } + ], + "text": "NEW EQUIPMENT" + } + ], + "servicedPeriod": { + "start": "2018-01-08", + "end": "2018-01-08" + }, + "locationCodeableConcept": { + "coding": [ + { + "system": "https://www.cms.gov/Medicare/Coding/place-of-service-codes/Place_of_Service_Code_Set", + "code": "11" + } + ] + }, + "quantity": { + "value": 1, + "unit": "Units", + "system": "http://unitsofmeasure.org", + "code": "[arb'U]" + }, + "net": { + "value": 704.26, + "currency": "USD" + }, + "noteNumber": [ + 1, + 2 + ], + "adjudication": [ + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "submitted" + } + ] + }, + "amount": { + "value": 704.26, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "benefit" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "copay" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "deductible" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "coinsurance" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "memberliability" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "noncovered" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "priorpayerpaid" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "paidtoprovider" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBPayerAdjudicationStatus", + "code": "outofnetwork" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + } + ] + } + ], + "total": [ + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "submitted" + } + ] + }, + "amount": { + "value": 704.26, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "benefit" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "copay" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/adjudication", + "code": "deductible" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "coinsurance" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "memberliability" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "noncovered" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "priorpayerpaid" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + }, + { + "category": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBAdjudication", + "code": "paidtoprovider" + } + ] + }, + "amount": { + "value": 0, + "currency": "USD" + } + } + ], + "payment": { + "date": "2021-01-22", + "amount": { + "value": 0, + "currency": "USD" + } + }, + "processNote": [ + { + "number": 1, + "type": "display", + "text": "AUD02: DENY, NOT AUTHORIZED, PROVIDER LIABILITY" + }, + { + "number": 2, + "type": "display", + "text": "BED08: DENY, PROCEDURE NOT COVERED" + } + ] + }, + "request": { + "method": "PUT", + "url": "ExplanationOfBenefit?identifier=37525500673" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "d16f4424-9703-23bf-8331-3fc4bceb0c21", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "https://healthy.kaiserpermanente.org/front-door", + "value": "1000116-GA" + } + ], + "name": [ + { + "use": "usual", + "text": "Gaabcsix Testing", + "family": "Testing", + "given": [ + "Gaabcsix" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "662-123-3456", + "use": "home" + } + ], + "gender": "male", + "birthDate": "1961-01-01", + "address": [ + { + "use": "home", + "type": "postal", + "line": [ + "TEST ADDRESS AVE, APT 234" + ], + "city": "ATLANTA", + "state": "GA", + "postalCode": "30301" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/d16f4424-9703-23bf-8331-3fc4bceb0c21" + } + }, + { + "fullUrl": "urn:uuid:a8430b1b-1f26-44ea-8866-a605ebb48f21", + "resource": { + "resourceType": "Coverage", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Coverage" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "FILL" + } + ] + }, + "system": "https://hl7.org/fhir/sid/coverageid", + "value": "1000116-GA-10159" + } + ], + "status": "active", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "HMO", + "display": "health maintenance organization policy" + } + ], + "text": "COMMERCIAL HMO-HMO-Amb Accum" + }, + "subscriberId": "1000116", + "beneficiary": { + "reference": "Patient/d16f4424-9703-23bf-8331-3fc4bceb0c21" + }, + "relationship": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/subscriber-relationship", + "code": "self", + "display": "Self" + } + ], + "text": "The Beneficiary is the Subscriber" + }, + "period": { + "start": "2017-01-01" + }, + "payor": [ + { + "reference": "Organization/b77d3b98-03d8-1f0a-07b7-30b636c6ea9b" + } + ], + "class": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/coverage-class", + "code": "group", + "display": "Group" + } + ], + "text": "An employee group" + }, + "value": "10159", + "name": "10159-100 STATE DEPTS, DFACS, HEALTH-NON-MEDICARE" + } + ] + }, + "request": { + "method": "PUT", + "url": "Coverage?identifier=1000116-GA-10159" + } + }, + { + "resource": { + "resourceType": "Organization", + "id": "e03b46ec-94df-0849-49eb-f5bba0c024c2", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType", + "code": "npi" + } + ] + }, + "system": "http://hl7.org/fhir/sid/us-npi" + }, + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "TAX" + } + ] + }, + "system": "urn:oid:2.16.840.1.113883.4.4", + "value": "330057155" + } + ], + "active": true, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/organization-type", + "code": "prov" + } + ] + } + ], + "name": "APRIA HEALTHCARE LLC", + "address": [ + { + "use": "work", + "type": "physical", + "line": [ + "2508 SOLUTIONS CENTER" + ], + "city": "CHICAGO", + "state": "IL", + "postalCode": "60677-2005", + "country": "USA" + } + ] + }, + "request": { + "method": "PUT", + "url": "Organization/e03b46ec-94df-0849-49eb-f5bba0c024c2" + } + }, + { + "resource": { + "resourceType": "Organization", + "id": "b77d3b98-03d8-1f0a-07b7-30b636c6ea9b", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization" + ] + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "FILL" + } + ] + }, + "system": "https://hl7.org/fhir/sid/organizationid", + "value": "NATLTAP GA-KFHP-GA" + } + ], + "active": true, + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/organization-type", + "code": "pay", + "display": "Payer" + } + ] + } + ], + "name": "KAISER FOUNDATION HEALTHPLAN, INC", + "telecom": [ + { + "system": "phone", + "value": "1-888-865-5813", + "use": "work" + } + ], + "address": [ + { + "use": "work", + "type": "postal", + "line": [ + "NATIONAL CLAIMS ADMINISTRATION GEORGIA", + "PO Box 629028" + ], + "city": "El Dorado Hills", + "state": "CA", + "postalCode": "95762-9028" + } + ] + }, + "request": { + "method": "PUT", + "url": "Organization/b77d3b98-03d8-1f0a-07b7-30b636c6ea9b" + } + }, + { + "resource": { + "resourceType": "Practitioner", + "id": "d2fc93e1-e1f8-c6d3-6c2c-9301f0e02c7c", + "meta": { + "lastUpdated": "2021-06-07", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner" + ] + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "NPI" + } + ] + }, + "system": "http://hl7.org/fhir/sid/us-npi", + "value": "PIN2001487498" + } + ], + "name": [ + { + "use": "usual", + "text": "APRIA HEALTHCARE LLC", + "family": "APRIA HEALTHCARE LLC" + } + ], + "address": [ + { + "use": "work", + "line": [ + "805 MARATHON PARKWAY", + "SUITE 160" + ], + "city": "LAWRENCEVILLE", + "state": "GA", + "postalCode": "30046" + } + ] + }, + "request": { + "method": "PUT", + "url": "Practitioner/d2fc93e1-e1f8-c6d3-6c2c-9301f0e02c7c" + } + }, + { + "resource": { + "resourceType": "Location", + "id": "11651884-37d2-eede-e1b9-059afd90811a", + "meta": { + "lastUpdated": "2021-06-07" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "NPI" + } + ] + }, + "value": "PIN12120678" + } + ], + "status": "active", + "name": "APRIA HEALTHCARE INC-30013", + "mode": "kind", + "type": [ + { + "coding": [ + { + "system": "https://www.cms.gov/Medicare/Coding/place-of-service-codes/Place_of_Service_Code_Set", + "code": "99" + } + ] + } + ], + "address": { + "use": "work", + "type": "physical", + "line": [ + "594 SIGMAN RD STE 100" + ], + "city": "CONYERS", + "state": "GA", + "postalCode": "30013-1365" + } + }, + "request": { + "method": "PUT", + "url": "Location/11651884-37d2-eede-e1b9-059afd90811a" + } + } + ] +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java index 6321ab1bd9e..7e50bf27652 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java @@ -184,7 +184,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa myHashIdentity = theHashIdentity; } - Long getHashSystemAndValue() { + public Long getHashSystemAndValue() { return myHashSystemAndValue; } @@ -192,7 +192,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa myHashSystemAndValue = theHashSystemAndValue; } - Long getHashValue() { + public Long getHashValue() { return myHashValue; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index 6e29ec79991..08c3fe5dbdb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -604,7 +604,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas myNarrativeText = theNarrativeText; } - public ResourceHistoryTable toHistory() { + public ResourceHistoryTable toHistory(boolean theCreateVersionTags) { ResourceHistoryTable retVal = new ResourceHistoryTable(); retVal.setResourceId(myId); @@ -623,7 +623,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas retVal.getTags().clear(); retVal.setHasTags(isHasTags()); - if (isHasTags()) { + if (isHasTags() && theCreateVersionTags) { for (ResourceTag next : getTags()) { retVal.addTag(next); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java index 0aee3537bd6..1a45a9c9256 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.api.server.storage; */ import ca.uhn.fhir.util.ObjectUtil; +import org.hl7.fhir.instance.model.api.IIdType; import java.util.ArrayList; import java.util.Collection; @@ -32,9 +33,9 @@ import java.util.Optional; * a Long, a String, or something else. */ public class ResourcePersistentId { - private Object myId; private Long myVersion; + private IIdType myAssociatedResourceId; public ResourcePersistentId(Object theId) { this(theId, null); @@ -50,6 +51,15 @@ public class ResourcePersistentId { myVersion = theVersion; } + public IIdType getAssociatedResourceId() { + return myAssociatedResourceId; + } + + public ResourcePersistentId setAssociatedResourceId(IIdType theAssociatedResourceId) { + myAssociatedResourceId = theAssociatedResourceId; + return this; + } + @Override public boolean equals(Object theO) { if (!(theO instanceof ResourcePersistentId)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java index 80af36bfbfe..9a31ce24998 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java @@ -28,6 +28,7 @@ import com.google.common.collect.ListMultimap; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IIdType; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collections; import java.util.Date; @@ -50,8 +51,11 @@ import java.util.function.Supplier; */ public class TransactionDetails { + public static final ResourcePersistentId NOT_FOUND = new ResourcePersistentId(-1L); + private final Date myTransactionDate; private Map myResolvedResourceIds = Collections.emptyMap(); + private Map myResolvedMatchUrls = Collections.emptyMap(); private Map myUserData; private ListMultimap myDeferredInterceptorBroadcasts; private EnumSet myDeferredInterceptorBroadcastPointcuts; @@ -82,14 +86,28 @@ public class TransactionDetails { return myResolvedResourceIds.get(idValue); } + /** + * Was the given resource ID resolved previously in this transaction as not existing + */ + public boolean isResolvedResourceIdEmpty(IIdType theId) { + if (myResolvedResourceIds != null) { + if (myResolvedResourceIds.containsKey(theId.toVersionless().getValue())) { + if (myResolvedResourceIds.get(theId.toVersionless().getValue()) == null) { + return true; + } + } + } + return false; + } + + /** * A Resolved Resource ID is a mapping between a resource ID (e.g. "Patient/ABC" or * "Observation/123") and a storage ID for that resource. Resources should only be placed within * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. */ - public void addResolvedResourceId(IIdType theResourceId, ResourcePersistentId thePersistentId) { + public void addResolvedResourceId(IIdType theResourceId, @Nullable ResourcePersistentId thePersistentId) { assert theResourceId != null; - assert thePersistentId != null; if (myResolvedResourceIds.isEmpty()) { myResolvedResourceIds = new HashMap<>(); @@ -97,6 +115,25 @@ public class TransactionDetails { myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId); } + public Map getResolvedMatchUrls() { + return myResolvedMatchUrls; + } + + /** + * A Resolved Conditional URL is a mapping between a conditional URL (e.g. "Patient?identifier=foo|bar" or + * "Observation/123") and a storage ID for that resource. Resources should only be placed within + * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. + */ + public void addResolvedMatchUrl(String theConditionalUrl, @Nonnull ResourcePersistentId thePersistentId) { + Validate.notBlank(theConditionalUrl); + Validate.notNull(thePersistentId); + + if (myResolvedMatchUrls.isEmpty()) { + myResolvedMatchUrls = new HashMap<>(); + } + myResolvedMatchUrls.put(theConditionalUrl, thePersistentId); + } + /** * This is the wall-clock time that a given transaction started. */