Improve SQL IN(..) queries (#1214)

* First attempt, probably has compile issues

* Fix qualified searches

* Another test fix

* More test fixes

* Add changelog

* Two more fixes

* Revert SQL logging
This commit is contained in:
James Agnew 2019-02-20 16:38:51 -05:00 committed by GitHub
parent 28b4b812ac
commit 89b08cd627
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 388 additions and 219 deletions

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -64,7 +64,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -92,6 +91,7 @@ import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.*;
@ -105,6 +105,7 @@ public class SearchBuilder implements ISearchBuilder {
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>()); private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class);
private static final int maxLoad = 800;
private static Long NO_MORE = -1L; private static Long NO_MORE = -1L;
private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest; private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
private static SearchParameterMap ourLastHandlerParamsForUnitTest; private static SearchParameterMap ourLastHandlerParamsForUnitTest;
@ -112,15 +113,14 @@ public class SearchBuilder implements ISearchBuilder {
private static boolean ourTrackHandlersForUnitTest; private static boolean ourTrackHandlersForUnitTest;
private final boolean myDontUseHashesForSearch; private final boolean myDontUseHashesForSearch;
private final DaoConfig myDaoConfig; private final DaoConfig myDaoConfig;
@Autowired @Autowired
protected IResourceTagDao myResourceTagDao; protected IResourceTagDao myResourceTagDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired @Autowired
private IResourceSearchViewDao myResourceSearchViewDao; private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired @Autowired
private FhirContext myContext; private FhirContext myContext;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired @Autowired
private IdHelperService myIdHelperService; private IdHelperService myIdHelperService;
@Autowired(required = false) @Autowired(required = false)
@ -137,7 +137,6 @@ public class SearchBuilder implements ISearchBuilder {
private MatchUrlService myMatchUrlService; private MatchUrlService myMatchUrlService;
@Autowired @Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
private List<Long> myAlsoIncludePids; private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder; private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao; private BaseHapiFhirDao<?> myCallingDao;
@ -823,6 +822,7 @@ public class SearchBuilder implements ISearchBuilder {
List<Predicate> codePredicates = new ArrayList<>(); List<Predicate> codePredicates = new ArrayList<>();
Join<ResourceTable, ResourceIndexedSearchParamToken> join = null; Join<ResourceTable, ResourceIndexedSearchParamToken> join = null;
List<IQueryParameterType> tokens = new ArrayList<>();
for (IQueryParameterType nextOr : theList) { for (IQueryParameterType nextOr : theList) {
if (nextOr instanceof TokenParam) { if (nextOr instanceof TokenParam) {
@ -836,14 +836,17 @@ public class SearchBuilder implements ISearchBuilder {
if (join == null) { if (join == null) {
join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); join = createOrReuseJoin(JoinEnum.TOKEN, theParamName);
} }
Predicate singleCode = createPredicateToken(nextOr, theResourceName, theParamName, myBuilder, join);
codePredicates.add(singleCode); tokens.add(nextOr);
} }
if (codePredicates.isEmpty()) { if (tokens.isEmpty()) {
return; return;
} }
List<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join);
codePredicates.addAll(singleCode);
Predicate spPredicate = myBuilder.or(toArray(codePredicates)); Predicate spPredicate = myBuilder.or(toArray(codePredicates));
myPredicates.add(spPredicate); myPredicates.add(spPredicate);
} }
@ -965,7 +968,9 @@ public class SearchBuilder implements ISearchBuilder {
} }
case TOKEN: { case TOKEN: {
From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = theRoot.join("myParamsToken", JoinType.INNER); From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = theRoot.join("myParamsToken", JoinType.INNER);
retVal = createPredicateToken(leftValue, theResourceName, theParam.getName(), myBuilder, tokenJoin); List<IQueryParameterType> tokens = Collections.singletonList(leftValue);
List<Predicate> tokenPredicates = createPredicateToken(tokens, theResourceName, theParam.getName(), myBuilder, tokenJoin);
retVal = myBuilder.and(tokenPredicates.toArray(new Predicate[0]));
break; break;
} }
case DATE: { case DATE: {
@ -1309,183 +1314,169 @@ public class SearchBuilder implements ISearchBuilder {
return orPredicates; return orPredicates;
} }
private Predicate createPredicateToken(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder, private List<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamToken> theFrom) { From<?, ResourceIndexedSearchParamToken> theFrom) {
String code; final List<VersionIndependentConcept> codes = new ArrayList<>();
String system;
TokenParamModifier modifier = null; TokenParamModifier modifier = null;
if (theParameter instanceof TokenParam) { for (IQueryParameterType nextParameter : theParameters) {
TokenParam id = (TokenParam) theParameter;
system = id.getSystem(); String code;
code = (id.getValue()); String system;
modifier = id.getModifier(); if (nextParameter instanceof TokenParam) {
} else if (theParameter instanceof BaseIdentifierDt) { TokenParam id = (TokenParam) nextParameter;
BaseIdentifierDt id = (BaseIdentifierDt) theParameter; system = id.getSystem();
system = id.getSystemElement().getValueAsString(); code = (id.getValue());
code = (id.getValueElement().getValue()); modifier = id.getModifier();
} else if (theParameter instanceof BaseCodingDt) { } else if (nextParameter instanceof BaseIdentifierDt) {
BaseCodingDt id = (BaseCodingDt) theParameter; BaseIdentifierDt id = (BaseIdentifierDt) nextParameter;
system = id.getSystemElement().getValueAsString(); system = id.getSystemElement().getValueAsString();
code = (id.getCodeElement().getValue()); code = (id.getValueElement().getValue());
} else if (theParameter instanceof NumberParam) { } else if (nextParameter instanceof BaseCodingDt) {
NumberParam number = (NumberParam) theParameter; BaseCodingDt id = (BaseCodingDt) nextParameter;
system = null; system = id.getSystemElement().getValueAsString();
code = number.getValueAsQueryToken(myContext); code = (id.getCodeElement().getValue());
} else { } else if (nextParameter instanceof NumberParam) {
throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass()); NumberParam number = (NumberParam) nextParameter;
system = null;
code = number.getValueAsQueryToken(myContext);
} else {
throw new IllegalArgumentException("Invalid token type: " + nextParameter.getClass());
}
if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
throw new InvalidRequestException(
"Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
}
if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
throw new InvalidRequestException(
"Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
}
/*
* Process token modifiers (:in, :below, :above)
*/
if (modifier == TokenParamModifier.IN) {
codes.addAll(myTerminologySvc.expandValueSet(code));
} else if (modifier == TokenParamModifier.ABOVE) {
system = determineSystemIfMissing(theParamName, code, system);
codes.addAll(myTerminologySvc.findCodesAbove(system, code));
} else if (modifier == TokenParamModifier.BELOW) {
system = determineSystemIfMissing(theParamName, code, system);
codes.addAll(myTerminologySvc.findCodesBelow(system, code));
} else {
codes.add(new VersionIndependentConcept(system, code));
}
} }
if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { List<VersionIndependentConcept> sortedCodesList = codes
throw new InvalidRequestException( .stream()
"Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); .filter(t -> t.getCode() != null || t.getSystem() != null)
} .sorted()
.distinct()
if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { .collect(Collectors.toList());
throw new InvalidRequestException(
"Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
}
/*
* Process token modifiers (:in, :below, :above)
*/
List<VersionIndependentConcept> codes;
if (modifier == TokenParamModifier.IN) {
codes = myTerminologySvc.expandValueSet(code);
} else if (modifier == TokenParamModifier.ABOVE) {
system = determineSystemIfMissing(theParamName, code, system);
codes = myTerminologySvc.findCodesAbove(system, code);
} else if (modifier == TokenParamModifier.BELOW) {
system = determineSystemIfMissing(theParamName, code, system);
codes = myTerminologySvc.findCodesBelow(system, code);
} else {
codes = Collections.singletonList(new VersionIndependentConcept(system, code));
}
if (codes.isEmpty()) { if (codes.isEmpty()) {
// This will never match anything // This will never match anything
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false); return Collections.singletonList(new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false));
} }
List<Predicate> retVal = new ArrayList<>();
// System only
List<VersionIndependentConcept> systemOnlyCodes = sortedCodesList.stream().filter(t -> isBlank(t.getCode())).collect(Collectors.toList());
if (!systemOnlyCodes.isEmpty()) {
retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemOnlyCodes, modifier, TokenModeEnum.SYSTEM_ONLY));
}
// Code only
List<VersionIndependentConcept> codeOnlyCodes = sortedCodesList.stream().filter(t -> t.getSystem() == null).collect(Collectors.toList());
if (!codeOnlyCodes.isEmpty()) {
retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, codeOnlyCodes, modifier, TokenModeEnum.VALUE_ONLY));
}
// System and code
List<VersionIndependentConcept> systemAndCodeCodes = sortedCodesList.stream().filter(t -> isNotBlank(t.getCode()) && t.getSystem() != null).collect(Collectors.toList());
if (!systemAndCodeCodes.isEmpty()) {
retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemAndCodeCodes, modifier, TokenModeEnum.SYSTEM_AND_VALUE));
}
return retVal;
}
private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<VersionIndependentConcept> theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) {
if (myDontUseHashesForSearch) { if (myDontUseHashesForSearch) {
ArrayList<Predicate> singleCodePredicates = new ArrayList<Predicate>(); final Path<String> systemExpression = theFrom.get("mySystem");
if (codes != null) { final Path<String> valueExpression = theFrom.get("myValue");
List<Predicate> orPredicates = new ArrayList<Predicate>(); List<Predicate> orPredicates = new ArrayList<>();
Map<String, List<VersionIndependentConcept>> map = new HashMap<String, List<VersionIndependentConcept>>(); switch (theTokenMode) {
for (VersionIndependentConcept nextCode : codes) { case SYSTEM_ONLY: {
List<VersionIndependentConcept> systemCodes = map.get(nextCode.getSystem()); List<String> systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList());
if (null == systemCodes) { Predicate orPredicate = systemExpression.in(systems);
systemCodes = new ArrayList<>(); orPredicates.add(orPredicate);
map.put(nextCode.getSystem(), systemCodes); break;
}
systemCodes.add(nextCode);
} }
// Use "in" in case of large numbers of codes due to param modifiers case VALUE_ONLY:
final Path<String> systemExpression = theFrom.get("mySystem"); List<String> codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList());
final Path<String> valueExpression = theFrom.get("myValue"); Predicate orPredicate = valueExpression.in(codes);
for (Map.Entry<String, List<VersionIndependentConcept>> entry : map.entrySet()) { orPredicates.add(orPredicate);
CriteriaBuilder.In<String> codePredicate = theBuilder.in(valueExpression); break;
boolean haveAtLeastOneCode = false; case SYSTEM_AND_VALUE:
for (VersionIndependentConcept nextCode : entry.getValue()) { for (VersionIndependentConcept next : theTokens) {
if (isNotBlank(nextCode.getCode())) { orPredicates.add(theBuilder.and(
codePredicate.value(nextCode.getCode()); toEqualOrIsNullPredicate(systemExpression, next.getSystem()),
haveAtLeastOneCode = true; toEqualOrIsNullPredicate(valueExpression, next.getCode())
} ));
} }
break;
if (entry.getKey() != null) {
Predicate systemPredicate = theBuilder.equal(systemExpression, entry.getKey());
if (haveAtLeastOneCode) {
orPredicates.add(theBuilder.and(systemPredicate, codePredicate));
} else {
orPredicates.add(systemPredicate);
}
} else {
orPredicates.add(codePredicate);
}
}
Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
if (modifier == TokenParamModifier.NOT) {
or = theBuilder.not(or);
}
singleCodePredicates.add(or);
} else {
/*
* Ok, this is a normal query
*/
if (StringUtils.isNotBlank(system)) {
if (modifier != null && modifier == TokenParamModifier.NOT) {
singleCodePredicates.add(theBuilder.notEqual(theFrom.get("mySystem"), system));
} else {
singleCodePredicates.add(theBuilder.equal(theFrom.get("mySystem"), system));
}
} else if (system == null) {
// don't check the system
} else {
// If the system is "", we only match on null systems
singleCodePredicates.add(theBuilder.isNull(theFrom.get("mySystem")));
}
if (StringUtils.isNotBlank(code)) {
if (modifier != null && modifier == TokenParamModifier.NOT) {
singleCodePredicates.add(theBuilder.notEqual(theFrom.get("myValue"), code));
} else {
singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code));
}
} else {
/*
* As of HAPI FHIR 1.5, if the client searched for a token with a system but no specified value this means to
* match all tokens with the given value.
*
* I'm not sure I agree with this, but hey.. FHIR-I voted and this was the result :)
*/
// singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
}
} }
Predicate singleCode = theBuilder.and(toArray(singleCodePredicates)); Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); if (theModifier == TokenParamModifier.NOT) {
} or = theBuilder.not(or);
}
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or);
}
/* /*
* Note: A null system value means "match any system", but * Note: A null system value means "match any system", but
* an empty-string system value means "match values that * an empty-string system value means "match values that
* explicitly have no system". * explicitly have no system".
*/ */
boolean haveSystem = codes.get(0).getSystem() != null;
boolean haveCode = isNotBlank(codes.get(0).getCode());
Expression<Long> hashField; Expression<Long> hashField;
if (!haveSystem && !haveCode) { List<Long> values;
// If we have neither, this isn't actually an expression so switch (theTokenMode) {
// just return 1=1 case SYSTEM_ONLY:
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, true); hashField = theFrom.get("myHashSystem").as(Long.class);
} else if (haveSystem && haveCode) { values = theTokens
hashField = theFrom.get("myHashSystemAndValue").as(Long.class); .stream()
} else if (haveSystem) { .map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, t.getSystem()))
hashField = theFrom.get("myHashSystem").as(Long.class); .collect(Collectors.toList());
} else { break;
hashField = theFrom.get("myHashValue").as(Long.class); case VALUE_ONLY:
} hashField = theFrom.get("myHashValue").as(Long.class);
values = theTokens
List<Long> values = new ArrayList<>(codes.size()); .stream()
for (VersionIndependentConcept next : codes) { .map(t -> ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, t.getCode()))
if (haveSystem && haveCode) { .collect(Collectors.toList());
values.add(ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, next.getSystem(), next.getCode())); break;
} else if (haveSystem) { case SYSTEM_AND_VALUE:
values.add(ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, next.getSystem())); default:
} else { hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
values.add(ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, next.getCode())); values = theTokens
} .stream()
.map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, t.getSystem(), t.getCode()))
.collect(Collectors.toList());
break;
} }
Predicate predicate = hashField.in(values); Predicate predicate = hashField.in(values);
if (modifier == TokenParamModifier.NOT) { if (theModifier == TokenParamModifier.NOT) {
Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName)); Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName));
Predicate disjunctionPredicate = theBuilder.not(predicate); Predicate disjunctionPredicate = theBuilder.not(predicate);
predicate = theBuilder.and(identityPredicate, disjunctionPredicate); predicate = theBuilder.and(identityPredicate, disjunctionPredicate);
@ -1493,6 +1484,13 @@ public class SearchBuilder implements ISearchBuilder {
return predicate; return predicate;
} }
private <T> Expression<Boolean> toEqualOrIsNullPredicate(Path<T> theExpression, T theCode) {
if (theCode == null) {
return myBuilder.isNull(theExpression);
}
return myBuilder.equal(theExpression, theCode);
}
@Override @Override
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid) { public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid) {
myParams = theParams; myParams = theParams;
@ -1966,8 +1964,6 @@ public class SearchBuilder implements ISearchBuilder {
return tagMap; return tagMap;
} }
private static final int maxLoad = 800;
@Override @Override
public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation, public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation,
EntityManager entityManager, FhirContext context, IDao theDao) { EntityManager entityManager, FhirContext context, IDao theDao) {
@ -2319,6 +2315,35 @@ public class SearchBuilder implements ISearchBuilder {
return qp; return qp;
} }
private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
String theResourceType) {
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
List<String> path = param.getPathsSplit();
/*
* SearchParameters can declare paths on multiple resources
* types. Here we only want the ones that actually apply.
*/
path = new ArrayList<>(path);
for (int i = 0; i < path.size(); i++) {
String nextPath = trim(path.get(i));
if (!nextPath.contains(theResourceType + ".")) {
path.remove(i);
i--;
}
}
return theFrom.get("mySourcePath").in(path);
}
private enum TokenModeEnum {
SYSTEM_ONLY,
VALUE_ONLY,
SYSTEM_AND_VALUE
}
public enum HandlerTypeEnum { public enum HandlerTypeEnum {
UNIQUE_INDEX, STANDARD_QUERY UNIQUE_INDEX, STANDARD_QUERY
} }
@ -2673,29 +2698,6 @@ public class SearchBuilder implements ISearchBuilder {
return likeExpression.replace("%", "[%]") + "%"; return likeExpression.replace("%", "[%]") + "%";
} }
private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
String theResourceType) {
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
List<String> path = param.getPathsSplit();
/*
* SearchParameters can declare paths on multiple resources
* types. Here we only want the ones that actually apply.
*/
path = new ArrayList<>(path);
for (int i = 0; i < path.size(); i++) {
String nextPath = trim(path.get(i));
if (!nextPath.contains(theResourceType + ".")) {
path.remove(i);
i--;
}
}
return theFrom.get("mySourcePath").in(path);
}
private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) { private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) {
if (thePids.isEmpty()) { if (thePids.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();

View File

@ -468,9 +468,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
ArrayList<VersionIndependentConcept> retVal = new ArrayList<>(); ArrayList<VersionIndependentConcept> retVal = new ArrayList<>();
for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : expandedR4.getContains()) { for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : expandedR4.getContains()) {
retVal.add( retVal.add(
new VersionIndependentConcept() new VersionIndependentConcept(nextContains.getSystem(), nextContains.getCode()));
.setSystem(nextContains.getSystem())
.setCode(nextContains.getCode()));
} }
return retVal; return retVal;
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,42 +20,65 @@ package ca.uhn.fhir.jpa.term;
* #L% * #L%
*/ */
public class VersionIndependentConcept { import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
private String mySystem; public class VersionIndependentConcept implements Comparable<VersionIndependentConcept> {
private String myCode;
/** private final String mySystem;
* Constructor private final String myCode;
*/ private int myHashCode;
public VersionIndependentConcept() {
super();
}
/** /**
* Constructor * Constructor
*/ */
public VersionIndependentConcept(String theSystem, String theCode) { public VersionIndependentConcept(String theSystem, String theCode) {
setSystem(theSystem); mySystem = theSystem;
setCode(theCode); myCode = theCode;
myHashCode = new HashCodeBuilder(17, 37)
.append(mySystem)
.append(myCode)
.toHashCode();
} }
public String getSystem() { public String getSystem() {
return mySystem; return mySystem;
} }
public VersionIndependentConcept setSystem(String theSystem) {
mySystem = theSystem;
return this;
}
public String getCode() { public String getCode() {
return myCode; return myCode;
} }
public VersionIndependentConcept setCode(String theCode) { @Override
myCode = theCode; public boolean equals(Object theO) {
return this; if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
VersionIndependentConcept that = (VersionIndependentConcept) theO;
return new EqualsBuilder()
.append(mySystem, that.mySystem)
.append(myCode, that.myCode)
.isEquals();
} }
@Override
public int hashCode() {
return myHashCode;
}
@Override
public int compareTo(VersionIndependentConcept theOther) {
CompareToBuilder b = new CompareToBuilder();
b.append(mySystem, theOther.getSystem());
b.append(myCode, theOther.getCode());
return b.toComparison();
}
} }

View File

@ -65,7 +65,7 @@ public class CaptureQueriesListener implements ProxyDataSourceBuilder.SingleQuer
List<String> nextParams = new ArrayList<>(myParams); List<String> nextParams = new ArrayList<>(myParams);
while (retVal.contains("?") && nextParams.size() > 0) { while (retVal.contains("?") && nextParams.size() > 0) {
int idx = retVal.indexOf("?"); int idx = retVal.indexOf("?");
retVal = retVal.substring(0, idx) + nextParams.remove(0) + retVal.substring(idx + 1); retVal = retVal.substring(0, idx) + "'" + nextParams.remove(0) + "'" + retVal.substring(idx + 1);
} }
} }

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;

