Skip to content

Commit 577bcf0

Browse files
Merge cebf9b5 into 4482622
2 parents 4482622 + cebf9b5 commit 577bcf0

File tree

2 files changed

+87
-44
lines changed

2 files changed

+87
-44
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: perf
3+
issue: 4915
4+
title: "Includes by canonical url now use an indexed query, and are much faster."

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
4949
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
5050
import ca.uhn.fhir.jpa.model.dao.JpaPid;
51+
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
5152
import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
5253
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
5354
import ca.uhn.fhir.jpa.model.search.SearchBuilderLoadIncludesParameters;
@@ -97,6 +98,7 @@
9798
import org.apache.commons.lang3.StringUtils;
9899
import org.apache.commons.lang3.Validate;
99100
import org.apache.commons.lang3.math.NumberUtils;
101+
import org.apache.commons.lang3.tuple.Pair;
100102
import org.hl7.fhir.instance.model.api.IAnyResource;
101103
import org.hl7.fhir.instance.model.api.IBaseResource;
102104
import org.slf4j.Logger;
@@ -107,6 +109,7 @@
107109
import org.springframework.transaction.support.TransactionSynchronizationManager;
108110

109111
import javax.annotation.Nonnull;
112+
import javax.annotation.Nullable;
110113
import javax.persistence.EntityManager;
111114
import javax.persistence.PersistenceContext;
112115
import javax.persistence.PersistenceContextType;
@@ -1305,73 +1308,53 @@ public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theP
13051308
paths = param.getPathsSplitForResourceType(resType);
13061309
// end replace
13071310

1308-
String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
1311+
Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
1312+
13091313
for (String nextPath : paths) {
1310-
boolean haveTargetTypesDefinedByParam = param.hasTargets();
13111314
String findPidFieldSqlColumn = findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) ? "src_resource_id" : "target_resource_id";
13121315
String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
13131316
if (findVersionFieldName != null) {
13141317
fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
13151318
}
1316-
1317-
// Query for includes lookup has consider 2 cases
1319+
1320+
// Query for includes lookup has 2 cases
13181321
// Case 1: Where target_resource_id is available in hfj_res_link table for local references
13191322
// Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical url in target_resource_url
13201323

