Merge pull request #2934 from hapifhir/double-conditionalCreateIssue

Double conditional create issue
This commit is contained in:
Tadgh 2021-08-31 15:34:41 -04:00 committed by GitHub
commit 502b2360af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 9 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 2933
jira: SMILE-3056
title: "Fixed a regression which causes transactions with multiple identical ifNoneExist clauses to create duplicate data."

View File

@ -244,24 +244,28 @@ public class TransactionProcessor extends BaseTransactionProcessor {
if (orPredicates.size() > 1) { if (orPredicates.size() > 1) {
cq.where(cb.or(orPredicates.toArray(EMPTY_PREDICATE_ARRAY))); cq.where(cb.or(orPredicates.toArray(EMPTY_PREDICATE_ARRAY)));
Map<Long, MatchUrlToResolve> hashToSearchMap = buildHashToSearchMap(searchParameterMapsToResolve); Map<Long, List<MatchUrlToResolve>> hashToSearchMap = buildHashToSearchMap(searchParameterMapsToResolve);
TypedQuery<ResourceIndexedSearchParamToken> query = myEntityManager.createQuery(cq); TypedQuery<ResourceIndexedSearchParamToken> query = myEntityManager.createQuery(cq);
List<ResourceIndexedSearchParamToken> results = query.getResultList(); List<ResourceIndexedSearchParamToken> results = query.getResultList();
for (ResourceIndexedSearchParamToken nextResult : results) { for (ResourceIndexedSearchParamToken nextResult : results) {
Optional<MatchUrlToResolve> matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashSystemAndValue())); Optional<List<MatchUrlToResolve>> matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashSystemAndValue()));
if (!matchedSearch.isPresent()) { if (!matchedSearch.isPresent()) {
matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashValue())); matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashValue()));
} }
matchedSearch.ifPresent(matchUrlToResolve -> setSearchToResolvedAndPrefetchFoundResourcePid(theTransactionDetails, idsToPreFetch, nextResult, matchUrlToResolve)); 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. //For each SP Map which did not return a result, tag it as not found.
searchParameterMapsToResolve.stream() searchParameterMapsToResolve.stream()
// No matches // No matches
.filter(match -> !match.myResolved) .filter(match -> !match.myResolved)
.forEach(match -> { .forEach(match -> {
ourLog.warn("Was unable to match url {} from database", match.myRequestUrl); ourLog.debug("Was unable to match url {} from database", match.myRequestUrl);
theTransactionDetails.addResolvedMatchUrl(match.myRequestUrl, TransactionDetails.NOT_FOUND); theTransactionDetails.addResolvedMatchUrl(match.myRequestUrl, TransactionDetails.NOT_FOUND);
}); });
} }
@ -322,22 +326,26 @@ public class TransactionProcessor extends BaseTransactionProcessor {
return hashPredicate; return hashPredicate;
} }
private Map<Long, MatchUrlToResolve> buildHashToSearchMap(List<MatchUrlToResolve> searchParameterMapsToResolve) { private Map<Long, List<MatchUrlToResolve>> buildHashToSearchMap(List<MatchUrlToResolve> searchParameterMapsToResolve) {
Map<Long, MatchUrlToResolve> hashToSearch = new HashMap<>(); Map<Long, List<MatchUrlToResolve>> hashToSearch = new HashMap<>();
//Build a lookup map so we don't have to iterate over the searches repeatedly. //Build a lookup map so we don't have to iterate over the searches repeatedly.
for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) {
if (nextSearchParameterMap.myHashSystemAndValue != null) { if (nextSearchParameterMap.myHashSystemAndValue != null) {
hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap); List<MatchUrlToResolve> matchUrlsToResolve = hashToSearch.getOrDefault(nextSearchParameterMap.myHashSystemAndValue, new ArrayList<>());
matchUrlsToResolve.add(nextSearchParameterMap);
hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, matchUrlsToResolve);
} }
if (nextSearchParameterMap.myHashValue!= null) { if (nextSearchParameterMap.myHashValue!= null) {
hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap); List<MatchUrlToResolve> matchUrlsToResolve = hashToSearch.getOrDefault(nextSearchParameterMap.myHashValue, new ArrayList<>());
matchUrlsToResolve.add(nextSearchParameterMap);
hashToSearch.put(nextSearchParameterMap.myHashValue, matchUrlsToResolve);
} }
} }
return hashToSearch; return hashToSearch;
} }
private void setSearchToResolvedAndPrefetchFoundResourcePid(TransactionDetails theTransactionDetails, List<Long> idsToPreFetch, ResourceIndexedSearchParamToken nextResult, MatchUrlToResolve nextSearchParameterMap) { private void setSearchToResolvedAndPrefetchFoundResourcePid(TransactionDetails theTransactionDetails, List<Long> idsToPreFetch, ResourceIndexedSearchParamToken nextResult, MatchUrlToResolve nextSearchParameterMap) {
ourLog.warn("Matched url {} from database", nextSearchParameterMap.myRequestUrl); ourLog.debug("Matched url {} from database", nextSearchParameterMap.myRequestUrl);
idsToPreFetch.add(nextResult.getResourcePid()); idsToPreFetch.add(nextResult.getResourcePid());
myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, nextSearchParameterMap.myResourceDefinition.getName(), nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, nextSearchParameterMap.myResourceDefinition.getName(), nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid()));
theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid()));

