Avoid redundant resource type selector in _id query (#2909)

* Fixed

* Add more tests

* Test fixes
This commit is contained in:
James Agnew 2021-08-23 09:04:51 -04:00 committed by GitHub
parent 4c2ae513d8
commit 3ebb374059
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 32 deletions

View File

@ -152,7 +152,7 @@ public class IdHelperService {
Validate.notNull(theId, "theId must not be null");
ResourcePersistentId retVal;
if (myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(theId)) {
if (idRequiresForcedId (theId)) {
if (myDaoConfig.isDeleteEnabled()) {
retVal = new ResourcePersistentId(resolveResourceIdentity(theRequestPartitionId, theResourceType, theId).getResourceId());
} else {
@ -174,6 +174,17 @@ public class IdHelperService {
return retVal;
}
/**
* Returns true if the given resource ID should be stored in a forced ID. Under default config
* (meaning client ID strategy is {@link ca.uhn.fhir.jpa.api.config.DaoConfig.ClientIdStrategyEnum#ALPHANUMERIC})
* this will return true if the ID has any non-digit characters.
*
* In {@link ca.uhn.fhir.jpa.api.config.DaoConfig.ClientIdStrategyEnum#ANY} mode it will always return true.
*/
public boolean idRequiresForcedId(String theId) {
return myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(theId);
}
@Nonnull
private String toForcedIdToPidKey(@Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theId) {
return RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + theResourceType + "/" + theId;

View File

@ -220,9 +220,14 @@ public class SearchBuilder implements ISearchBuilder {
}
SearchContainedModeEnum searchContainedMode = theParams.getSearchContainedMode();
// Handle _id last, since it can typically be tacked onto a different parameter
List<String> paramNames = myParams.keySet().stream().filter(t -> !t.equals(IAnyResource.SP_RES_ID)).collect(Collectors.toList());
if (myParams.containsKey(IAnyResource.SP_RES_ID)) {
paramNames.add(IAnyResource.SP_RES_ID);
}
// Handle each parameter
List<String> paramNames = new ArrayList<>(myParams.keySet());
for (String nextParamName : paramNames) {
if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) {
// Skip parameters for Subject, Patient, Code and Category for LastN as these will be filtered by Elasticsearch
@ -857,7 +862,7 @@ public class SearchBuilder implements ISearchBuilder {
resourceLink = (Long) ((Object[]) nextRow)[0];
version = (Long) ((Object[]) nextRow)[1];
} else {
resourceLink = (Long)nextRow;
resourceLink = (Long) nextRow;
}
pidsToInclude.add(new ResourcePersistentId(resourceLink, version));
@ -926,8 +931,8 @@ public class SearchBuilder implements ISearchBuilder {
if (resourceLink != null) {
ResourcePersistentId persistentId;
if (findVersionFieldName != null) {
persistentId = new ResourcePersistentId(((Object[])resourceLink)[0]);
persistentId.setVersion((Long) ((Object[])resourceLink)[1]);
persistentId = new ResourcePersistentId(((Object[]) resourceLink)[0]);
persistentId.setVersion((Long) ((Object[]) resourceLink)[1]);
} else {
persistentId = new ResourcePersistentId(resourceLink);
}
@ -977,7 +982,7 @@ public class SearchBuilder implements ISearchBuilder {
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = includedPidList.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {

View File

@ -82,8 +82,8 @@ public class BasePredicateBuilder {
return mySearchSqlBuilder.createConditionForValueWithComparator(theComparator, theColumn, theValue);
}
protected BaseJoiningPredicateBuilder getOrCreateQueryRootTable() {
return mySearchSqlBuilder.getOrCreateFirstPredicateBuilder();
protected BaseJoiningPredicateBuilder getOrCreateQueryRootTable(boolean theIncludeResourceTypeAndNonDeletedFlag) {
return mySearchSqlBuilder.getOrCreateFirstPredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag);
}
public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn theFromColumn, DbColumn theToColumn) {

View File

@ -65,6 +65,7 @@ public class ResourceIdPredicateBuilder extends BasePredicateBuilder {
Set<ResourcePersistentId> allOrPids = null;
SearchFilterParser.CompareOperation defaultOperation = SearchFilterParser.CompareOperation.eq;
boolean allIdsAreForcedIds = true;
for (List<? extends IQueryParameterType> nextValue : theValues) {
Set<ResourcePersistentId> orPids = new HashSet<>();
boolean haveValue = false;
@ -76,6 +77,9 @@ public class ResourceIdPredicateBuilder extends BasePredicateBuilder {
IdType valueAsId = new IdType(value);
if (isNotBlank(value)) {
if (!myIdHelperService.idRequiresForcedId(valueAsId.getIdPart()) && allIdsAreForcedIds) {
allIdsAreForcedIds = false;
}
haveValue = true;
try {
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(theRequestPartitionId, theResourceName, valueAsId.getIdPart());
@ -114,7 +118,7 @@ public class ResourceIdPredicateBuilder extends BasePredicateBuilder {
List<Long> resourceIds = ResourcePersistentId.toLongList(allOrPids);
if (theSourceJoinColumn == null) {
BaseJoiningPredicateBuilder queryRootTable = super.getOrCreateQueryRootTable();
BaseJoiningPredicateBuilder queryRootTable = super.getOrCreateQueryRootTable(!allIdsAreForcedIds);
Condition predicate;
switch (operation) {
default:

View File

@ -469,18 +469,31 @@ public class SearchQueryBuilder {
* If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it.
*/
public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder() {
return getOrCreateFirstPredicateBuilder(true);
}
/**
* If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it.
*/
public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder(boolean theIncludeResourceTypeAndNonDeletedFlag) {
if (myFirstPredicateBuilder == null) {
getOrCreateResourceTablePredicateBuilder();
getOrCreateResourceTablePredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag);
}
return myFirstPredicateBuilder;
}
public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder() {
return getOrCreateResourceTablePredicateBuilder(true);
}
public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder(boolean theIncludeResourceTypeAndNonDeletedFlag) {
if (myResourceTableRoot == null) {
ResourceTablePredicateBuilder resourceTable = mySqlBuilderFactory.resourceTable(this);
addTable(resourceTable, null);
Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates();
addPredicate(typeAndDeletionPredicate);
if (theIncludeResourceTypeAndNonDeletedFlag) {
Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates();
addPredicate(typeAndDeletionPredicate);
}
myResourceTableRoot = resourceTable;
}
return myResourceTableRoot;

View File

@ -155,12 +155,12 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
}
logAllResourceLinks();
runInTransaction(() -> {
Map<String, Integer> counts = getResourceCountMap();
assertEquals(1, counts.get("Patient"), counts.toString());
assertEquals(1, counts.get("Observation"), counts.toString());
logAllResourceLinks();
assertEquals(6, myResourceLinkDao.count());
assertEquals(6, myResourceTableDao.count());
assertEquals(14, myResourceHistoryTableDao.count());
@ -194,12 +194,12 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
}
logAllResourceLinks();
runInTransaction(() -> {
Map<String, Integer> counts = getResourceCountMap();
assertEquals(1, counts.get("Patient"), counts.toString());
assertEquals(1, counts.get("Observation"), counts.toString());
logAllResourceLinks();
assertEquals(6, myResourceLinkDao.count());
assertEquals(6, myResourceTableDao.count());
assertEquals(14, myResourceHistoryTableDao.count());

View File

@ -1475,21 +1475,28 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
id2 = myOrganizationDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getValue();
}
// FIXME: restore
int size;
SearchParameterMap params = new SearchParameterMap();
// params.setLoadSynchronous(true);
// assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(id1));
//
// params = new SearchParameterMap();
// params.add("_id", new StringParam(id1));
// assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(id1));
//
// params = new SearchParameterMap();
// params.add("_id", new StringParam("9999999999999999"));
// assertEquals(0, toList(myPatientDao.search(params)).size());
myCaptureQueriesListener.clear();
params = new SearchParameterMap();
params.setLoadSynchronous(true);
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(id1));
params = new SearchParameterMap();
params.add("_id", new StringParam(id1));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(id1));
params = new SearchParameterMap();
params.add("_id", new StringParam("9999999999999999"));
assertEquals(0, toList(myPatientDao.search(params)).size());
params = new SearchParameterMap();
params.add("_id", new StringParam(id2));
assertEquals(0, toList(myPatientDao.search(params)).size());
size = toList(myPatientDao.search(params)).size();
myCaptureQueriesListener.logAllQueries();
assertEquals(0, size);
}
@ -1596,8 +1603,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
assertEquals(1, countMatches(sqlQuery, "res_id = '123'"), sqlQuery);
assertEquals(1, countMatches(sqlQuery, "join"), sqlQuery);
assertEquals(1, countMatches(sqlQuery, "hash_sys_and_value"), sqlQuery);
assertEquals(1, countMatches(sqlQuery, "res_type = 'diagnosticreport"), sqlQuery); // could be 0
assertEquals(1, countMatches(sqlQuery, "res_deleted_at"), sqlQuery); // could be 0
assertEquals(0, countMatches(sqlQuery, "res_type = 'diagnosticreport"), sqlQuery); // could be 0
assertEquals(0, countMatches(sqlQuery, "res_deleted_at"), sqlQuery); // could be 0
}
}

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
@ -18,6 +19,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.StringUtils;
@ -26,6 +28,8 @@ import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
@ -797,6 +801,112 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals(1, StringUtils.countMatches(selectQuery, "SELECT"));
}
/**
* Make sure that if we're performing a query where the resource type is implicitly known,
* we don't include a selector for the resource type
*
* This test is for queries with _id where the ID is a forced ID
*/
@Test
public void testSearchOnIdAndReference_SearchById() {
Patient p = new Patient();
p.setId("B");
myPatientDao.update(p);
Observation obs = new Observation();
obs.setId("A");
obs.setSubject(new Reference("Patient/B"));
obs.setStatus(Observation.ObservationStatus.FINAL);
myObservationDao.update(obs);
Observation obs2 = new Observation();
obs2.setSubject(new Reference("Patient/B"));
obs2.setStatus(Observation.ObservationStatus.FINAL);
String obs2id = myObservationDao.create(obs2).getId().getIdPart();
assertThat(obs2id, matchesPattern("^[0-9]+$"));
// Search by ID where all IDs are forced IDs
{
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("_id", new TokenParam("A"));
map.add("subject", new ReferenceParam("Patient/B"));
map.add("status", new TokenParam("final"));
myCaptureQueriesListener.clear();
IBundleProvider outcome = myObservationDao.search(map, new SystemRequestDetails());
assertEquals(1, outcome.getResources(0, 999).size());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
String selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "forcedid0_.resource_type='observation'"), selectQuery);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "forcedid0_.forced_id in ('a')"), selectQuery);
selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "select t1.res_id from hfj_resource t1"), selectQuery);
assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), "t1.res_type = 'observation'"), selectQuery);
assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), "t1.res_deleted_at is null"), selectQuery);
}
// Search by ID where at least one ID is a numeric ID
{
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("_id", new TokenOrListParam(null, "A", obs2id));
myCaptureQueriesListener.clear();
IBundleProvider outcome = myObservationDao.search(map, new SystemRequestDetails());
assertEquals(2, outcome.size());
assertEquals(2, outcome.getResources(0, 999).size());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
String selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "select t0.res_id from hfj_resource t0"), selectQuery);
// Because we included a non-forced ID, we need to verify the type
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_type = 'observation'"), selectQuery);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_deleted_at is null"), selectQuery);
}
// Delete the resource - The searches should generate similar SQL now, but
// not actually return the result
myObservationDao.delete(new IdType("Observation/A"));
myObservationDao.delete(new IdType("Observation/" + obs2id));
// Search by ID where all IDs are forced IDs
{
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("_id", new TokenParam("A"));
myCaptureQueriesListener.clear();
IBundleProvider outcome = myObservationDao.search(map, new SystemRequestDetails());
assertEquals(0, outcome.size());
assertEquals(0, outcome.getResources(0, 999).size());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
String selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "forcedid0_.resource_type='observation'"), selectQuery);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "forcedid0_.forced_id in ('a')"), selectQuery);
selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "select t0.res_id from hfj_resource t0"), selectQuery);
assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_type = 'observation'"), selectQuery);
assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_deleted_at is null"), selectQuery);
}
// Search by ID where at least one ID is a numeric ID
{
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("_id", new TokenOrListParam(null, "A", obs2id));
myCaptureQueriesListener.clear();
IBundleProvider outcome = myObservationDao.search(map, new SystemRequestDetails());
assertEquals(0, outcome.size());
assertEquals(0, outcome.getResources(0, 999).size());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
String selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "select t0.res_id from hfj_resource t0"), selectQuery);
// Because we included a non-forced ID, we need to verify the type
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_type = 'observation'"), selectQuery);
assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_deleted_at is null"), selectQuery);
}
}
@AfterEach
public void afterResetDao() {
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());