13211324
// Case 1:
1325+
Map<String, Object> localReferenceQueryParams = new HashMap<>();
1326+
13221327
String searchPidFieldSqlColumn = searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) ? "target_resource_id" : "src_resource_id";
1323-
StringBuilder resourceIdBasedQuery = new StringBuilder("SELECT " + fieldsToLoad +
1328+
StringBuilder localReferenceQuery = new StringBuilder("SELECT " + fieldsToLoad +
13241329
" FROM hfj_res_link r " +
13251330
" WHERE r.src_path = :src_path AND " +
13261331
" r.target_resource_id IS NOT NULL AND " +
13271332
" r." + searchPidFieldSqlColumn + " IN (:target_pids) ");
1328-
if (targetResourceType != null) {
1329-
resourceIdBasedQuery.append(" AND r.target_resource_type = :target_resource_type ");
1330-
} else if (haveTargetTypesDefinedByParam) {
1331-
resourceIdBasedQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
1332-
}
1333-
1334-
// Case 2:
1335-
String fieldsToLoadFromSpidxUriTable = "rUri.res_id";
1336-
// to match the fields loaded in union
1337-
if (fieldsToLoad.split(",").length > 1) {
1338-
for (int i = 0; i < fieldsToLoad.split(",").length - 1; i++) {
1339-
fieldsToLoadFromSpidxUriTable += ", NULL";
1333+
localReferenceQueryParams.put("src_path", nextPath);
1334+
// we loop over target_pids later.
1335+
if (targetResourceTypes != null) {
1336+
if (targetResourceTypes.size() == 1) {
1337+
localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type ");
1338+
localReferenceQueryParams.put("target_resource_type", targetResourceTypes.iterator().next());
1339+
} else {
1340+
localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
1341+
localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
13401342
}
13411343
}
1342-
//@formatter:off
1343-
StringBuilder resourceUrlBasedQuery = new StringBuilder("SELECT " + fieldsToLoadFromSpidxUriTable +
1344-
" FROM hfj_res_link r " +
1345-
" JOIN hfj_spidx_uri rUri ON ( " +
1346-
" r.target_resource_url = rUri.sp_uri AND " +
1347-
" rUri.sp_name = 'url' ");
1348-
1349-
if (targetResourceType != null) {
1350-
resourceUrlBasedQuery.append(" AND rUri.res_type = :target_resource_type ");
13511344

1352-
} else if (haveTargetTypesDefinedByParam) {
1353-
resourceUrlBasedQuery.append(" AND rUri.res_type IN (:target_resource_types) ");
1354-
}
1345+
// Case 2:
1346+
Pair<String, Map<String, Object>> canonicalQuery = buildCanonicalUrlQuery(findVersionFieldName, searchPidFieldSqlColumn, targetResourceTypes);
13551347

1356-
resourceUrlBasedQuery.append(" ) ");
1357-
resourceUrlBasedQuery.append(
1358-
" WHERE r.src_path = :src_path AND " +
1359-
" r.target_resource_id IS NULL AND " +
1360-
" r." + searchPidFieldSqlColumn + " IN (:target_pids) ");
13611348
//@formatter:on
13621349

1363-
String sql = resourceIdBasedQuery + " UNION " + resourceUrlBasedQuery;
1350+
String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft();
13641351

13651352
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
13661353
for (Collection<JpaPid> nextPartition : partitions) {
13671354
Query q = entityManager.createNativeQuery(sql, Tuple.class);
1368-
q.setParameter("src_path", nextPath);
13691355
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
1370-
if (targetResourceType != null) {
1371-
q.setParameter("target_resource_type", targetResourceType);
1372-
} else if (haveTargetTypesDefinedByParam) {
1373-
q.setParameter("target_resource_types", param.getTargets());
1374-
}
1356+
localReferenceQueryParams.forEach(q::setParameter);
1357+
canonicalQuery.getRight().forEach(q::setParameter);
13751358

13761359
if (maxCount != null) {
13771360
q.setMaxResults(maxCount);
@@ -1395,7 +1378,7 @@ public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theP
13951378

13961379
nextRoundMatches.clear();
13971380
for (JpaPid next : pidsToInclude) {
1398-
if (original.contains(next) == false && allAdded.contains(next) == false) {
1381+
if ( !original.contains(next) && !allAdded.contains(next) ) {
13991382
nextRoundMatches.add(next);
14001383
}
14011384
}
@@ -1406,7 +1389,7 @@ public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theP
14061389
break;
14071390
}
14081391

1409-
} while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound);
1392+
} while (!includes.isEmpty() && !nextRoundMatches.isEmpty() && addedSomeThisRound);
14101393

14111394
allAdded.removeAll(original);
14121395

@@ -1415,7 +1398,7 @@ public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theP
14151398
// Interceptor call: STORAGE_PREACCESS_RESOURCES
14161399
// This can be used to remove results from the search result details before
14171400
// the user has a chance to know that they were in the results
1418-
if (allAdded.size() > 0) {
1401+
if (!allAdded.isEmpty()) {
14191402

14201403
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, myInterceptorBroadcaster, request)) {
14211404
List<JpaPid> includedPidList = new ArrayList<>(allAdded);
@@ -1440,6 +1423,62 @@ public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theP
14401423
return allAdded;
14411424
}
14421425

1426+
@Nullable
1427+
private static Set<String> computeTargetResourceTypes(Include nextInclude, RuntimeSearchParam param) {
1428+
String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
1429+
boolean haveTargetTypesDefinedByParam = param.hasTargets();
1430+
Set<String> targetResourceTypes;
1431+
if (targetResourceType != null) {
1432+
targetResourceTypes = Set.of(targetResourceType);
1433+
} else if (haveTargetTypesDefinedByParam) {
1434+
targetResourceTypes = param.getTargets();
1435+
} else {
1436+
// all types!
1437+
targetResourceTypes = null;
1438+
}
1439+
return targetResourceTypes;
1440+
}
1441+
1442+
@Nonnull
1443+
private Pair<String, Map<String, Object>> buildCanonicalUrlQuery(String theVersionFieldName, String thePidFieldSqlColumn, Set<String> theTargetResourceTypes) {
1444+
String fieldsToLoadFromSpidxUriTable = "rUri.res_id";
1445+
if (theVersionFieldName != null) {
1446+
// canonical-uri references aren't versioned, but we need to match the column count for the UNION
1447+
fieldsToLoadFromSpidxUriTable += ", NULL";
1448+
}
1449+
// The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url.
1450+
// But sp_name isn't indexed, so we use hash_identity instead.
1451+
if (theTargetResourceTypes == null) {
1452+
// hash_identity includes the resource type. So a null wildcard must be replaced with a list of all types.
1453+
theTargetResourceTypes = myDaoRegistry.getRegisteredDaoTypes();
1454+
}
1455+
assert !theTargetResourceTypes.isEmpty();
1456+
1457+
Set<Long> identityHashesForTypes = theTargetResourceTypes.stream()
1458+
.map(type-> BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionSettings, myRequestPartitionId, type, "url"))
1459+
.collect(Collectors.toSet());
1460+
1461+
Map<String, Object> canonicalUriQueryParams = new HashMap<>();
1462+
StringBuilder canonicalUrlQuery = new StringBuilder(
1463+
"SELECT " + fieldsToLoadFromSpidxUriTable +
1464+
" FROM hfj_res_link r " +
1465+
" JOIN hfj_spidx_uri rUri ON ( ");
1466+
// join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2
1467+
if (theTargetResourceTypes.size() == 1) {
1468+
canonicalUrlQuery.append(" rUri.hash_identity = :uri_identity_hash ");
1469+
canonicalUriQueryParams.put("uri_identity_hash", identityHashesForTypes.iterator().next());
1470+
} else {
1471+
canonicalUrlQuery.append(" rUri.hash_identity in (:uri_identity_hashes) ");
1472+
canonicalUriQueryParams.put("uri_identity_hashes", identityHashesForTypes);
1473+
}
1474+
1475+
canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri )" +
1476+
" WHERE r.src_path = :src_path AND " +
1477+
" r.target_resource_id IS NULL AND " +
1478+
" r." + thePidFieldSqlColumn + " IN (:target_pids) ");
1479+
return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams);
1480+
}
1481+
14431482
private List<Collection<JpaPid>> partition(Collection<JpaPid> theNextRoundMatches, int theMaxLoad) {
14441483
if (theNextRoundMatches.size() <= theMaxLoad) {
14451484
return Collections.singletonList(theNextRoundMatches);

0 commit comments

Comments
 (0)