Collapse date joins (#1726)
* Collapse date joins * Add changelog * Address review comments * Add whitespace * fix test Co-authored-by: Ken Stevens <ken.stevens@sympatico.ca>
This commit is contained in:
parent
f1646fd184
commit
b2b0ff22b4
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
type: perf
|
||||||
|
issue: 1726
|
||||||
|
title: When performing date range searches in the JPA server, the server was generating extra
|
||||||
|
unneccessary joins in the generated SQL. This has been streamlined, which should result in
|
||||||
|
faster searches when performing date ranges.
|
|
@ -39,13 +39,17 @@ import javax.persistence.criteria.Join;
|
||||||
import javax.persistence.criteria.Predicate;
|
import javax.persistence.criteria.Predicate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Scope("prototype")
|
@Scope("prototype")
|
||||||
public class PredicateBuilderDate extends BasePredicateBuilder implements IPredicateBuilder {
|
public class PredicateBuilderDate extends BasePredicateBuilder implements IPredicateBuilder {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class);
|
||||||
|
|
||||||
|
private Map<String, Join<ResourceTable, ResourceIndexedSearchParamDate>> myJoinMap;
|
||||||
|
|
||||||
PredicateBuilderDate(SearchBuilder theSearchBuilder) {
|
PredicateBuilderDate(SearchBuilder theSearchBuilder) {
|
||||||
super(theSearchBuilder);
|
super(theSearchBuilder);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +60,18 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
||||||
List<? extends IQueryParameterType> theList,
|
List<? extends IQueryParameterType> theList,
|
||||||
SearchFilterParser.CompareOperation operation) {
|
SearchFilterParser.CompareOperation operation) {
|
||||||
|
|
||||||
Join<ResourceTable, ResourceIndexedSearchParamDate> join = createJoin(SearchBuilderJoinEnum.DATE, theParamName);
|
boolean newJoin = false;
|
||||||
|
if (myJoinMap == null) {
|
||||||
|
myJoinMap = new HashMap<>();
|
||||||
|
}
|
||||||
|
String key = theResourceName + " " + theParamName;
|
||||||
|
|
||||||
|
Join<ResourceTable, ResourceIndexedSearchParamDate> join = myJoinMap.get(key);
|
||||||
|
if (join == null) {
|
||||||
|
join = createJoin(SearchBuilderJoinEnum.DATE, theParamName);
|
||||||
|
myJoinMap.put(key, join);
|
||||||
|
newJoin = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (theList.get(0).getMissing() != null) {
|
if (theList.get(0).getMissing() != null) {
|
||||||
Boolean missing = theList.get(0).getMissing();
|
Boolean missing = theList.get(0).getMissing();
|
||||||
|
@ -77,7 +92,14 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate orPredicates = myBuilder.or(toArray(codePredicates));
|
Predicate orPredicates = myBuilder.or(toArray(codePredicates));
|
||||||
|
|
||||||
|
if (newJoin) {
|
||||||
|
Predicate identityAndValuePredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, orPredicates);
|
||||||
|
myQueryRoot.addPredicate(identityAndValuePredicate);
|
||||||
|
} else {
|
||||||
myQueryRoot.addPredicate(orPredicates);
|
myQueryRoot.addPredicate(orPredicates);
|
||||||
|
}
|
||||||
|
|
||||||
return orPredicates;
|
return orPredicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +108,13 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
||||||
String theParamName,
|
String theParamName,
|
||||||
CriteriaBuilder theBuilder,
|
CriteriaBuilder theBuilder,
|
||||||
From<?, ResourceIndexedSearchParamDate> theFrom) {
|
From<?, ResourceIndexedSearchParamDate> theFrom) {
|
||||||
return createPredicateDate(theParam,
|
Predicate predicateDate = createPredicateDate(theParam,
|
||||||
theResourceName,
|
theResourceName,
|
||||||
theParamName,
|
theParamName,
|
||||||
theBuilder,
|
theBuilder,
|
||||||
theFrom,
|
theFrom,
|
||||||
null);
|
null);
|
||||||
|
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, predicateDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate createPredicateDate(IQueryParameterType theParam,
|
private Predicate createPredicateDate(IQueryParameterType theParam,
|
||||||
|
@ -99,7 +122,7 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
||||||
String theParamName,
|
String theParamName,
|
||||||
CriteriaBuilder theBuilder,
|
CriteriaBuilder theBuilder,
|
||||||
From<?, ResourceIndexedSearchParamDate> theFrom,
|
From<?, ResourceIndexedSearchParamDate> theFrom,
|
||||||
SearchFilterParser.CompareOperation operation) {
|
SearchFilterParser.CompareOperation theOperation) {
|
||||||
|
|
||||||
Predicate p;
|
Predicate p;
|
||||||
if (theParam instanceof DateParam) {
|
if (theParam instanceof DateParam) {
|
||||||
|
@ -109,7 +132,7 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
||||||
p = createPredicateDateFromRange(theBuilder,
|
p = createPredicateDateFromRange(theBuilder,
|
||||||
theFrom,
|
theFrom,
|
||||||
range,
|
range,
|
||||||
operation);
|
theOperation);
|
||||||
} else {
|
} else {
|
||||||
// TODO: handle missing date param?
|
// TODO: handle missing date param?
|
||||||
p = null;
|
p = null;
|
||||||
|
@ -119,12 +142,12 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
||||||
p = createPredicateDateFromRange(theBuilder,
|
p = createPredicateDateFromRange(theBuilder,
|
||||||
theFrom,
|
theFrom,
|
||||||
range,
|
range,
|
||||||
operation);
|
theOperation);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Invalid token type: " + theParam.getClass());
|
throw new IllegalArgumentException("Invalid token type: " + theParam.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, p);
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,
|
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,
|
||||||
|
|
|
@ -3258,6 +3258,105 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchWithDateAndReusesExistingJoin() {
|
||||||
|
// Add a search parameter to Observation.issued, so that between that one
|
||||||
|
// and the existing one on Observation.effective, we have 2 date search parameters
|
||||||
|
// on the same resource
|
||||||
|
{
|
||||||
|
SearchParameter sp = new SearchParameter();
|
||||||
|
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
sp.setType(Enumerations.SearchParamType.DATE);
|
||||||
|
sp.setCode("issued");
|
||||||
|
sp.setExpression("Observation.issued");
|
||||||
|
mySearchParameterDao.create(sp);
|
||||||
|
mySearchParamRegistry.forceRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dates are reversed on these two observations
|
||||||
|
IIdType obsId1;
|
||||||
|
{
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.setIssuedElement(new InstantType("2020-06-06T12:00:00Z"));
|
||||||
|
obs.setEffective(new InstantType("2019-06-06T12:00:00Z"));
|
||||||
|
obsId1 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
IIdType obsId2;
|
||||||
|
{
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.setIssuedElement(new InstantType("2019-06-06T12:00:00Z"));
|
||||||
|
obs.setEffective(new InstantType("2020-06-06T12:00:00Z"));
|
||||||
|
obsId2 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add two with a period
|
||||||
|
IIdType obsId3;
|
||||||
|
{
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.setEffective(new Period().setStartElement(new DateTimeType("2000-06-06T12:00:00Z")).setEndElement(new DateTimeType("2001-06-06T12:00:00Z")));
|
||||||
|
obsId3 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
IIdType obsId4;
|
||||||
|
{
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.setEffective(new Period().setStartElement(new DateTimeType("2001-01-01T12:00:00Z")).setEndElement(new DateTimeType("2002-01-01T12:00:00Z")));
|
||||||
|
obsId4 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two AND instances of 1 SP
|
||||||
|
{
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
|
params.setLoadSynchronous(true);
|
||||||
|
params.add("issued", new DateParam("ge2020-06-05"));
|
||||||
|
params.add("issued", new DateParam("lt2020-06-07"));
|
||||||
|
List<IIdType> patients = toUnqualifiedVersionlessIds(myObservationDao.search(params));
|
||||||
|
assertThat(patients.toString(), patients, contains(obsId1));
|
||||||
|
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||||
|
ourLog.info("Search query:\n{}", searchQuery);
|
||||||
|
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "join"));
|
||||||
|
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "hash_identity"));
|
||||||
|
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toLowerCase(), "sp_value_low"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two AND instances of 1 SP and 1 instance of another
|
||||||
|
{
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
|
params.setLoadSynchronous(true);
|
||||||
|
params.add("issued", new DateParam("ge2020-06-05"));
|
||||||
|
params.add("issued", new DateParam("lt2020-06-07"));
|
||||||
|
params.add("date", new DateParam("gt2019-06-05"));
|
||||||
|
params.add("date", new DateParam("lt2019-06-07"));
|
||||||
|
List<IIdType> patients = toUnqualifiedVersionlessIds(myObservationDao.search(params));
|
||||||
|
assertThat(patients.toString(), patients, contains(obsId1));
|
||||||
|
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||||
|
ourLog.info("Search query:\n{}", searchQuery);
|
||||||
|
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toLowerCase(), "join"));
|
||||||
|
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toLowerCase(), "hash_identity"));
|
||||||
|
assertEquals(searchQuery, 4, StringUtils.countMatches(searchQuery.toLowerCase(), "sp_value_low"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Period search
|
||||||
|
{
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
|
params.setLoadSynchronous(true);
|
||||||
|
params.add("date", new DateParam("lt2002-01-01T12:00:00Z"));
|
||||||
|
List<IIdType> patients = toUnqualifiedVersionlessIds(myObservationDao.search(params));
|
||||||
|
assertThat(patients.toString(), patients, containsInAnyOrder(obsId3, obsId4));
|
||||||
|
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||||
|
ourLog.info("Search query:\n{}", searchQuery);
|
||||||
|
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "join"));
|
||||||
|
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "hash_identity"));
|
||||||
|
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "sp_value_low"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearchWithFetchSizeDefaultMaximum() {
|
public void testSearchWithFetchSizeDefaultMaximum() {
|
||||||
myDaoConfig.setFetchSizeDefaultMaximum(5);
|
myDaoConfig.setFetchSizeDefaultMaximum(5);
|
||||||
|
|
|
@ -21,16 +21,16 @@ package ca.uhn.fhir.jpa.migrate.tasks.api;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
|
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
|
||||||
import ca.uhn.fhir.jpa.migrate.tasks.SchemaInitializationProvider;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface ISchemaInitializationProvider {
|
public interface ISchemaInitializationProvider {
|
||||||
|
|
||||||
List<String> getSqlStatements(DriverTypeEnum theDriverType);
|
List<String> getSqlStatements(DriverTypeEnum theDriverType);
|
||||||
|
|
||||||
String getSchemaExistsIndicatorTable();
|
String getSchemaExistsIndicatorTable();
|
||||||
|
|
||||||
String getSchemaDescription();
|
String getSchemaDescription();
|
||||||
|
|
||||||
SchemaInitializationProvider setSchemaDescription(String theSchemaDescription);
|
ISchemaInitializationProvider setSchemaDescription(String theSchemaDescription);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,11 @@ public class InitializeSchemaTaskTest extends BaseTest {
|
||||||
return "TEST";
|
return "TEST";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ISchemaInitializationProvider setSchemaDescription(String theSchemaDescription) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object theO) {
|
public boolean equals(Object theO) {
|
||||||
if (this == theO) return true;
|
if (this == theO) return true;
|
||||||
|
|
Loading…
Reference in New Issue