Transaction SQL Optimization (#4679)

* Start work optimizing transaction

* Tons of test cleanup

* Cleanup

* More optimization

* Optimize

* Many tests fixed

* Work on test fixes

* Optimization done, now doing cleanup

* Cleanup

* Add docs

* Test fixes

* Test fix

* License headers

* Test fix

* Test cleanup

* Test fix
This commit is contained in:
James Agnew 2023-03-24 10:29:56 -04:00 committed by GitHub
parent 90895aab57
commit bcc1ca7593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 24464 additions and 293 deletions

View File

@ -35,6 +35,7 @@ import org.apache.http.message.BasicNameValuePair;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
@ -54,7 +55,9 @@ import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.endsWith;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@SuppressWarnings("JavadocLinkAsPlainText")
public class UrlUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class);
@ -134,6 +137,72 @@ public class UrlUtil {
return theExtensionUrl;
}
/**
* Given a FHIR resource URL, extracts the associated resource type. Supported formats
* include the following inputs, all of which will return {@literal Patient}. If no
* resource type can be determined, {@literal null} will be returned.
* <ul>
* <li>Patient
* <li>Patient?
* <li>Patient?identifier=foo
* <li>/Patient
* <li>/Patient?
* <li>/Patient?identifier=foo
* <li>http://foo/base/Patient?identifier=foo
* <li>http://foo/base/Patient/1
* <li>http://foo/base/Patient/1/_history/2
* <li>Patient/1
* <li>Patient/1/_history/2
* <li>/Patient/1
* <li>/Patient/1/_history/2
* </ul>
*/
@Nullable
public static String determineResourceTypeInResourceUrl(FhirContext theFhirContext, String theUrl) {
if (theUrl == null) {
return null;
}
if (theUrl.startsWith("urn:")) {
return null;
}
String resourceType = null;
int qmIndex = theUrl.indexOf("?");
if (qmIndex > 0) {
String urlResourceType = theUrl.substring(0, qmIndex);
int slashIdx = urlResourceType.lastIndexOf('/');
if (slashIdx != -1) {
urlResourceType = urlResourceType.substring(slashIdx + 1);
}
if (isNotBlank(urlResourceType)) {
resourceType = urlResourceType;
}
} else {
resourceType = theUrl;
int slashIdx = resourceType.indexOf('/');
if (slashIdx == 0) {
resourceType = resourceType.substring(1);
}
slashIdx = resourceType.indexOf('/');
if (slashIdx != -1) {
resourceType = new IdDt(resourceType).getResourceType();
}
}
try {
if (isNotBlank(resourceType)) {
theFhirContext.getResourceDefinition(resourceType);
}
} catch (DataFormatException e) {
return null;
}
return resourceType;
}
/**
* URL encode a value according to RFC 3986
* <p>

View File

@ -61,6 +61,7 @@ ca.uhn.fhir.rest.server.method.ResourceParameter.noContentTypeInRequest=No Conte
ca.uhn.fhir.rest.server.method.ResourceParameter.failedToParseRequest=Failed to parse request body as {0} resource. Error was: {1}
ca.uhn.fhir.parser.ParserState.wrongResourceTypeFound=Incorrect resource type found, expected "{0}" but found "{1}"
ca.uhn.fhir.rest.api.server.storage.TransactionDetails.invalidMatchUrlMultipleMatches=Invalid match URL "{0}" - Multiple resources match this search
ca.uhn.fhir.rest.server.RestfulServer.getPagesNonHttpGet=Requests for _getpages must use HTTP GET
ca.uhn.fhir.rest.server.RestfulServer.unknownMethod=Invalid request: The FHIR endpoint on this server does not know how to handle {0} operation[{1}] with parameters [{2}]
ca.uhn.fhir.rest.server.RestfulServer.rootRequest=This is the base URL of FHIR server. Unable to handle this request, as it does not contain a resource type or operation name.
@ -78,7 +79,6 @@ ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor.externalizedBinarySt
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}"
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlMultipleMatches=Invalid match URL "{0}" - Multiple resources match this search
ca.uhn.fhir.jpa.dao.BaseStorageDao.inlineMatchNotSupported=Inline match URLs are not supported on this server. Cannot process reference: "{0}"
ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithMultipleMatchFailure=Failed to {0} resource with match URL "{1}" because this search matched {2} resources
ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithIdNotMatchFailure=Failed to {0} resource with match URL "{1}" because the matching resource does not match the provided ID

View File

@ -0,0 +1,9 @@
---
type: add
issue: 4679
title: "When executing FHIR transactions in the JPA server where the transaction contained large numbers
of inline match URLs, the transaction processor will now prefetch the match URL targets in a single
optimized batch. This avoids a potentially significant number of database round trips. In addition,
the SQL query used for pre-resolving resource update targets in the transaction processor has been
reworked to fully leverage available database indexes. This should result in significant performance
improvements for certain large FHIR transactions."

View File

@ -377,6 +377,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
validateResourceIdCreation(theResource, theRequest);
}
if (theMatchUrl != null) {
// Note: We actually create the search URL below by calling enforceMatchUrlResourceUniqueness
// since we can't do that until we know the assigned PID, but we set this flag up here
// because we need to set it before we persist the ResourceTable entity in order to
// avoid triggering an extra DB update
entity.setSearchUrlPresent(true);
}
// Perform actual DB update
// this call will also update the metadata
ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, false, theTransactionDetails, false, thePerformIndexing);
@ -416,7 +424,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid);
theTransactionDetails.addResolvedResource(jpaPid.getAssociatedResourceId(), theResource);
// Pre-cache the match URL
// Pre-cache the match URL, and create an entry in the HFJ_RES_SEARCH_URL table to
// protect against concurrent writes to the same conditional URL
if (theMatchUrl != null) {
myResourceSearchUrlSvc.enforceMatchUrlResourceUniqueness(getResourceName(), theMatchUrl, jpaPid);
myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theMatchUrl, jpaPid);
@ -1777,8 +1786,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// we stored a resource searchUrl at creation time to prevent resource duplication. Let's remove the entry on the
// first update but guard against unnecessary trips to the database on subsequent ones.
if(theEntity.getVersion() < 2){
ResourceTable entity = (ResourceTable) theEntity;
if (entity.isSearchUrlPresent() && thePerformIndexing) {
myResourceSearchUrlSvc.deleteByResId((Long) theEntity.getPersistentId().getId());
entity.setSearchUrlPresent(false);
}
return super.doUpdateForUpdateOrPatch(theRequest, theResourceId, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theResource, theEntity, theOperationType, theTransactionDetails);

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao;
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.IDaoRegistry;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
@ -30,15 +31,20 @@ import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
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.jpa.util.QueryChunker;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import org.apache.commons.lang3.Validate;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.instance.model.api.IBase;
@ -55,6 +61,7 @@ import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.PersistenceException;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
@ -62,18 +69,16 @@ import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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 ca.uhn.fhir.util.UrlUtil.determineResourceTypeInResourceUrl;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class TransactionProcessor extends BaseTransactionProcessor {
@ -118,6 +123,13 @@ public class TransactionProcessor extends BaseTransactionProcessor {
myFhirContext = theFhirContext;
}
@Override
public void setStorageSettings(StorageSettings theStorageSettings) {
myStorageSettings = (JpaStorageSettings) theStorageSettings;
super.setStorageSettings(theStorageSettings);
}
@Override
protected EntriesToProcessMap doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
IdSubstitutionMap theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) {
@ -128,21 +140,17 @@ public class TransactionProcessor extends BaseTransactionProcessor {
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<RequestPartitionId> requestPartitionIdsForAllEntries = new HashSet<>();
for (IBase nextEntry : theEntries) {
IBaseResource resource = versionAdapter.getResource(nextEntry);
if (resource != null) {
RequestPartitionId requestPartition = myRequestPartitionSvc.determineCreatePartitionForRequest(theRequest, resource, myFhirContext.getResourceType(resource));
requestPartitionIdsForAllEntries.add(requestPartition);
}
}
if (requestPartitionIdsForAllEntries.size() == 1) {
requestPartitionId = requestPartitionIdsForAllEntries.iterator().next();
}
requestPartitionId = getSinglePartitionForAllEntriesOrNull(theRequest, theEntries, versionAdapter);
}
if (requestPartitionId != null) {
preFetch(theTransactionDetails, theEntries, versionAdapter, requestPartitionId);
}
return super.doTransactionWriteOperations(theRequest, theActionName, theTransactionDetails, theAllIds, theIdSubstitutions, theIdToPersistedOutcome, theResponse, theOriginalRequestOrder, theEntries, theTransactionStopWatch);
}
private void preFetch(TransactionDetails theTransactionDetails, List<IBase> theEntries, ITransactionProcessorVersionAdapter theVersionAdapter, RequestPartitionId theRequestPartitionId) {
Set<String> foundIds = new HashSet<>();
List<Long> idsToPreFetch = new ArrayList<>();
@ -150,20 +158,34 @@ public class TransactionProcessor extends BaseTransactionProcessor {
* Pre-Fetch any resources that are referred to normally by ID, e.g.
* regular FHIR updates within the transaction.
*/
preFetchResourcesById(theTransactionDetails, theEntries, theVersionAdapter, theRequestPartitionId, foundIds, idsToPreFetch);
/*
* Pre-resolve any conditional URLs we can
*/
preFetchConditionalUrls(theTransactionDetails, theEntries, theVersionAdapter, theRequestPartitionId, idsToPreFetch);
IFhirSystemDao<?, ?> systemDao = myApplicationContext.getBean(IFhirSystemDao.class);
systemDao.preFetchResources(JpaPid.fromLongList(idsToPreFetch));
}
private void preFetchResourcesById(TransactionDetails theTransactionDetails, List<IBase> theEntries, ITransactionProcessorVersionAdapter theVersionAdapter, RequestPartitionId theRequestPartitionId, Set<String> foundIds, List<Long> idsToPreFetch) {
List<IIdType> idsToPreResolve = new ArrayList<>();
for (IBase nextEntry : theEntries) {
IBaseResource resource = versionAdapter.getResource(nextEntry);
IBaseResource resource = theVersionAdapter.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());
String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry);
if ("PUT".equals(verb) || "PATCH".equals(verb)) {
String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry);
if (countMatches(requestUrl, '/') == 1 && countMatches(requestUrl, '?') == 0) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setValue(requestUrl);
idsToPreResolve.add(id);
}
}
}
}
List<JpaPid> outcome = myIdHelperService.resolveResourcePersistentIdsWithCache(requestPartitionId, idsToPreResolve)
List<JpaPid> outcome = myIdHelperService.resolveResourcePersistentIdsWithCache(theRequestPartitionId, idsToPreResolve)
.stream().collect(Collectors.toList());
for (JpaPid next : outcome) {
foundIds.add(next.getAssociatedResourceId().toUnqualifiedVersionless().getValue());
@ -177,109 +199,200 @@ public class TransactionProcessor extends BaseTransactionProcessor {
theTransactionDetails.addResolvedResourceId(next.toUnqualifiedVersionless(), null);
}
}
}
/*
* Pre-resolve any conditional URLs we can
*/
private void preFetchConditionalUrls(TransactionDetails theTransactionDetails, List<IBase> theEntries, ITransactionProcessorVersionAdapter theVersionAdapter, RequestPartitionId theRequestPartitionId, List<Long> idsToPreFetch) {
List<MatchUrlToResolve> searchParameterMapsToResolve = new ArrayList<>();
for (IBase nextEntry : theEntries) {
IBaseResource resource = versionAdapter.getResource(nextEntry);
IBaseResource resource = theVersionAdapter.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("?")) {
JpaPid cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(resourceType, requestUrl);
if (cachedId != null) {
idsToPreFetch.add(cachedId.getId());
} 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));
String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry);
String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry);
String requestIfNoneExist = theVersionAdapter.getEntryIfNoneExist(nextEntry);
String resourceType = determineResourceTypeInResourceUrl(myFhirContext, requestUrl);
if (resourceType == null && resource != null) {
resourceType = myFhirContext.getResourceType(resource);
}
if (("PUT".equals(verb) || "PATCH".equals(verb)) && requestUrl != null && requestUrl.contains("?")) {
preFetchConditionalUrl(resourceType, requestUrl, true, idsToPreFetch, searchParameterMapsToResolve);
} else if ("POST".equals(verb) && requestIfNoneExist != null && requestIfNoneExist.contains("?")) {
JpaPid cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(resourceType, requestIfNoneExist);
if (cachedId != null) {
idsToPreFetch.add(cachedId.getId());
} 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));
preFetchConditionalUrl(resourceType, requestIfNoneExist, false, idsToPreFetch, searchParameterMapsToResolve);
}
if (myStorageSettings.isAllowInlineMatchUrlReferences()) {
List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(resource);
for (ResourceReferenceInfo next : references) {
String referenceUrl = next.getResourceReference().getReferenceElement().getValue();
String refResourceType = determineResourceTypeInResourceUrl(myFhirContext, referenceUrl);
if (refResourceType != null) {
preFetchConditionalUrl(refResourceType, referenceUrl, false, idsToPreFetch, searchParameterMapsToResolve);
}
}
}
}
}
new QueryChunker<MatchUrlToResolve>()
.chunk(searchParameterMapsToResolve, 100, map ->
preFetchSearchParameterMaps(theTransactionDetails, theRequestPartitionId, map, idsToPreFetch));
}
}
if (searchParameterMapsToResolve.size() > 0) {
CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
CriteriaQuery<ResourceIndexedSearchParamToken> cq = cb.createQuery(ResourceIndexedSearchParamToken.class);
Root<ResourceIndexedSearchParamToken> from = cq.from(ResourceIndexedSearchParamToken.class);
List<Predicate> orPredicates = new ArrayList<>();
for (MatchUrlToResolve next : searchParameterMapsToResolve) {
/**
* @param theTransactionDetails The active transaction details
* @param theRequestPartitionId The active partition
* @param theInputParameters These are the search parameter maps that will actually be resolved
* @param theOutputPidsToLoadFully This list will be added to with any resource PIDs that need to be fully
* pre-loaded (ie. fetch the actual resource body since we're presumably
* going to update it and will need to see its current state eventually)
*/
private void preFetchSearchParameterMaps(TransactionDetails theTransactionDetails, RequestPartitionId theRequestPartitionId, List<MatchUrlToResolve> theInputParameters, List<Long> theOutputPidsToLoadFully) {
Set<Long> systemAndValueHashes = new HashSet<>();
Set<Long> valueHashes = new HashSet<>();
for (MatchUrlToResolve next : theInputParameters) {
Collection<List<List<IQueryParameterType>>> values = next.myMatchUrlSearchMap.values();
if (values.size() == 1) {
List<List<IQueryParameterType>> andList = values.iterator().next();
IQueryParameterType param = andList.get(0).get(0);
if (param instanceof TokenParam) {
Predicate hashPredicate = buildHashPredicateFromTokenParam((TokenParam)param, requestPartitionId, cb, from, next);
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);
}
buildHashPredicateFromTokenParam((TokenParam) param, theRequestPartitionId, next, systemAndValueHashes, valueHashes);
}
}
}
if (orPredicates.size() > 1) {
cq.where(cb.or(orPredicates.toArray(EMPTY_PREDICATE_ARRAY)));
preFetchSearchParameterMapsToken("myHashSystemAndValue", systemAndValueHashes, theTransactionDetails, theRequestPartitionId, theInputParameters, theOutputPidsToLoadFully);
preFetchSearchParameterMapsToken("myHashValue", valueHashes, theTransactionDetails, theRequestPartitionId, theInputParameters, theOutputPidsToLoadFully);
Map<Long, List<MatchUrlToResolve>> hashToSearchMap = buildHashToSearchMap(searchParameterMapsToResolve);
TypedQuery<ResourceIndexedSearchParamToken> query = myEntityManager.createQuery(cq);
List<ResourceIndexedSearchParamToken> results = query.getResultList();
for (ResourceIndexedSearchParamToken nextResult : results) {
Optional<List<MatchUrlToResolve>> matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashSystemAndValue()));
if (!matchedSearch.isPresent()) {
matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashValue()));
}
matchedSearch.ifPresent(matchUrlsToResolve -> {
matchUrlsToResolve.forEach(matchUrl -> {
setSearchToResolvedAndPrefetchFoundResourcePid(theTransactionDetails, idsToPreFetch, nextResult, matchUrl);
});
});
}
//For each SP Map which did not return a result, tag it as not found.
searchParameterMapsToResolve.stream()
if (!valueHashes.isEmpty() || !systemAndValueHashes.isEmpty()) {
theInputParameters.stream()
// No matches
.filter(match -> !match.myResolved)
.forEach(match -> {
ourLog.debug("Was unable to match url {} from database", match.myRequestUrl);
theTransactionDetails.addResolvedMatchUrl(match.myRequestUrl, TransactionDetails.NOT_FOUND);
theTransactionDetails.addResolvedMatchUrl(myFhirContext, match.myRequestUrl, TransactionDetails.NOT_FOUND);
});
}
}
IFhirSystemDao<?,?> systemDao = myApplicationContext.getBean(IFhirSystemDao.class);
systemDao.preFetchResources(JpaPid.fromLongList(idsToPreFetch));
/**
* Here we do a select against the {@link ResourceIndexedSearchParamToken} table for any rows that have the
* specific sys+val or val hashes we know we need to pre-fetch.
* <p>
* Note that we do a tuple query for only 2 columns in order to ensure that we can get by with only
* the data in the index (ie no need to load the actual table rows).
*/
private void preFetchSearchParameterMapsToken(String theIndexColumnName, Set<Long> theHashesForIndexColumn, TransactionDetails theTransactionDetails, RequestPartitionId theRequestPartitionId, List<MatchUrlToResolve> theInputParameters, List<Long> theOutputPidsToLoadFully) {
if (!theHashesForIndexColumn.isEmpty()) {
ListMultimap<Long, MatchUrlToResolve> hashToSearchMap = buildHashToSearchMap(theInputParameters, theIndexColumnName);
CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<ResourceIndexedSearchParamToken> from = cq.from(ResourceIndexedSearchParamToken.class);
cq.multiselect(from.get("myResourcePid").as(Long.class), from.get(theIndexColumnName).as(Long.class));
Predicate masterPredicate;
if (theHashesForIndexColumn.size() == 1) {
masterPredicate = cb.equal(from.get(theIndexColumnName).as(Long.class), theHashesForIndexColumn.iterator().next());
} else {
masterPredicate = from.get(theIndexColumnName).as(Long.class).in(theHashesForIndexColumn);
}
return super.doTransactionWriteOperations(theRequest, theActionName, theTransactionDetails, theAllIds, theIdSubstitutions, theIdToPersistedOutcome, theResponse, theOriginalRequestOrder, theEntries, theTransactionStopWatch);
if (myPartitionSettings.isPartitioningEnabled() && !myPartitionSettings.isIncludePartitionInSearchHashes()) {
if (theRequestPartitionId.isDefaultPartition()) {
Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue").as(Integer.class));
masterPredicate = cb.and(partitionIdCriteria, masterPredicate);
} else if (!theRequestPartitionId.isAllPartitions()) {
Predicate partitionIdCriteria = from.get("myPartitionIdValue").as(Integer.class).in(theRequestPartitionId.getPartitionIds());
masterPredicate = cb.and(partitionIdCriteria, masterPredicate);
}
}
cq.where(masterPredicate);
TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
/*
* If we have 10 unique conditional URLs we're resolving, each one should
* resolve to 0..1 resources if they are valid as conditional URLs. So we would
* expect this query to return 0..10 rows, since conditional URLs for all
* conditional operations except DELETE (which isn't being applied here) are
* only allowed to resolve to 0..1 resources.
*
* If a conditional URL matches 2+ resources that is an error, and we'll
* be throwing an exception below. This limit is here for safety just to
* ensure that if someone uses a conditional URL that matches a million resources,
* we don't do a super-expensive fetch.
*/
query.setMaxResults(theHashesForIndexColumn.size() + 1);
List<Tuple> results = query.getResultList();
for (Tuple nextResult : results) {
Long nextResourcePid = nextResult.get(0, Long.class);
Long nextHash = nextResult.get(1, Long.class);
List<MatchUrlToResolve> matchedSearch = hashToSearchMap.get(nextHash);
matchedSearch.forEach(matchUrl -> {
ourLog.debug("Matched url {} from database", matchUrl.myRequestUrl);
if (matchUrl.myShouldPreFetchResourceBody) {
theOutputPidsToLoadFully.add(nextResourcePid);
}
myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, matchUrl.myResourceDefinition.getName(), matchUrl.myRequestUrl, JpaPid.fromId(nextResourcePid));
theTransactionDetails.addResolvedMatchUrl(myFhirContext, matchUrl.myRequestUrl, JpaPid.fromId(nextResourcePid));
matchUrl.setResolved(true);
});
}
}
}
/**
* Note that if {@literal theShouldPreFetchResourceBody} is false, then we'll check if a given match
* URL resolves to a resource PID, but we won't actually try to load that resource. If we're resolving
* a match URL because it's there for a conditional update, we'll eagerly fetch the
* actual resource because we need to know its current state in order to update it. However, if
* the match URL is from an inline match URL in a resource body, we really only care about
* the PID and don't need the body so we don't load it. This does have a security implication, since
* it means that the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES} pointcut
* isn't fired even though the user has resolved the URL (meaning they may be able to test for
* the existence of a resource using a match URL). There is a test for this called
* {@literal testTransactionCreateInlineMatchUrlWithAuthorizationDenied()}. This security tradeoff
* is acceptable since we're only prefetching things with very simple match URLs (nothing with
* a reference in it for example) so it's not really possible to doing anything useful with this.
*
* @param theResourceType The resource type associated with the match URL (ie what resource type should it resolve to)
* @param theRequestUrl The actual match URL, which could be as simple as just parameters or could include the resource type too
* @param theShouldPreFetchResourceBody Should we also fetch the actual resource body, or just figure out the PID associated with it. See the method javadoc above for some context.
* @param theOutputIdsToPreFetch This will be populated with any resource PIDs that need to be pre-fetched
* @param theOutputSearchParameterMapsToResolve This will be populated with any {@link SearchParameterMap} instances corresponding to match URLs we need to resolve
*/
private void preFetchConditionalUrl(String theResourceType, String theRequestUrl, boolean theShouldPreFetchResourceBody, List<Long> theOutputIdsToPreFetch, List<MatchUrlToResolve> theOutputSearchParameterMapsToResolve) {
JpaPid cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(theResourceType, theRequestUrl);
if (cachedId != null) {
if (theShouldPreFetchResourceBody) {
theOutputIdsToPreFetch.add(cachedId.getId());
}
} else if (SINGLE_PARAMETER_MATCH_URL_PATTERN.matcher(theRequestUrl).matches()) {
RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
SearchParameterMap matchUrlSearchMap = myMatchUrlService.translateMatchUrl(theRequestUrl, resourceDefinition);
theOutputSearchParameterMapsToResolve.add(new MatchUrlToResolve(theRequestUrl, matchUrlSearchMap, resourceDefinition, theShouldPreFetchResourceBody));
}
}
private RequestPartitionId getSinglePartitionForAllEntriesOrNull(RequestDetails theRequest, List<IBase> theEntries, ITransactionProcessorVersionAdapter versionAdapter) {
RequestPartitionId retVal = null;
Set<RequestPartitionId> requestPartitionIdsForAllEntries = new HashSet<>();
for (IBase nextEntry : theEntries) {
IBaseResource resource = versionAdapter.getResource(nextEntry);
if (resource != null) {
RequestPartitionId requestPartition = myRequestPartitionSvc.determineCreatePartitionForRequest(theRequest, resource, myFhirContext.getResourceType(resource));
requestPartitionIdsForAllEntries.add(requestPartition);
}
}
if (requestPartitionIdsForAllEntries.size() == 1) {
retVal = requestPartitionIdsForAllEntries.iterator().next();
}
return retVal;
}
/**
@ -287,44 +400,31 @@ public class TransactionProcessor extends BaseTransactionProcessor {
* If neither are available, it returns null.
*/
@Nullable
private Predicate buildHashPredicateFromTokenParam(TokenParam theTokenParam, RequestPartitionId theRequestPartitionId, CriteriaBuilder cb, Root<ResourceIndexedSearchParamToken> from, MatchUrlToResolve theMatchUrl) {
Predicate hashPredicate = null;
private void buildHashPredicateFromTokenParam(TokenParam theTokenParam, RequestPartitionId theRequestPartitionId, MatchUrlToResolve theMatchUrl, Set<Long> theSysAndValuePredicates, Set<Long> theValuePredicates) {
if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) {
theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue(myPartitionSettings, theRequestPartitionId, theMatchUrl.myResourceDefinition.getName(), theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), theTokenParam.getSystem(), theTokenParam.getValue());
hashPredicate = cb.equal(from.get("myHashSystemAndValue").as(Long.class), theMatchUrl.myHashSystemAndValue);
theSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue);
} else if (isNotBlank(theTokenParam.getValue())) {
theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue(myPartitionSettings, theRequestPartitionId, theMatchUrl.myResourceDefinition.getName(), theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), theTokenParam.getValue());
hashPredicate = cb.equal(from.get("myHashValue").as(Long.class), theMatchUrl.myHashValue);
}
return hashPredicate;
theValuePredicates.add(theMatchUrl.myHashValue);
}
private Map<Long, List<MatchUrlToResolve>> buildHashToSearchMap(List<MatchUrlToResolve> searchParameterMapsToResolve) {
Map<Long, List<MatchUrlToResolve>> hashToSearch = new HashMap<>();
}
private ListMultimap<Long, MatchUrlToResolve> buildHashToSearchMap(List<MatchUrlToResolve> searchParameterMapsToResolve, String theIndex) {
ListMultimap<Long, MatchUrlToResolve> hashToSearch = ArrayListMultimap.create();
//Build a lookup map so we don't have to iterate over the searches repeatedly.
for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) {
if (nextSearchParameterMap.myHashSystemAndValue != null) {
List<MatchUrlToResolve> matchUrlsToResolve = hashToSearch.getOrDefault(nextSearchParameterMap.myHashSystemAndValue, new ArrayList<>());
matchUrlsToResolve.add(nextSearchParameterMap);
hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, matchUrlsToResolve);
if (nextSearchParameterMap.myHashSystemAndValue != null && theIndex.equals("myHashSystemAndValue")) {
hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap);
}
if (nextSearchParameterMap.myHashValue!= null) {
List<MatchUrlToResolve> matchUrlsToResolve = hashToSearch.getOrDefault(nextSearchParameterMap.myHashValue, new ArrayList<>());
matchUrlsToResolve.add(nextSearchParameterMap);
hashToSearch.put(nextSearchParameterMap.myHashValue, matchUrlsToResolve);
if (nextSearchParameterMap.myHashValue != null && theIndex.equals("myHashValue")) {
hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap);
}
}
return hashToSearch;
}
private void setSearchToResolvedAndPrefetchFoundResourcePid(TransactionDetails theTransactionDetails, List<Long> idsToPreFetch, ResourceIndexedSearchParamToken nextResult, MatchUrlToResolve nextSearchParameterMap) {
ourLog.debug("Matched url {} from database", nextSearchParameterMap.myRequestUrl);
idsToPreFetch.add(nextResult.getResourcePid());
myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, nextSearchParameterMap.myResourceDefinition.getName(), nextSearchParameterMap.myRequestUrl, JpaPid.fromId(nextResult.getResourcePid()));
theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, JpaPid.fromId(nextResult.getResourcePid()));
nextSearchParameterMap.setResolved(true);
}
@Override
protected void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) {
try {
@ -372,15 +472,18 @@ public class TransactionProcessor extends BaseTransactionProcessor {
private final String myRequestUrl;
private final SearchParameterMap myMatchUrlSearchMap;
private final RuntimeResourceDefinition myResourceDefinition;
private final boolean myShouldPreFetchResourceBody;
public boolean myResolved;
private Long myHashValue;
private Long myHashSystemAndValue;
public MatchUrlToResolve(String theRequestUrl, SearchParameterMap theMatchUrlSearchMap, RuntimeResourceDefinition theResourceDefinition) {
public MatchUrlToResolve(String theRequestUrl, SearchParameterMap theMatchUrlSearchMap, RuntimeResourceDefinition theResourceDefinition, boolean theShouldPreFetchResourceBody) {
myRequestUrl = theRequestUrl;
myMatchUrlSearchMap = theMatchUrlSearchMap;
myResourceDefinition = theResourceDefinition;
myShouldPreFetchResourceBody = theShouldPreFetchResourceBody;
}
public void setResolved(boolean theResolved) {
myResolved = theResolved;
}

View File

@ -60,6 +60,7 @@ import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
@ -345,9 +346,22 @@ public class IdHelperService implements IIdHelperService<JpaPid> {
private void doResolvePersistentIds(RequestPartitionId theRequestPartitionId, List<IIdType> theIds, List<JpaPid> theOutputListToPopulate) {
CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
CriteriaQuery<ForcedId> criteriaQuery = cb.createQuery(ForcedId.class);
CriteriaQuery<Tuple> criteriaQuery = cb.createTupleQuery();
Root<ForcedId> from = criteriaQuery.from(ForcedId.class);
/*
* We don't currently have an index that satisfies these three columns, but the
* index IDX_FORCEDID_TYPE_FID does include myResourceType and myForcedId
* so we're at least minimizing the amount of data we fetch. A largescale test
* on Postgres does confirm that this lookup does use the index and is pretty
* performant.
*/
criteriaQuery.multiselect(
from.get("myResourcePid").as(Long.class),
from.get("myResourceType").as(String.class),
from.get("myForcedId").as(String.class)
);
List<Predicate> predicates = new ArrayList<>(theIds.size());
for (IIdType next : theIds) {
@ -366,16 +380,19 @@ public class IdHelperService implements IIdHelperService<JpaPid> {
criteriaQuery.where(cb.or(predicates.toArray(EMPTY_PREDICATE_ARRAY)));
TypedQuery<ForcedId> query = myEntityManager.createQuery(criteriaQuery);
List<ForcedId> results = query.getResultList();
for (ForcedId nextId : results) {
TypedQuery<Tuple> query = myEntityManager.createQuery(criteriaQuery);
List<Tuple> results = query.getResultList();
for (Tuple nextId : results) {
// Check if the nextId has a resource ID. It may have a null resource ID if a commit is still pending.
if (nextId.getResourceId() != null) {
JpaPid jpaPid = JpaPid.fromId(nextId.getResourceId());
populateAssociatedResourceId(nextId.getResourceType(), nextId.getForcedId(), jpaPid);
Long resourceId = nextId.get(0, Long.class);
String resourceType = nextId.get(1, String.class);
String forcedId = nextId.get(2, String.class);
if (resourceId != null) {
JpaPid jpaPid = JpaPid.fromId(resourceId);
populateAssociatedResourceId(resourceType, forcedId, jpaPid);
theOutputListToPopulate.add(jpaPid);
String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getForcedId());
String key = toForcedIdToPidKey(theRequestPartitionId, resourceType, forcedId);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, jpaPid);
}
}

View File

@ -37,6 +37,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.util.VersionEnum;
@ -200,6 +201,12 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.nullable()
.type(ColumnTypeEnum.DATE_ONLY);
}
version
.onTable(ResourceTable.HFJ_RESOURCE)
.addColumn("20230323.1", "SEARCH_URL_PRESENT")
.nullable()
.type(ColumnTypeEnum.BOOLEAN);
}
protected void init640() {

View File

@ -298,6 +298,15 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@OptimisticLock(excluded = true)
private String myFhirId;
/**
* Is there a corresponding row in {@link ResourceSearchUrlEntity} for
* this row.
* TODO: Added in 6.6.0 - Should make this a primitive boolean at some point
*/
@OptimisticLock(excluded = true)
@Column(name = "SEARCH_URL_PRESENT", nullable = true)
private Boolean mySearchUrlPresent = false;
/**
* Populate myFhirId with server-assigned sequence id when no client-id provided.
* We eat this complexity during insert to simplify query time with a uniform column.
@ -694,6 +703,14 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
myNarrativeText = theNarrativeText;
}
public boolean isSearchUrlPresent() {
return Boolean.TRUE.equals(mySearchUrlPresent);
}
public void setSearchUrlPresent(boolean theSearchUrlPresent) {
mySearchUrlPresent = theSearchUrlPresent;
}
public ResourceHistoryTable toHistory(boolean theCreateVersionTags) {
ResourceHistoryTable retVal = new ResourceHistoryTable();

View File

@ -381,7 +381,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), containsString("with match URL \"Patient"));
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}
@ -1350,7 +1350,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), containsString("with match URL \"Patient"));
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}

View File

@ -3566,7 +3566,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
assertEquals(10, myCaptureQueriesListener.countSelectQueries());
assertEquals(5, myCaptureQueriesListener.countUpdateQueries());
assertEquals(1, myCaptureQueriesListener.countInsertQueries());
assertEquals(1, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
String unformattedSql = myCaptureQueriesListener.getUpdateQueriesForCurrentThread().get(0).getSql(true, false);
assertThat(unformattedSql, stringContainsInOrder(
"SRC_PATH='Observation.performer'",

View File

@ -829,7 +829,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertEquals(Msg.code(1092) + "Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithTwoMatches\" - Multiple resources match this search", e.getMessage());
assertEquals(Msg.code(2207) + "Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithTwoMatches\" - Multiple resources match this search", e.getMessage());
}
}
@ -905,7 +905,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), containsString("with match URL \"Patient"));
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}
@ -2052,7 +2052,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), containsString("with match URL \"Patient"));
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}
@ -2451,7 +2451,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
mySystemDao.transaction(mySrd, bundle);
fail();
} catch (PreconditionFailedException e) {
assertEquals(Msg.code(1092) + "Invalid match URL \"Patient?identifier=http://www.ghh.org/identifiers|condreftestpatid1\" - Multiple resources match this search", e.getMessage());
assertEquals(Msg.code(2207) + "Invalid match URL \"Patient?identifier=http://www.ghh.org/identifiers|condreftestpatid1\" - Multiple resources match this search", e.getMessage());
}
}

View File

@ -66,7 +66,7 @@ public class ConsumeFilesStepR4Test extends BaseJpaR4Test {
assertEquals(4, myCaptureQueriesListener.logSelectQueries().size());
assertEquals(0, myCaptureQueriesListener.countInsertQueries());
assertEquals(0, myCaptureQueriesListener.countUpdateQueries());
assertEquals(2, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
@ -115,7 +115,7 @@ public class ConsumeFilesStepR4Test extends BaseJpaR4Test {
assertEquals(4, myCaptureQueriesListener.logSelectQueries().size());
assertEquals(2, myCaptureQueriesListener.logInsertQueries());
assertEquals(4, myCaptureQueriesListener.logUpdateQueries());
assertEquals(2, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.IDaoRegistry;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -26,6 +27,8 @@ import org.hl7.fhir.r4.model.Meta;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Answers;
import org.mockito.Spy;
import org.slf4j.Logger;
@ -45,6 +48,7 @@ import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@ -59,6 +63,8 @@ public class TransactionProcessorTest {
@Autowired
private TransactionProcessor myTransactionProcessor;
@MockBean
private DaoRegistry myDaoRegistry;
@MockBean
private EntityManagerFactory myEntityManagerFactory;
@MockBean(answer = Answers.RETURNS_DEEP_STUBS)
private EntityManager myEntityManager;
@ -115,6 +121,7 @@ public class TransactionProcessorTest {
}
}
@Configuration
@Import(ThreadPoolFactoryConfig.class)
public static class MyConfig {

View File

@ -28,6 +28,7 @@ import javax.servlet.ServletException;
import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@ -207,15 +208,20 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
public RequestPartitionId partitionIdentifyRead(ServletRequestDetails theRequestDetails) {
// Just to be nice, figure out the first line in the stack that isn't a part of the
// partitioning or interceptor infrastructure, just so it's obvious who is asking
// for a partition ID
String stack;
try {
throw new Exception();
} catch (Exception e) {
stack = StackTraceHelper.getStackAsString(e);
int lastWantedNewLine = StringUtils.ordinalIndexOf(stack, "\n", 25);
if (lastWantedNewLine != -1) {
stack = stack.substring(0, lastWantedNewLine);
}
stack = Arrays.stream(stack.split("\\n"))
.filter(t->t.contains("ca.uhn.fhir"))
.filter(t->!t.toLowerCase().contains("interceptor"))
.filter(t->!t.toLowerCase().contains("partitionhelper"))
.findFirst()
.orElse("UNKNOWN");
}
RequestPartitionId retVal = myReadRequestPartitionIds.remove(0);

View File

@ -218,6 +218,7 @@ public class FhirResourceDaoR4ConcurrentCreateTest extends BaseJpaR4Test {
super(theName);
}
@Override
public void invoke(IPointcut thePointcut, HookParams theArgs) {
doInvoke(thePointcut, theArgs);
}

View File

@ -130,37 +130,14 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
* Make a transaction with conditional updates that will fail due to
* constraint errors and be retried automatically. Make sure that the
* retry succeeds and that the data ultimately gets written.
*
* This test used to use a composite unique search parameter, but
* can now rely on the {@link ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity}
* instead.
*/
@Test
public void testTransactionCreates_WithRetry() throws ExecutionException, InterruptedException {
myInterceptorRegistry.registerInterceptor(myRetryInterceptor);
myStorageSettings.setUniqueIndexesEnabled(true);
// Create a unique search parameter to enfore uniqueness
// TODO: remove this once we have a better way to enfore these
SearchParameter sp = new SearchParameter();
sp.setId("SearchParameter/Practitioner-identifier");
sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setCode("identifier");
sp.setExpression("Practitioner.identifier");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.addBase("Practitioner");
mySearchParameterDao.update(sp);
sp = new SearchParameter();
sp.setId("SearchParameter/Practitioner-identifier-unique");
sp.setType(Enumerations.SearchParamType.COMPOSITE);
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.addBase("Practitioner");
sp.addComponent()
.setExpression("Practitioner")
.setDefinition("SearchParameter/Practitioner-identifier");
sp.addExtension()
.setUrl(HapiExtensions.EXT_SP_UNIQUE)
.setValue(new BooleanType(true));
mySearchParameterDao.update(sp);
mySearchParamRegistry.forceRefresh();
AtomicInteger setCounter = new AtomicInteger(0);
AtomicInteger fuzzCounter = new AtomicInteger(0);
@ -191,9 +168,9 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
assertEquals(1, counts.get("Patient"), counts.toString());
assertEquals(1, counts.get("Observation"), counts.toString());
assertEquals(7, myResourceLinkDao.count()); // 1 for SP, 6 for transaction
assertEquals(8, myResourceTableDao.count()); // 2 SPs, 6 resources
assertEquals(16, myResourceHistoryTableDao.count());
assertEquals(6, myResourceLinkDao.count());
assertEquals(6, myResourceTableDao.count());
assertEquals(14, myResourceHistoryTableDao.count());
});
}

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
@ -44,6 +45,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.Provenance;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ServiceRequest;
@ -68,6 +70,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
@ -133,7 +136,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
assertThat(myCaptureQueriesListener.getInsertQueriesForCurrentThread(), empty());
assertEquals(1, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
@ -159,7 +162,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
myCaptureQueriesListener.logDeleteQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
@Test
@ -178,7 +181,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.clear();
group = updateGroup(group, patientList.subList(initialPatientsCount, allPatientsCount));
assertQueryCount(10, 1, 2, 1);
assertQueryCount(10, 1, 2, 0);
assertEquals(allPatientsCount, group.getMember().size());
@ -200,7 +203,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
group = updateGroup(group, Collections.emptyList());
myCaptureQueriesListener.logSelectQueries();
assertQueryCount(5, 1, 2, 1);
assertQueryCount(5, 1, 2, 0);
assertEquals(30, group.getMember().size());
@ -237,7 +240,69 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
myCaptureQueriesListener.logDeleteQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
@Test
public void testUpdate_DeletesSearchUrlOnlyWhenPresent() {
Patient p = new Patient();
p.setActive(false);
p.addIdentifier().setSystem("http://foo").setValue("123");
myCaptureQueriesListener.clear();
IIdType id = myPatientDao.create(p, "Patient?identifier=http://foo|123", mySrd).getId();
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(1L, id.getVersionIdPartAsLong());
// Update 1 - Should delete search URL
p.setActive(true);
myCaptureQueriesListener.clear();
id = myPatientDao.update(p, "Patient?identifier=http://foo|123", mySrd).getId();
assertEquals(1, myCaptureQueriesListener.countDeleteQueries());
assertEquals(2L, id.getVersionIdPartAsLong());
// Update 2 - Should not try to delete search URL
p.setActive(false);
myCaptureQueriesListener.clear();
id = myPatientDao.update(p, "Patient?identifier=http://foo|123", mySrd).getId();
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(3L, id.getVersionIdPartAsLong());
}
@Test
public void testUpdate_DeletesSearchUrlOnlyWhenPresent_NonConditional() {
Patient p = new Patient();
p.setActive(false);
p.addIdentifier().setSystem("http://foo").setValue("123");
myCaptureQueriesListener.clear();
IIdType id = myPatientDao.create(p, mySrd).getId();
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(1L, id.getVersionIdPartAsLong());
// Update 1 - Should not try to delete search URL since none should exist
p.setActive(true);
myCaptureQueriesListener.clear();
id = myPatientDao.update(p, "Patient?identifier=http://foo|123", mySrd).getId();
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(2L, id.getVersionIdPartAsLong());
// Update 2 - Should not try to delete search URL
p.setActive(false);
myCaptureQueriesListener.clear();
id = myPatientDao.update(p, "Patient?identifier=http://foo|123", mySrd).getId();
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(3L, id.getVersionIdPartAsLong());
}
@ -543,7 +608,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
myCaptureQueriesListener.logDeleteQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
// Third time (caches all loaded by now)
@ -661,9 +726,9 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
public void assertNoPartitionSelectors() {
List<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueriesForCurrentThread();
for (SqlQuery next : selectQueries) {
assertEquals(0, StringUtils.countMatches(next.getSql(true, true).toLowerCase(), "partition_id is null"), () -> next.getSql(true, true));
assertEquals(0, StringUtils.countMatches(next.getSql(true, true).toLowerCase(), "partition_id="), () -> next.getSql(true, true));
assertEquals(0, StringUtils.countMatches(next.getSql(true, true).toLowerCase(), "partition_id ="), () -> next.getSql(true, true));
assertEquals(0, countMatches(next.getSql(true, true).toLowerCase(), "partition_id is null"), () -> next.getSql(true, true));
assertEquals(0, countMatches(next.getSql(true, true).toLowerCase(), "partition_id="), () -> next.getSql(true, true));
assertEquals(0, countMatches(next.getSql(true, true).toLowerCase(), "partition_id ="), () -> next.getSql(true, true));
}
}
@ -1058,7 +1123,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true).toLowerCase();
assertEquals(1, StringUtils.countMatches(sql, "join"), sql);
assertEquals(1, countMatches(sql, "join"), sql);
}
@ -1231,7 +1296,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.clear();
mySystemDao.transaction(mySrd, createTransactionWithCreatesAndOneMatchUrl());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
// 1 lookup for the match URL only
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
assertEquals(19, myCaptureQueriesListener.countInsertQueries());
@ -1426,7 +1490,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(2, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
assertEquals(4, myCaptureQueriesListener.countUpdateQueries());
assertEquals(3, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
/*
* Third time with mass ingestion mode enabled
@ -1442,7 +1506,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(2, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
assertEquals(4, myCaptureQueriesListener.countUpdateQueries());
assertEquals(1, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
}
@ -1509,7 +1573,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(7, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
assertEquals(4, myCaptureQueriesListener.countUpdateQueries());
assertEquals(3, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
/*
* Third time with mass ingestion mode enabled
@ -1525,7 +1589,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
assertEquals(4, myCaptureQueriesListener.countUpdateQueries());
assertEquals(1, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
}
@ -1553,6 +1617,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
mySystemDao.transaction(mySrd, input);
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, countMatches(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false), "'6445233466262474106'"));
assertEquals(1, countMatches(myCaptureQueriesListener.getSelectQueries().get(1).getSql(true, false), "'LOC'"));
assertEquals(6, runInTransaction(() -> myResourceTableDao.count()));
// Second identical pass
@ -1600,7 +1666,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myCaptureQueriesListener.clear();
mySystemDao.transaction(mySrd, input);
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(6, runInTransaction(() -> myResourceTableDao.count()));
// Second identical pass
@ -1783,7 +1849,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
Bundle outcome = mySystemDao.transaction(mySrd, input.get());
ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
myCaptureQueriesListener.logSelectQueries();
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
// One to prefetch sys+val, one to prefetch val
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
myCaptureQueriesListener.logInsertQueries();
assertEquals(45, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
@ -1798,12 +1865,12 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
outcome = mySystemDao.transaction(mySrd, input.get());
ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
myCaptureQueriesListener.logSelectQueries();
assertEquals(8, myCaptureQueriesListener.countSelectQueries());
assertEquals(9, myCaptureQueriesListener.countSelectQueries());
myCaptureQueriesListener.logInsertQueries();
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
assertEquals(8, myCaptureQueriesListener.countUpdateQueries());
assertEquals(4, myCaptureQueriesListener.countDeleteQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
/*
* Third time with mass ingestion mode enabled
@ -1815,7 +1882,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
outcome = mySystemDao.transaction(mySrd, input.get());
ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
myCaptureQueriesListener.logSelectQueries();
assertEquals(7, myCaptureQueriesListener.countSelectQueries());
assertEquals(8, myCaptureQueriesListener.countSelectQueries());
myCaptureQueriesListener.logInsertQueries();
assertEquals(4, myCaptureQueriesListener.countInsertQueries());
myCaptureQueriesListener.logUpdateQueries();
@ -1946,7 +2013,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
// Make sure the match URL query uses a small limit
String matchUrlQuery = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, true);
assertThat(matchUrlQuery, containsString("t0.HASH_SYS_AND_VALUE = '-4132452001562191669'"));
assertThat(matchUrlQuery, containsString("HASH_SYS_AND_VALUE='-4132452001562191669'"));
assertThat(matchUrlQuery, containsString("limit '2'"));
runInTransaction(() -> {
@ -2041,7 +2108,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@ -2541,7 +2608,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
// Lookup the two existing IDs to make sure they are legit
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(10, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
@ -2598,9 +2665,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
output = mySystemDao.transaction(mySrd, input);
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
// Lookup the two existing IDs to make sure they are legit
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(10, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
@ -2663,6 +2729,60 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
}
/**
* This test runs a transaction bundle that has a large number of inline match URLs,
* as well as a large number of updates (PUT). This means that a lot of URLs and resources
* need to be resolved (ie SQL SELECT) in order to proceed with the transaction. Prior
* to the optimization that introduced this test, we had 140 SELECTs, now it's 17.
*/
@Test
public void testTransactionWithManyInlineMatchUrls() throws IOException {
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true);
Bundle input = loadResource(myFhirContext, Bundle.class, "/r4/test-patient-bundle.json");
myCaptureQueriesListener.clear();
Bundle output = mySystemDao.transaction(mySrd, input);
myCaptureQueriesListener.logSelectQueries();
assertEquals(17, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(6607, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(418, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(input.getEntry().size(), output.getEntry().size());
runInTransaction(()->{
assertEquals(437, myResourceTableDao.count());
assertEquals(437, myResourceHistoryTableDao.count());
});
}
@Test
public void testTransactionWithClientAssignedId() {
BundleBuilder bb = new BundleBuilder(myFhirContext);
for (int i = 0; i < 5; i++) {
Provenance prov = new Provenance();
prov.setId(IdType.newRandomUuid());
prov.setOccurred(new DateTimeType("2022"));
bb.addTransactionUpdateEntry(prov).conditional("Provenance/Patient-0d3b0c98-048e-4111-b804-d1c6c7816d5e-" + i);
}
Bundle input = bb.getBundleTyped();
// input.getEntry().get(0).
myCaptureQueriesListener.clear();
mySystemDao.transaction(mySrd, input);
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
}
@Test
public void testValueSetExpand_NotPreExpanded_UseHibernateSearch() {
createLocalCsAndVs();
@ -2845,7 +2965,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(7, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@ -2888,7 +3008,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(6, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(7, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}

View File

@ -1146,7 +1146,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@Test
@ -1216,7 +1216,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
@ -1278,7 +1278,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
pt.getNameFirstRep().addGiven("GIVEN1C");
myPatientDao.update(pt);
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
}
@ -1307,7 +1307,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
pt.addName().setFamily("FAMILY2");
myPatientDao.update(pt);
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
assertEquals(4, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
@ -1355,7 +1355,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
*/
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
@ -1413,7 +1413,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
pt.getManagingOrganization().setReference(orgId2.getValue());
myPatientDao.update(pt);
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE

View File

@ -39,7 +39,9 @@ import ca.uhn.fhir.util.BundleBuilder;
import ca.uhn.fhir.util.ClasspathUtil;
import org.apache.commons.io.IOUtils;
import org.hamcrest.Matchers;
import org.hibernate.envers.query.AuditEntity;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.AllergyIntolerance;
import org.hl7.fhir.r4.model.Appointment;
@ -72,6 +74,7 @@ import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
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.Provenance;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
@ -143,6 +146,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(defaults.isAutoCreatePlaceholderReferenceTargets());
myStorageSettings.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(defaults.isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets());
myStorageSettings.setAutoVersionReferenceAtPaths(defaults.getAutoVersionReferenceAtPaths());
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(defaults.isAutoCreatePlaceholderReferenceTargets());
myFhirContext.getParserOptions().setAutoContainReferenceTargetsWithNoId(true);
}
@ -1488,24 +1492,48 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
}
}
/**
* This test is testing whether someone can sneakily figure out the existence of a resource
* by creating a match URL that references it, even though the user doesn't have permission
* to see that resource.
* <p>
* This security check requires a match URL that is too complex for the pre-fetching that
* happens in {@link ca.uhn.fhir.jpa.dao.TransactionProcessor}'s preFetchConditionalUrl
* method (see the javadoc on that method for more details).
*/
@Test
public void testTransactionCreateInlineMatchUrlWithAuthorizationDenied() {
// setup
String methodName = "testTransactionCreateInlineMatchUrlWithAuthorizationDenied";
Bundle request = new Bundle();
myStorageSettings.setAllowInlineMatchUrlReferences(true);
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.setId("Patient/" + methodName);
IIdType id = myPatientDao.update(p, mySrd).getId();
ourLog.info("Created patient, got it: {}", id);
// Let's create a sensitive observation - Nobody must know we caught COVID!
Patient patient = new Patient();
patient.setId("J");
patient.addName().setFamily("Corona").addGiven("John");
myPatientDao.update(patient, mySrd);
Observation o = new Observation();
o.getCode().setText("Some Observation");
o.getSubject().setReference("Patient?identifier=urn%3Asystem%7C" + methodName);
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST);
Observation obs = new Observation();
obs.setId("Observation/O");
obs.setStatus(ObservationStatus.FINAL);
obs.setSubject(new Reference("Patient/J"));
obs.getCode().addCoding()
.setSystem("http://loinc.org")
.setCode("94505-5")
.setDisplay("SARS-CoV-2 (COVID-19) IgG Ab [Units/volume] in Serum or Plasma by Immunoassay");
obs.setValue(new Quantity()
.setValue(284L)
.setCode("[arb'U]/ml")
.setSystem("http://unitsofmeasure.org"));
myObservationDao.update(obs, mySrd);
// Create a bundle that tries to sneakily link to the
// patient's covid test
Bundle request = new Bundle();
Observation sneakyObs = new Observation();
sneakyObs.setSubject(new Reference("Patient/J"));
sneakyObs.addHasMember(new Reference("Observation?patient=Patient/J&code=http://loinc.org|94505-5"));
request.addEntry().setResource(sneakyObs).getRequest().setMethod(HTTPVerb.POST);
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.TRANSACTION);
@ -1528,7 +1556,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
// verify
fail();
} catch (ResourceNotFoundException e) {
assertEquals(Msg.code(1091) + "Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithAuthorizationDenied\" - No resources match this search", e.getMessage());
assertEquals(Msg.code(1091) + "Invalid match URL \"Observation?patient=Patient/J&code=http://loinc.org|94505-5\" - No resources match this search", e.getMessage());
} finally {
myInterceptorRegistry.unregisterInterceptor(interceptor);
}
@ -1904,7 +1932,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertEquals(Msg.code(1092) + "Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithTwoMatches\" - Multiple resources match this search", e.getMessage());
assertEquals(Msg.code(2207) + "Invalid match URL \"Patient?identifier=urn%3Asystem%7CtestTransactionCreateInlineMatchUrlWithTwoMatches\" - Multiple resources match this search", e.getMessage());
}
}
@ -1980,7 +2008,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), containsString("with match URL \"Patient"));
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}
@ -3534,7 +3562,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
mySystemDao.transaction(mySrd, request);
fail();
} catch (PreconditionFailedException e) {
assertThat(e.getMessage(), containsString("with match URL \"Patient"));
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}
@ -4080,7 +4108,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
mySystemDao.transaction(mySrd, bundle);
fail();
} catch (PreconditionFailedException e) {
assertEquals(Msg.code(1092) + "Invalid match URL \"Patient?identifier=http://www.ghh.org/identifiers|condreftestpatid1\" - Multiple resources match this search", e.getMessage());
assertEquals(Msg.code(2207) + "Invalid match URL \"Patient?identifier=http://www.ghh.org/identifiers|condreftestpatid1\" - Multiple resources match this search", e.getMessage());
}
}

