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 extends BaseTag> 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 extends BaseTag> myTagList;
+ Collection extends BaseTag> 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 extends IBaseResource> 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