View File

@ -343,8 +343,8 @@ public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest {
.stream()
.collect(MultimapCollector.toMultimap(t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getResourceType(), t -> new IdType(t.getResponse().getLocation()).toUnqualifiedVersionless().getValue()));
logAllResources();
Multimap<String, Integer> resourcesByType = runInTransaction(() -> {
logAllResources();
return myResourceTableDao.findAll().stream().collect(MultimapCollector.toMultimap(t -> t.getResourceType(), t -> t.getPartitionId().getPartitionId()));
});
@ -378,8 +378,8 @@ public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest {
String patientId = resourceIds.get("Patient").get(0);
logAllResources();
Multimap<String, Integer> resourcesByType = runInTransaction(() -> {
logAllResources();
return myResourceTableDao.findAll().stream().collect(MultimapCollector.toMultimap(t -> t.getResourceType(), t -> t.getPartitionId().getPartitionId()));
});
@ -426,8 +426,8 @@ public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest {
String patientId = resourceIds.get("Patient").get(0);
logAllResources();
Multimap<String, Integer> resourcesByType = runInTransaction(() -> {
logAllResources();
return myResourceTableDao.findAll().stream().collect(MultimapCollector.toMultimap(t -> t.getResourceType(), t -> t.getPartitionId().getPartitionId()));
});