View File

@ -68,6 +68,7 @@ import org.mockito.ArgumentCaptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
@ -118,7 +119,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@AfterEach
public void afterEach() {
myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(new JpaStorageSettings().isMarkResourcesForReindexingUponSearchParameterChange());
JpaStorageSettings defaults = new JpaStorageSettings();
myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(defaults.isMarkResourcesForReindexingUponSearchParameterChange());
myStorageSettings.setMatchUrlCacheEnabled(defaults.isMatchUrlCacheEnabled());
}
@Test
@ -2763,8 +2766,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
Bundle outcome = mySystemDao.transaction(mySrd, input.get());
ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false), containsString("resourcein0_.HASH_SYS_AND_VALUE='-4132452001562191669' and (resourcein0_.PARTITION_ID in ('1'))"));
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false), containsString("esourcein0_.PARTITION_ID in ('1')"));
assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false), containsString("HASH_SYS_AND_VALUE in ('7432183691485874662' , '-3772330830566471409'"));
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(45, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
@ -2779,12 +2783,12 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
outcome = mySystemDao.transaction(mySrd, input.get());
ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(9, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
assertEquals(8, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(4, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
/*
* Third time with mass ingestion mode enabled
@ -2796,7 +2800,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
outcome = mySystemDao.transaction(mySrd, input.get());
ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(7, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
@ -2819,6 +2823,64 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@Test
public void testTransactionWithManyInlineMatchUrls() throws IOException {
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true);
myStorageSettings.setMatchUrlCacheEnabled(true);
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setRequestPartitionId(RequestPartitionId.fromPartitionId(myPartitionId));
Bundle input = loadResource(myFhirContext, Bundle.class, "/r4/test-patient-bundle.json");
myCaptureQueriesListener.clear();
Bundle output = mySystemDao.transaction(requestDetails, input);
myCaptureQueriesListener.logSelectQueries();
assertEquals(18, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(6607, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(418, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(input.getEntry().size(), output.getEntry().size());
runInTransaction(()->{
assertEquals(437, myResourceTableDao.count());
assertEquals(437, myResourceHistoryTableDao.count());
});
/*
* Run a second time
*/
requestDetails = new SystemRequestDetails();
requestDetails.setRequestPartitionId(RequestPartitionId.fromPartitionId(myPartitionId));
input = loadResource(myFhirContext, Bundle.class, "/r4/test-patient-bundle.json");
myCaptureQueriesListener.clear();
output = mySystemDao.transaction(requestDetails, input);
myCaptureQueriesListener.logSelectQueries();
assertEquals(29, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(input.getEntry().size(), output.getEntry().size());
runInTransaction(()->{
assertEquals(437, myResourceTableDao.count());
assertEquals(437, myResourceHistoryTableDao.count());
});
}
/**
* JA: I disabled this test - I am not clear on what it was actually trying to test

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,386 @@
package ca.uhn.fhir.jpa.dao.r5;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.Observation;
import org.hl7.fhir.r5.model.Patient;
import org.hl7.fhir.r5.model.Quantity;
import org.hl7.fhir.r5.model.Reference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class FhirSystemDaoTransactionR5Test extends BaseJpaR5Test {
@AfterEach
public void after() {
JpaStorageSettings defaults = new JpaStorageSettings();
myStorageSettings.setIndexMissingFields(defaults.getIndexMissingFields());
myStorageSettings.setMatchUrlCacheEnabled(defaults.isMatchUrlCacheEnabled());
myStorageSettings.setDeleteEnabled(defaults.isDeleteEnabled());
}
/**
* If an inline match URL is the same as a conditional create in the same transaction, make sure we
* don't issue a select for it
*/
@ParameterizedTest(name = "{index}: {0}")
@CsvSource({
"Match URL Cache Enabled, true",
"Match URL Cache Disabled, false"
})
public void testInlineMatchUrlMatchesConditionalUpdate(@SuppressWarnings("unused") String theName, boolean theMatchUrlCacheEnabled) {
// Setup
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
myStorageSettings.setMatchUrlCacheEnabled(theMatchUrlCacheEnabled);
BundleBuilder bb = new BundleBuilder(myFhirContext);
Observation observation1 = new Observation();
observation1.addIdentifier().setSystem("http://observation").setValue("111");
observation1.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation1).conditional("Observation?identifier=http://observation|111");
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://patient").setValue("123");
bb.addTransactionUpdateEntry(patient).conditional("Patient?identifier=http://patient|123");
Observation observation2 = new Observation();
observation2.addIdentifier().setSystem("http://observation").setValue("222");
observation2.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation2).conditional("Observation?identifier=http://observation|222");
Observation observation3 = new Observation();
observation3.addIdentifier().setSystem("http://observation").setValue("333");
observation3.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation3).conditional("Observation?identifier=http://observation|333");
Bundle input = bb.getBundleTyped();
// Test
myCaptureQueriesListener.clear();
Bundle output = mySystemDao.transaction(mySrd, input);
// Verify
// One select to resolve the 3 match URLs
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
String firstSelectQuery = myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false);
assertEquals(1, countMatches(firstSelectQuery, "HASH_SYS_AND_VALUE in (? , ? , ? , ?)"), firstSelectQuery);
assertEquals(23, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(4, output.getEntry().size());
IdType patientId = new IdType(output.getEntry().get(1).getResponse().getLocation());
IdType observationId = new IdType(output.getEntry().get(2).getResponse().getLocation());
Observation actualObs = myObservationDao.read(observationId, mySrd);
assertEquals(patientId.toUnqualifiedVersionless().getValue(), actualObs.getSubject().getReference());
myCaptureQueriesListener.logInsertQueries();
runInTransaction(() -> {
assertEquals(4, myResourceTableDao.count());
assertEquals(4, myResourceHistoryTableDao.count());
assertEquals(6, myResourceLinkDao.count());
assertEquals(5, myResourceIndexedSearchParamTokenDao.count());
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
});
/*
* Repeat, and make sure we reuse
*/
bb = new BundleBuilder(myFhirContext);
observation1 = new Observation();
observation1.addIdentifier().setSystem("http://observation").setValue("111");
observation1.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation1).conditional("Observation?identifier=http://observation|111");
patient = new Patient();
patient.addIdentifier().setSystem("http://patient").setValue("123");
bb.addTransactionUpdateEntry(patient).conditional("Patient?identifier=http://patient|123");
observation2 = new Observation();
observation2.addIdentifier().setSystem("http://observation").setValue("222");
observation2.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation2).conditional("Observation?identifier=http://observation|222");
observation3 = new Observation();
observation3.addIdentifier().setSystem("http://observation").setValue("333");
observation3.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation3).conditional("Observation?identifier=http://observation|333");
input = bb.getBundleTyped();
// Test
myCaptureQueriesListener.clear();
output = mySystemDao.transaction(mySrd, input);
// Verify
assertEquals(theMatchUrlCacheEnabled ? 4 : 5, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(4, output.getEntry().size());
patientId = new IdType(output.getEntry().get(1).getResponse().getLocation());
observationId = new IdType(output.getEntry().get(2).getResponse().getLocation());
actualObs = myObservationDao.read(observationId, mySrd);
assertEquals(patientId.toUnqualifiedVersionless().getValue(), actualObs.getSubject().getReference());
myCaptureQueriesListener.logInsertQueries();
runInTransaction(() -> {
assertEquals(4, myResourceTableDao.count());
assertEquals(4, myResourceHistoryTableDao.count());
assertEquals(6, myResourceLinkDao.count());
assertEquals(5, myResourceIndexedSearchParamTokenDao.count());
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
});
/*
* Repeat once more
*/
bb = new BundleBuilder(myFhirContext);
observation1 = new Observation();
observation1.addIdentifier().setSystem("http://observation").setValue("111");
observation1.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation1).conditional("Observation?identifier=http://observation|111");
patient = new Patient();
patient.addIdentifier().setSystem("http://patient").setValue("123");
bb.addTransactionUpdateEntry(patient).conditional("Patient?identifier=http://patient|123");
observation2 = new Observation();
observation2.addIdentifier().setSystem("http://observation").setValue("222");
observation2.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionUpdateEntry(observation2).conditional("Observation?identifier=http://observation|222");
input = bb.getBundleTyped();
// Test
myCaptureQueriesListener.clear();
output = mySystemDao.transaction(mySrd, input);
// Verify
assertEquals(theMatchUrlCacheEnabled ? 4 : 5, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(3, output.getEntry().size());
patientId = new IdType(output.getEntry().get(1).getResponse().getLocation());
observationId = new IdType(output.getEntry().get(2).getResponse().getLocation());
actualObs = myObservationDao.read(observationId, mySrd);
assertEquals(patientId.toUnqualifiedVersionless().getValue(), actualObs.getSubject().getReference());
myCaptureQueriesListener.logInsertQueries();
runInTransaction(() -> {
assertEquals(4, myResourceTableDao.count());
assertEquals(4, myResourceHistoryTableDao.count());
assertEquals(6, myResourceLinkDao.count());
assertEquals(5, myResourceIndexedSearchParamTokenDao.count());
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
});
}
@ParameterizedTest(name = "{index}: {0}")
@CsvSource({
"Pre-existing with cache, true ,true",
"Pre-existing without cache, true ,false",
"No match with cache, false ,true",
"No match without cache, false ,false",
})
public void testRepeatedInlineMatchUrls(@SuppressWarnings("unused") String theName, boolean theTargetAlreadyExists, boolean theMatchUrlCacheEnabled) {
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true);
myStorageSettings.setMatchUrlCacheEnabled(theMatchUrlCacheEnabled);
myStorageSettings.setDeleteEnabled(false);
if (theTargetAlreadyExists) {
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://patient").setValue("123");
myPatientDao.create(patient, mySrd);
}
BundleBuilder bb = new BundleBuilder(myFhirContext);
for (int i = 0; i < 4; i++) {
Observation observation1 = new Observation();
observation1.addIdentifier().setSystem("http://observation").setValue(Integer.toString(i));
observation1.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionCreateEntry(observation1);
}
Bundle input = bb.getBundleTyped();
// Test
myCaptureQueriesListener.clear();
Bundle output = mySystemDao.transaction(mySrd, input);
// Verify
myCaptureQueriesListener.logSelectQueries();
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(theTargetAlreadyExists ? 20 : 24, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(4, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(4, output.getEntry().size());
runInTransaction(() -> {
assertEquals(5, myResourceTableDao.count());
assertEquals(5, myResourceHistoryTableDao.count());
assertEquals(8, myResourceLinkDao.count());
assertEquals(6, myResourceIndexedSearchParamTokenDao.count());
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
});
/*
* Second pass
*/
bb = new BundleBuilder(myFhirContext);
for (int i = 0; i < 4; i++) {
Observation observation1 = new Observation();
observation1.addIdentifier().setSystem("http://observation").setValue(Integer.toString(i));
observation1.setSubject(new Reference("Patient?identifier=http://patient|123"));
bb.addTransactionCreateEntry(observation1);
}
input = bb.getBundleTyped();
// Test
myCaptureQueriesListener.clear();
output = mySystemDao.transaction(mySrd, input);
// Verify
myCaptureQueriesListener.logSelectQueries();
assertEquals(theMatchUrlCacheEnabled ? 0 : 1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(20, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(4, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(4, output.getEntry().size());
runInTransaction(() -> {
assertEquals(9, myResourceTableDao.count());
assertEquals(9, myResourceHistoryTableDao.count());
assertEquals(16, myResourceLinkDao.count());
assertEquals(10, myResourceIndexedSearchParamTokenDao.count());
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
});
}
@ParameterizedTest(name = "{index}: {0}")
@CsvSource({
"NC Pre-existing with cache, true ,true, false",
"NC Pre-existing without cache, true ,false, false",
"NC No match with cache, false ,true, false",
"NC No match without cache, false ,false, false",
"C Pre-existing with cache, true ,true, true",
"C Pre-existing without cache, true ,false, true",
"C No match with cache, false ,true, true",
"C No match without cache, false ,false, true",
})
public void testComplexConditionalUpdate(String theName, boolean theTargetAlreadyExists, boolean theMatchUrlCacheEnabled, boolean theResourceChanges) {
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true);
myStorageSettings.setMatchUrlCacheEnabled(theMatchUrlCacheEnabled);
myStorageSettings.setDeleteEnabled(false);
Patient patient = new Patient();
patient.setId("Patient/P");
myPatientDao.update(patient, mySrd);
if (theTargetAlreadyExists) {
Observation observation1 = new Observation();
observation1.setValue(new Quantity(5L));
observation1.setSubject(new Reference("Patient/P"));
myObservationDao.create(observation1, mySrd);
}
BundleBuilder bb = new BundleBuilder(myFhirContext);
Observation observation1 = new Observation();
if (theResourceChanges) {
observation1.addNote().setText(UUID.randomUUID().toString());
}
observation1.setValue(new Quantity(5L));
observation1.setSubject(new Reference("Patient/P"));
bb.addTransactionUpdateEntry(observation1).conditional("Observation?subject=Patient/P&value-quantity=5");
Bundle input = bb.getBundleTyped();
// Test
myCaptureQueriesListener.clear();
Bundle output = mySystemDao.transaction(mySrd, input);
// Verify
myCaptureQueriesListener.logSelectQueries();
if (theTargetAlreadyExists) {
if (theResourceChanges) {
assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
} else {
assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
}
} else {
if (theResourceChanges) {
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(7, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
} else {
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(7, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
}
}
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countCommits());
assertEquals(0, myCaptureQueriesListener.countRollbacks());
assertEquals(1, output.getEntry().size());
runInTransaction(() -> {
assertEquals(2, myResourceTableDao.count());
assertEquals((theTargetAlreadyExists && theResourceChanges) ? 3 : 2, myResourceHistoryTableDao.count());
assertEquals(2, myResourceLinkDao.count());
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
assertEquals(0, myResourceIndexedSearchParamStringDao.count());
});
}
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.embedded;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.embedded;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.embedded;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.embedded;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.embedded;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.rest.api.server.storage;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
@ -209,14 +210,14 @@ public class TransactionDetails {
* "<code>Observation/123</code>") 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 IResourcePersistentId thePersistentId) {
public void addResolvedMatchUrl(FhirContext theFhirContext, String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) {
Validate.notBlank(theConditionalUrl);
Validate.notNull(thePersistentId);
if (myResolvedMatchUrls.isEmpty()) {
myResolvedMatchUrls = new HashMap<>();
} else if (matchUrlWithDiffIdExists(theConditionalUrl, thePersistentId)) {
String msg = "Invalid match URL " + theConditionalUrl + " - Multiple resources match this search";
String msg = theFhirContext.getLocalizer().getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", theConditionalUrl);
throw new PreconditionFailedException(Msg.code(2207) + msg);
}
myResolvedMatchUrls.put(theConditionalUrl, thePersistentId);

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server - Batch2 Task Processor
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.batch2.model;
import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk;

View File

@ -256,7 +256,11 @@ public abstract class BaseStorageDao {
protected DaoMethodOutcome toMethodOutcome(RequestDetails theRequest, @Nonnull final IBasePersistedResource theEntity, @Nonnull IBaseResource theResource, @Nullable String theMatchUrl, @Nonnull RestOperationTypeEnum theOperationType) {
DaoMethodOutcome outcome = new DaoMethodOutcome();
outcome.setPersistentId(theEntity.getPersistentId());
IResourcePersistentId persistentId = theEntity.getPersistentId();
persistentId.setAssociatedResourceId(theResource.getIdElement());
outcome.setPersistentId(persistentId);
outcome.setMatchUrl(theMatchUrl);
outcome.setOperationType(theOperationType);

View File

@ -152,7 +152,7 @@ public class MatchResourceUrlService<T extends IResourcePersistentId> {
if (retVal.size() == 1) {
T pid = retVal.iterator().next();
theTransactionDetails.addResolvedMatchUrl(matchUrl, pid);
theTransactionDetails.addResolvedMatchUrl(myContext, matchUrl, pid);
if (myStorageSettings.isMatchUrlCacheEnabled()) {
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, pid);
}
@ -216,7 +216,7 @@ public class MatchResourceUrlService<T extends IResourcePersistentId> {
Validate.notBlank(theMatchUrl);
Validate.notNull(theResourcePersistentId);
String matchUrl = massageForStorage(theResourceType, theMatchUrl);
theTransactionDetails.addResolvedMatchUrl(matchUrl, theResourcePersistentId);
theTransactionDetails.addResolvedMatchUrl(myContext, matchUrl, theResourcePersistentId);
if (myStorageSettings.isMatchUrlCacheEnabled()) {
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, theResourcePersistentId);
}

View File

@ -100,25 +100,30 @@ public abstract class BaseSearchParamWithInlineReferencesExtractor<T extends IRe
}
T match;
IIdType newId = null;
if (matches.isEmpty()) {
Optional<IBasePersistedResource> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null, theRequestDetails, theTransactionDetails);
if (placeholderOpt.isPresent()) {
match = (T) placeholderOpt.get().getPersistentId();
match.setAssociatedResourceId(placeholderOpt.get().getIdDt());
theTransactionDetails.addResolvedMatchUrl(nextIdText, match);
newId = myFhirContext.getVersion().newIdType();
newId.setValue(placeholderOpt.get().getIdDt().getValue());
match.setAssociatedResourceId(newId);
theTransactionDetails.addResolvedMatchUrl(myFhirContext, nextIdText, match);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match);
} else {
String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(Msg.code(1091) + msg);
}
} else if (matches.size() > 1) {
String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
String msg = myFhirContext.getLocalizer().getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(Msg.code(1092) + msg);
} else {
match = matches.iterator().next();
}
IIdType newId = myIdHelperService.translatePidIdToForcedId(myFhirContext, resourceTypeString, match);
if (newId == null) {
newId = myIdHelperService.translatePidIdToForcedId(myFhirContext, resourceTypeString, match);
}
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
if (theTransactionDetails != null) {

View File

@ -1,18 +1,26 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.http.message.BasicNameValuePair;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
public class UrlUtilTest {
private final FhirContext myCtx = FhirContext.forR4Cached();
@Test
public void testNormalizeCanonicalUrl() {
assertEquals("http://foo", UrlUtil.normalizeCanonicalUrlForComparison("http://foo/"));
@ -122,4 +130,37 @@ public class UrlUtilTest {
containsInAnyOrder(new BasicNameValuePair("names", "homer|simpson")));
}
@ParameterizedTest
@CsvSource({
"null, null",
"null, urn:uuid:12345",
"Patient, Patient",
"Patient, Patient?",
"Patient, Patient?identifier=foo",
"Patient, /Patient",
"Patient, /Patient?",
"Patient, /Patient?identifier=foo",
"Patient, http://foo/base/Patient?identifier=foo",
"Patient, http://foo/base/Patient/1",
"Patient, http://foo/base/Patient/1/_history/2",
"Patient, /Patient/1",
"Patient, /Patient/1/_history/2",
"Patient, Patient/1",
"Patient, Patient/1/_history/2",
})
public void testDetermineResourceTypeInResourceUrl(String theExpected, String theUrl) {
String url = theUrl;
if (url.equals("null")) {
url = null;
}
String actual = UrlUtil.determineResourceTypeInResourceUrl(myCtx, url);
if (theExpected.equals("null")) {
assertNull(actual);
} else {
assertEquals(theExpected, actual);
}
}
}