View File

@ -20,6 +20,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
@ -126,6 +127,7 @@ import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
@ -150,12 +152,14 @@ import java.util.stream.Collectors;
import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE; import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE;
import static org.apache.commons.lang3.StringUtils.countMatches; import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
@ -1356,6 +1360,34 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
assertThat(actual, contains(id)); assertThat(actual, contains(id));
} }
@Test
@DisplayName("Duplicate Conditional Creates all resolve to the same match")
public void testDuplicateConditionalCreatesOnToken() throws IOException {
String inputString = IOUtils.toString(getClass().getResourceAsStream("/duplicate-conditional-create.json"), StandardCharsets.UTF_8);
Bundle firstBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString);
//Before you ask, yes, this has to be separately parsed. The reason for this is that the parameters passed to mySystemDao.transaction are _not_ immutable, so we cannot
//simply reuse the original bundle object.
Bundle duplicateBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString);
Bundle bundleResponse = mySystemDao.transaction(new SystemRequestDetails(), firstBundle);
bundleResponse.getEntry()
.forEach( entry -> assertThat(entry.getResponse().getStatus(), is(equalTo("201 Created"))));
IBundleProvider search = myOrganizationDao.search(new SearchParameterMap().setLoadSynchronous(true));
assertEquals(1, search.getAllResources().size());
//Running the bundle again should just result in 0 new resources created, as the org should already exist, and there is no update to the SR.
bundleResponse= mySystemDao.transaction(new SystemRequestDetails(), duplicateBundle);
bundleResponse.getEntry()
.forEach( entry -> {
assertThat(entry.getResponse().getStatus(), is(equalTo("200 OK")));
});
search = myOrganizationDao.search(new SearchParameterMap().setLoadSynchronous(true), new SystemRequestDetails());
assertEquals(1, search.getAllResources().size());
}
@Test @Test
public void testIndexNoDuplicatesToken() { public void testIndexNoDuplicatesToken() {
Patient res = new Patient(); Patient res = new Patient();

View File

@ -0,0 +1,66 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:4cd35592-5d4d-462b-8483-e404c023d316",
"resource": {
"resourceType": "Organization",
"identifier": [
{
"system": "https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id",
"value": "3972"
}
]
},
"request": {
"method": "POST",
"url": "/Organization",
"ifNoneExist": "Organization?identifier=https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id|3972"
}
},
{
"fullUrl": "urn:uuid:02643c1d-94d1-4991-a063-036fa0f57ec2",
"resource": {
"resourceType": "Organization",
"identifier": [
{
"system": "https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id",
"value": "3972"
}
]
},
"request": {
"method": "POST",
"url": "/Organization",
"ifNoneExist": "Organization?identifier=https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id|3972"
}
},
{
"fullUrl": "urn:uuid:8271e94f-e08b-498e-ad6d-751928c3ff99",
"resource": {
"resourceType": "ServiceRequest",
"identifier": [
{
"system": "https://fhir-tester.ca/NamingSystem/service-request-id",
"value": "1"
}
],
"performer": [
{
"reference": "urn:uuid:4cd35592-5d4d-462b-8483-e404c023d316",
"type": "Organization"
},
{
"reference": "urn:uuid:02643c1d-94d1-4991-a063-036fa0f57ec2",
"type": "Organization"
}
]
},
"request": {
"method": "PUT",
"url": "/ServiceRequest?identifier=https://fhir-tester.ca/NamingSystem/service-request-id|1"
}
}
]
}