View File

@ -2030,7 +2030,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
} }
@Test @Test
public void testSearchStringParamWithLike() throws Exception { public void testSearchStringParamWithLike() {
SearchParameterMap map = new SearchParameterMap(); SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_FAMILY, new StringOrListParam().addOr(new StringParam("AAA")).addOr(new StringParam("BBB"))); map.add(Patient.SP_FAMILY, new StringOrListParam().addOr(new StringParam("AAA")).addOr(new StringParam("BBB")));
map.setLoadSynchronous(true); map.setLoadSynchronous(true);
@ -2047,6 +2047,95 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
} }
@Test
public void testSearchTokenListLike() {
Patient p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("FOO");
myPatientDao.create(p);
p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("BAR");
myPatientDao.create(p);
p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("BAZ");
myPatientDao.create(p);
CaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenOrListParam().addOr(new TokenParam("FOO")).addOr(new TokenParam("BAR")));
map.setLoadSynchronous(true);
IBundleProvider search = myPatientDao.search(map);
List<String> queries = CaptureQueriesListener
.getLastNQueries()
.stream()
.map(t -> t.getSql(true, true))
.filter(t -> t.contains("select"))
.collect(Collectors.toList());
String resultingQueryFormatted = queries.get(queries.size() - 1);
ourLog.info("Resulting query formatted:\n{}", resultingQueryFormatted);
queries = CaptureQueriesListener
.getLastNQueries()
.stream()
.map(t -> t.getSql(true, false))
.filter(t -> t.contains("select"))
.collect(Collectors.toList());
String resultingQueryNotFormatted = queries.get(queries.size() - 1);
assertEquals(resultingQueryFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "HASH_VALUE"));
assertThat(resultingQueryNotFormatted, containsString("HASH_VALUE in ('3140583648400062149' , '4929264259256651518')"));
// Ensure that the search actually worked
assertEquals(2, search.size().intValue());
}
@Test
public void testSearchTokenListWithMixedCombinations() {
Patient p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("FOO");
myPatientDao.create(p);
p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("BAR");
myPatientDao.create(p);
p = new Patient();
p.addIdentifier().setSystem("SAS").setValue("BAZ");
myPatientDao.create(p);
CaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenOrListParam().addOr(new TokenParam("SAS", null)).addOr(new TokenParam("FOO")).addOr(new TokenParam("BAR")));
map.setLoadSynchronous(true);
IBundleProvider search = myPatientDao.search(map);
List<String> queries = CaptureQueriesListener
.getLastNQueries()
.stream()
.map(t -> t.getSql(true, true))
.filter(t -> t.contains("select"))
.collect(Collectors.toList());
String resultingQueryFormatted = queries.get(queries.size() - 1);
ourLog.info("Resulting query formatted:\n{}", resultingQueryFormatted);
queries = CaptureQueriesListener
.getLastNQueries()
.stream()
.map(t -> t.getSql(true, false))
.filter(t -> t.contains("select"))
.collect(Collectors.toList());
String resultingQueryNotFormatted = queries.get(queries.size() - 1);
assertEquals(resultingQueryFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "HASH_VALUE"));
assertThat(resultingQueryNotFormatted, containsString("HASH_VALUE in ('3140583648400062149' , '4929264259256651518')"));
// Ensure that the search actually worked
assertEquals(3, search.size().intValue());
}
@Test @Test
public void testSearchStringParam() throws Exception { public void testSearchStringParam() throws Exception {
IIdType pid1; IIdType pid1;

View File

@ -1,9 +1,10 @@
package ca.uhn.fhir.jpa.dao.r4; package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.config.CaptureQueriesListener;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.*;
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;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
@ -39,6 +40,7 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -386,6 +388,51 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
} }
@Test
public void testSearchTokenListLike() {
Patient p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("FOO");
myPatientDao.create(p);
p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("BAR");
myPatientDao.create(p);
p = new Patient();
p.addIdentifier().setSystem("SYS").setValue("BAZ");
myPatientDao.create(p);
CaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenOrListParam().addOr(new TokenParam("FOO")).addOr(new TokenParam("BAR")));
map.setLoadSynchronous(true);
IBundleProvider search = myPatientDao.search(map);
List<String> queries = CaptureQueriesListener
.getLastNQueries()
.stream()
.map(t -> t.getSql(true, true))
.filter(t -> t.contains("select"))
.collect(Collectors.toList());
String resultingQueryFormatted = queries.get(queries.size() - 1);
ourLog.info("Resulting query formatted:\n{}", resultingQueryFormatted);
queries = CaptureQueriesListener
.getLastNQueries()
.stream()
.map(t -> t.getSql(true, false))
.filter(t -> t.contains("select"))
.collect(Collectors.toList());
String resultingQueryNotFormatted = queries.get(queries.size() - 1);
assertEquals(resultingQueryFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "SP_VALUE"));
assertThat(resultingQueryNotFormatted, containsString("SP_VALUE in ('BAR' , 'FOO')"));
// Ensure that the search actually worked
assertEquals(2, search.size().intValue());
}
@Test @Test
public void testHasParameter() { public void testHasParameter() {
IIdType pid0; IIdType pid0;
@ -616,7 +663,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
expect1.setResource(resource); expect1.setResource(resource);
expect1.calculateHashes(); expect1.calculateHashes();
assertThat("Got: \"" + results.toString()+"\"", results, containsInAnyOrder(expect0, expect1)); assertThat("Got: \"" + results.toString() + "\"", results, containsInAnyOrder(expect0, expect1));
} }
}); });
} }
@ -1059,7 +1106,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1"); QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1");
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1);
IBundleProvider result = myObservationDao.search(map); IBundleProvider result = myObservationDao.search(map);
assertThat("Got: "+ toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue())); assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue()));
} }
} }
@ -1091,7 +1138,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
CompositeParam<TokenParam, QuantityParam> val = new CompositeParam<>(v0, v1); CompositeParam<TokenParam, QuantityParam> val = new CompositeParam<>(v0, v1);
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val); SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val);
IBundleProvider result = myObservationDao.search(map); IBundleProvider result = myObservationDao.search(map);
assertThat("Got: "+ toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue())); assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue()));
} }
{ {
TokenParam v0 = new TokenParam("http://foo", "code1"); TokenParam v0 = new TokenParam("http://foo", "code1");
@ -2245,18 +2292,25 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd); myPatientDao.create(patient, mySrd);
patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue(null);
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
patient = new Patient(); patient = new Patient();
patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002"); patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd); myPatientDao.create(patient, mySrd);
{ {
// Match system="urn:system" and value = *
SearchParameterMap map = new SearchParameterMap(); SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null)); map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null));
IBundleProvider retrieved = myPatientDao.search(map); IBundleProvider retrieved = myPatientDao.search(map);
assertEquals(2, retrieved.size().intValue()); assertEquals(2, retrieved.size().intValue());
} }
{ {
// Match system="urn:system" and value = ""
SearchParameterMap map = new SearchParameterMap(); SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", "")); map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", ""));
IBundleProvider retrieved = myPatientDao.search(map); IBundleProvider retrieved = myPatientDao.search(map);
@ -3288,7 +3342,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
"Observation/YES21", "Observation/YES21",
"Observation/YES22", "Observation/YES22",
"Observation/YES23" "Observation/YES23"
)); ));
} }
private void createObservationWithEffective(String theId, String theEffective) { private void createObservationWithEffective(String theId, String theEffective) {

View File

@ -48,6 +48,10 @@
using the string name of the datatype (e.g. "dateTime") in order to help using the string name of the datatype (e.g. "dateTime") in order to help
building Parameters resources in a version-independent way. building Parameters resources in a version-independent way.
</action> </action>
<action type="add">
The JPA query builder has been optimized to take better advantage of SQL IN (..) expressions
when performing token searches with multiple OR values.
</action>
</release> </release>
<release version="3.7.0" date="2019-02-06" description="Gale"> <release version="3.7.0" date="2019-02-06" description="Gale">
<action type="add"> <action type="add">