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");
* 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.
@ -64,7 +64,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -92,6 +91,7 @@ import java.math.BigDecimal;
import java.math.MathContext;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
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 org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class);
private static final int maxLoad = 800;
private static Long NO_MORE = -1L;
private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
private static SearchParameterMap ourLastHandlerParamsForUnitTest;
@ -112,15 +113,14 @@ public class SearchBuilder implements ISearchBuilder {
private static boolean ourTrackHandlersForUnitTest;
private final boolean myDontUseHashesForSearch;
private final DaoConfig myDaoConfig;
@Autowired
protected IResourceTagDao myResourceTagDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired
private FhirContext myContext;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private IdHelperService myIdHelperService;
@Autowired(required = false)
@ -137,7 +137,6 @@ public class SearchBuilder implements ISearchBuilder {
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao;
@ -823,6 +822,7 @@ public class SearchBuilder implements ISearchBuilder {
List<Predicate> codePredicates = new ArrayList<>();
Join<ResourceTable, ResourceIndexedSearchParamToken> join = null;
List<IQueryParameterType> tokens = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
if (nextOr instanceof TokenParam) {
@ -836,14 +836,17 @@ public class SearchBuilder implements ISearchBuilder {
if (join == null) {
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;
}
List<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join);
codePredicates.addAll(singleCode);
Predicate spPredicate = myBuilder.or(toArray(codePredicates));
myPredicates.add(spPredicate);
}
@ -965,7 +968,9 @@ public class SearchBuilder implements ISearchBuilder {
}
case TOKEN: {
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;
}
case DATE: {
@ -1309,183 +1314,169 @@ public class SearchBuilder implements ISearchBuilder {
return orPredicates;
}
private Predicate createPredicateToken(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamToken> theFrom) {
String code;
String system;
private List<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamToken> theFrom) {
final List<VersionIndependentConcept> codes = new ArrayList<>();
TokenParamModifier modifier = null;
if (theParameter instanceof TokenParam) {
TokenParam id = (TokenParam) theParameter;
system = id.getSystem();
code = (id.getValue());
modifier = id.getModifier();
} else if (theParameter instanceof BaseIdentifierDt) {
BaseIdentifierDt id = (BaseIdentifierDt) theParameter;
system = id.getSystemElement().getValueAsString();
code = (id.getValueElement().getValue());
} else if (theParameter instanceof BaseCodingDt) {
BaseCodingDt id = (BaseCodingDt) theParameter;
system = id.getSystemElement().getValueAsString();
code = (id.getCodeElement().getValue());
} else if (theParameter instanceof NumberParam) {
NumberParam number = (NumberParam) theParameter;
system = null;
code = number.getValueAsQueryToken(myContext);
} else {
throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass());
for (IQueryParameterType nextParameter : theParameters) {
String code;
String system;
if (nextParameter instanceof TokenParam) {
TokenParam id = (TokenParam) nextParameter;
system = id.getSystem();
code = (id.getValue());
modifier = id.getModifier();
} else if (nextParameter instanceof BaseIdentifierDt) {
BaseIdentifierDt id = (BaseIdentifierDt) nextParameter;
system = id.getSystemElement().getValueAsString();
code = (id.getValueElement().getValue());
} else if (nextParameter instanceof BaseCodingDt) {
BaseCodingDt id = (BaseCodingDt) nextParameter;
system = id.getSystemElement().getValueAsString();
code = (id.getCodeElement().getValue());
} else if (nextParameter instanceof NumberParam) {
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) {
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)
*/
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));
}
List<VersionIndependentConcept> sortedCodesList = codes
.stream()
.filter(t -> t.getCode() != null || t.getSystem() != null)
.sorted()
.distinct()
.collect(Collectors.toList());
if (codes.isEmpty()) {
// 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) {
ArrayList<Predicate> singleCodePredicates = new ArrayList<Predicate>();
if (codes != null) {
final Path<String> systemExpression = theFrom.get("mySystem");
final Path<String> valueExpression = theFrom.get("myValue");
List<Predicate> orPredicates = new ArrayList<Predicate>();
Map<String, List<VersionIndependentConcept>> map = new HashMap<String, List<VersionIndependentConcept>>();
for (VersionIndependentConcept nextCode : codes) {
List<VersionIndependentConcept> systemCodes = map.get(nextCode.getSystem());
if (null == systemCodes) {
systemCodes = new ArrayList<>();
map.put(nextCode.getSystem(), systemCodes);
}
systemCodes.add(nextCode);
List<Predicate> orPredicates = new ArrayList<>();
switch (theTokenMode) {
case SYSTEM_ONLY: {
List<String> systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList());
Predicate orPredicate = systemExpression.in(systems);
orPredicates.add(orPredicate);
break;
}
// Use "in" in case of large numbers of codes due to param modifiers
final Path<String> systemExpression = theFrom.get("mySystem");
final Path<String> valueExpression = theFrom.get("myValue");
for (Map.Entry<String, List<VersionIndependentConcept>> entry : map.entrySet()) {
CriteriaBuilder.In<String> codePredicate = theBuilder.in(valueExpression);
boolean haveAtLeastOneCode = false;
for (VersionIndependentConcept nextCode : entry.getValue()) {
if (isNotBlank(nextCode.getCode())) {
codePredicate.value(nextCode.getCode());
haveAtLeastOneCode = true;
}
case VALUE_ONLY:
List<String> codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList());
Predicate orPredicate = valueExpression.in(codes);
orPredicates.add(orPredicate);
break;
case SYSTEM_AND_VALUE:
for (VersionIndependentConcept next : theTokens) {
orPredicates.add(theBuilder.and(
toEqualOrIsNullPredicate(systemExpression, next.getSystem()),
toEqualOrIsNullPredicate(valueExpression, next.getCode())
));
}
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")));
}
break;
}
Predicate singleCode = theBuilder.and(toArray(singleCodePredicates));
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode);
}
Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
if (theModifier == TokenParamModifier.NOT) {
or = theBuilder.not(or);
}
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or);
}
/*
* Note: A null system value means "match any system", but
* an empty-string system value means "match values that
* explicitly have no system".
*/
boolean haveSystem = codes.get(0).getSystem() != null;
boolean haveCode = isNotBlank(codes.get(0).getCode());
Expression<Long> hashField;
if (!haveSystem && !haveCode) {
// If we have neither, this isn't actually an expression so
// just return 1=1
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, true);
} else if (haveSystem && haveCode) {
hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
} else if (haveSystem) {
hashField = theFrom.get("myHashSystem").as(Long.class);
} else {
hashField = theFrom.get("myHashValue").as(Long.class);
}
List<Long> values = new ArrayList<>(codes.size());
for (VersionIndependentConcept next : codes) {
if (haveSystem && haveCode) {
values.add(ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, next.getSystem(), next.getCode()));
} else if (haveSystem) {
values.add(ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, next.getSystem()));
} else {
values.add(ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, next.getCode()));
}
List<Long> values;
switch (theTokenMode) {
case SYSTEM_ONLY:
hashField = theFrom.get("myHashSystem").as(Long.class);
values = theTokens
.stream()
.map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, t.getSystem()))
.collect(Collectors.toList());
break;
case VALUE_ONLY:
hashField = theFrom.get("myHashValue").as(Long.class);
values = theTokens
.stream()
.map(t -> ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, t.getCode()))
.collect(Collectors.toList());
break;
case SYSTEM_AND_VALUE:
default:
hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
values = theTokens
.stream()
.map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, t.getSystem(), t.getCode()))
.collect(Collectors.toList());
break;
}
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 disjunctionPredicate = theBuilder.not(predicate);
predicate = theBuilder.and(identityPredicate, disjunctionPredicate);
@ -1493,6 +1484,13 @@ public class SearchBuilder implements ISearchBuilder {
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
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid) {
myParams = theParams;
@ -1966,8 +1964,6 @@ public class SearchBuilder implements ISearchBuilder {
return tagMap;
}
private static final int maxLoad = 800;
@Override
public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation,
EntityManager entityManager, FhirContext context, IDao theDao) {
@ -2319,6 +2315,35 @@ public class SearchBuilder implements ISearchBuilder {
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 {
UNIQUE_INDEX, STANDARD_QUERY
}
@ -2673,29 +2698,6 @@ public class SearchBuilder implements ISearchBuilder {
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) {
if (thePids.isEmpty()) {
return Collections.emptyList();

View File

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

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term;
* 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.
@ -20,42 +20,65 @@ package ca.uhn.fhir.jpa.term;
* #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;
private String myCode;
public class VersionIndependentConcept implements Comparable<VersionIndependentConcept> {
/**
* Constructor
*/
public VersionIndependentConcept() {
super();
}
private final String mySystem;
private final String myCode;
private int myHashCode;
/**
* Constructor
*/
public VersionIndependentConcept(String theSystem, String theCode) {
setSystem(theSystem);
setCode(theCode);
mySystem = theSystem;
myCode = theCode;
myHashCode = new HashCodeBuilder(17, 37)
.append(mySystem)
.append(myCode)
.toHashCode();
}
public String getSystem() {
return mySystem;
}
public VersionIndependentConcept setSystem(String theSystem) {
mySystem = theSystem;
return this;
}
public String getCode() {
return myCode;
}
public VersionIndependentConcept setCode(String theCode) {
myCode = theCode;
return this;
@Override
public boolean equals(Object theO) {
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);
while (retVal.contains("?") && nextParams.size() > 0) {
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.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;

View File

@ -2030,7 +2030,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
}
@Test
public void testSearchStringParamWithLike() throws Exception {
public void testSearchStringParamWithLike() {
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_FAMILY, new StringOrListParam().addOr(new StringParam("AAA")).addOr(new StringParam("BBB")));
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
public void testSearchStringParam() throws Exception {
IIdType pid1;

View File

@ -1,9 +1,10 @@
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.model.entity.*;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
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.model.api.Include;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
@ -39,6 +40,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.*;
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
public void testHasParameter() {
IIdType pid0;
@ -616,7 +663,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
expect1.setResource(resource);
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");
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1);
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);
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val);
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");
@ -2245,18 +2292,25 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
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.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
myPatientDao.create(patient, mySrd);
{
// Match system="urn:system" and value = *
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null));
IBundleProvider retrieved = myPatientDao.search(map);
assertEquals(2, retrieved.size().intValue());
}
{
// Match system="urn:system" and value = ""
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", ""));
IBundleProvider retrieved = myPatientDao.search(map);
@ -3288,7 +3342,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
"Observation/YES21",
"Observation/YES22",
"Observation/YES23"
));
));
}
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
building Parameters resources in a version-independent way.
</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 version="3.7.0" date="2019-02-06" description="Gale">
<action type="add">