Ja inline references (#1252)
* Use a single IN clause for OR reference searches * Add changelog * One more attempt at getting the free port util to actually work... sigh * One more tweak to free ports * Bundle fix
This commit is contained in:
parent
9a54d7086e
commit
e85bec2858
|
@ -24,10 +24,12 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
@ -38,38 +40,38 @@ import java.util.List;
|
|||
* for a long time (potentially lots of them!) and will leave your system low on
|
||||
* ports if you put it into production.
|
||||
* </b></p>
|
||||
*
|
||||
* <p>
|
||||
* How it works:
|
||||
*
|
||||
* <p>
|
||||
* We have lots of tests that need a free port because they want to open up
|
||||
* a server, and need the port to be unique and unused so that the tests can
|
||||
* run multithreaded. This turns out to just be an awful problem to solve for
|
||||
* lots of reasons:
|
||||
*
|
||||
* <p>
|
||||
* 1. You can request a free port from the OS by calling <code>new ServerSocket(0);</code>
|
||||
* and this seems to work 99% of the time, but occasionally on a heavily loaded
|
||||
* server if two processes ask at the exact same time they will receive the
|
||||
* same port assignment, and one will fail.
|
||||
* and this seems to work 99% of the time, but occasionally on a heavily loaded
|
||||
* server if two processes ask at the exact same time they will receive the
|
||||
* same port assignment, and one will fail.
|
||||
* 2. Tests run in separate processes, so we can't just rely on keeping a collection
|
||||
* of assigned ports or anything like that.
|
||||
*
|
||||
* of assigned ports or anything like that.
|
||||
* <p>
|
||||
* So we solve this like this:
|
||||
*
|
||||
* <p>
|
||||
* At random, this class will pick a "control port" and bind it. A control port
|
||||
* is just a randomly chosen port that is a multiple of 100. If we can bind
|
||||
* successfully to that port, we now own the range of "n+1 to n+99". If we can't
|
||||
* bind that port, it means some other process has probably taken it so
|
||||
* we'll just try again until we find an available control port.
|
||||
*
|
||||
* <p>
|
||||
* Assuming we successfully bind a control port, we'll give out any available
|
||||
* ports in the range "n+1 to n+99" until we've exhausted the whole set, and
|
||||
* then we'll pick another control port (if we actually get asked for over
|
||||
* 100 ports.. this should be a rare event).
|
||||
*
|
||||
* <p>
|
||||
* This mechanism has the benefit of (fingers crossed) being bulletproof
|
||||
* in terms of its ability to give out ports that are actually free, thereby
|
||||
* preventing random test failures.
|
||||
*
|
||||
* <p>
|
||||
* This mechanism has the drawback of never giving up a control port once
|
||||
* it has assigned one. To be clear, this class is deliberately leaking
|
||||
* resources. Again, no production use!
|
||||
|
@ -106,6 +108,32 @@ public class PortUtil {
|
|||
myCurrentControlSocketPort = null;
|
||||
}
|
||||
|
||||
private static boolean isAvailable(int port) {
|
||||
ServerSocket ss = null;
|
||||
DatagramSocket ds = null;
|
||||
try {
|
||||
ss = new ServerSocket(port);
|
||||
ss.setReuseAddress(true);
|
||||
ds = new DatagramSocket(port);
|
||||
ds.setReuseAddress(true);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (ds != null) {
|
||||
ds.close();
|
||||
}
|
||||
|
||||
if (ss != null) {
|
||||
try {
|
||||
ss.close();
|
||||
} catch (IOException e) {
|
||||
/* should not be thrown */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and release all control sockets
|
||||
*/
|
||||
|
@ -149,48 +177,54 @@ public class PortUtil {
|
|||
int nextCandidatePort = myCurrentControlSocketPort + myCurrentOffset;
|
||||
|
||||
// Try to open a port on this socket and use it
|
||||
try (ServerSocket server = new ServerSocket()) {
|
||||
server.setReuseAddress(true);
|
||||
server.bind(new InetSocketAddress("localhost", nextCandidatePort));
|
||||
try (Socket client = new Socket()) {
|
||||
client.setReuseAddress(true);
|
||||
client.connect(new InetSocketAddress("localhost", nextCandidatePort));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// try (ServerSocket server = new ServerSocket()) {
|
||||
// server.setReuseAddress(true);
|
||||
// server.bind(new InetSocketAddress("localhost", nextCandidatePort));
|
||||
// try (Socket client = new Socket()) {
|
||||
// client.setReuseAddress(true);
|
||||
// client.connect(new InetSocketAddress("localhost", nextCandidatePort));
|
||||
// }
|
||||
// } catch (IOException e) {
|
||||
// continue;
|
||||
// }
|
||||
if (!isAvailable(nextCandidatePort)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Log who asked for the port, just in case that's useful
|
||||
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
|
||||
StackTraceElement previousElement = stackTraceElements[2];
|
||||
StackTraceElement previousElement = Arrays.stream(stackTraceElements)
|
||||
.filter(t -> !t.toString().contains("PortUtil.") && !t.toString().contains("getStackTrace"))
|
||||
.findFirst()
|
||||
.orElse(stackTraceElements[2]);
|
||||
ourLog.info("Returned available port {} for: {}", nextCandidatePort, previousElement.toString());
|
||||
|
||||
/*
|
||||
* This is an attempt to make sure the port is actually
|
||||
* free before releasing it. For whatever reason on Linux
|
||||
* it seems like even after we close the ServerSocket there
|
||||
* is a short while where it is not possible to bind the
|
||||
* port, even though it should be released by then.
|
||||
*
|
||||
* I don't have any solid evidence that this is a good
|
||||
* way to do this, but it seems to help...
|
||||
*/
|
||||
for (int i = 0; i < 10; i++) {
|
||||
try (Socket client = new Socket()) {
|
||||
client.setReuseAddress(true);
|
||||
client.connect(new InetSocketAddress(nextCandidatePort), 1000);
|
||||
ourLog.info("Socket still seems open");
|
||||
Thread.sleep(250);
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException theE) {
|
||||
// ignore
|
||||
}
|
||||
// /*
|
||||
// * This is an attempt to make sure the port is actually
|
||||
// * free before releasing it. For whatever reason on Linux
|
||||
// * it seems like even after we close the ServerSocket there
|
||||
// * is a short while where it is not possible to bind the
|
||||
// * port, even though it should be released by then.
|
||||
// *
|
||||
// * I don't have any solid evidence that this is a good
|
||||
// * way to do this, but it seems to help...
|
||||
// */
|
||||
// for (int i = 0; i < 10; i++) {
|
||||
// try (Socket client = new Socket()) {
|
||||
// client.setReuseAddress(true);
|
||||
// client.connect(new InetSocketAddress(nextCandidatePort), 1000);
|
||||
// ourLog.info("Socket still seems open");
|
||||
// Thread.sleep(250);
|
||||
// } catch (Exception e) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// Thread.sleep(250);
|
||||
// } catch (InterruptedException theE) {
|
||||
// // ignore
|
||||
// }
|
||||
|
||||
return nextCandidatePort;
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ public class PortUtilTest {
|
|||
|
||||
int tasksCount = 20;
|
||||
ExecutorService pool = Executors.newFixedThreadPool(tasksCount);
|
||||
int portsPerTaskCount = 51;
|
||||
int portsPerTaskCount = 151;
|
||||
for (int i = 0; i < tasksCount; i++) {
|
||||
pool.submit(() -> {
|
||||
PortUtil portUtil = new PortUtil();
|
||||
|
|
|
@ -1002,6 +1002,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
throw new ResourceNotFoundException(theId);
|
||||
}
|
||||
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
|
||||
entity.setTransientForcedId(theId.getIdPart());
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -391,7 +391,8 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
|
||||
Join<ResourceTable, ResourceLink> join = createJoin(JoinEnum.REFERENCE, theParamName);
|
||||
|
||||
List<Predicate> codePredicates = new ArrayList<>();
|
||||
List<IIdType> targetIds = new ArrayList<>();
|
||||
List<String> targetQualifiedUrls = new ArrayList<>();
|
||||
|
||||
for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
|
||||
IQueryParameterType nextOr = theList.get(orIdx);
|
||||
|
@ -400,173 +401,31 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
ReferenceParam ref = (ReferenceParam) nextOr;
|
||||
|
||||
if (isBlank(ref.getChain())) {
|
||||
|
||||
/*
|
||||
* Handle non-chained search, e.g. Patient?organization=Organization/123
|
||||
*/
|
||||
|
||||
IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
|
||||
|
||||
if (dt.hasBaseUrl()) {
|
||||
if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
|
||||
dt = dt.toUnqualified();
|
||||
targetIds.add(dt);
|
||||
} else {
|
||||
ourLog.debug("Searching for resource link with target URL: {}", dt.getValue());
|
||||
Predicate eq = myBuilder.equal(join.get("myTargetResourceUrl"), dt.getValue());
|
||||
codePredicates.add(eq);
|
||||
continue;
|
||||
targetQualifiedUrls.add(dt.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
List<Long> targetPid;
|
||||
try {
|
||||
targetPid = myIdHelperService.translateForcedIdToPids(dt);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
// Use a PID that will never exist
|
||||
targetPid = Collections.singletonList(-1L);
|
||||
}
|
||||
for (Long next : targetPid) {
|
||||
ourLog.debug("Searching for resource link with target PID: {}", next);
|
||||
|
||||
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
|
||||
Predicate pidPredicate = myBuilder.equal(join.get("myTargetResourcePid"), next);
|
||||
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
|
||||
} else {
|
||||
targetIds.add(dt);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
final List<Class<? extends IBaseResource>> resourceTypes;
|
||||
String resourceId;
|
||||
if (!ref.getValue().matches("[a-zA-Z]+/.*")) {
|
||||
/*
|
||||
* Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
|
||||
*/
|
||||
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
resourceTypes = new ArrayList<>();
|
||||
|
||||
Set<String> targetTypes = param.getTargets();
|
||||
|
||||
if (targetTypes != null && !targetTypes.isEmpty()) {
|
||||
for (String next : targetTypes) {
|
||||
resourceTypes.add(myContext.getResourceDefinition(next).getImplementingClass());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypes.isEmpty()) {
|
||||
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
|
||||
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
|
||||
if (searchParamByName == null) {
|
||||
throw new InternalErrorException("Could not find parameter " + theParamName);
|
||||
}
|
||||
String paramPath = searchParamByName.getPath();
|
||||
if (paramPath.endsWith(".as(Reference)")) {
|
||||
paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
|
||||
}
|
||||
|
||||
if (paramPath.contains(".extension(")) {
|
||||
int startIdx = paramPath.indexOf(".extension(");
|
||||
int endIdx = paramPath.indexOf(')', startIdx);
|
||||
if (startIdx != -1 && endIdx != -1) {
|
||||
paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath);
|
||||
if (def instanceof RuntimeChildChoiceDefinition) {
|
||||
RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
|
||||
resourceTypes.addAll(choiceDef.getResourceTypes());
|
||||
} else if (def instanceof RuntimeChildResourceDefinition) {
|
||||
RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
|
||||
resourceTypes.addAll(resDef.getResourceTypes());
|
||||
if (resourceTypes.size() == 1) {
|
||||
if (resourceTypes.get(0).isInterface()) {
|
||||
throw new InvalidRequestException("Unable to perform search for unqualified chain '" + theParamName + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '" + theParamName + ":[ResourceType]' to perform this search.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypes.isEmpty()) {
|
||||
for (BaseRuntimeElementDefinition<?> next : myContext.getElementDefinitions()) {
|
||||
if (next instanceof RuntimeResourceDefinition) {
|
||||
RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
|
||||
resourceTypes.add(nextResDef.getImplementingClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resourceId = ref.getValue();
|
||||
|
||||
} else {
|
||||
try {
|
||||
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType());
|
||||
resourceTypes = new ArrayList<>(1);
|
||||
resourceTypes.add(resDef.getImplementingClass());
|
||||
resourceId = ref.getIdPart();
|
||||
} catch (DataFormatException e) {
|
||||
throw new InvalidRequestException("Invalid resource type: " + ref.getResourceType());
|
||||
}
|
||||
}
|
||||
|
||||
boolean foundChainMatch = false;
|
||||
|
||||
for (Class<? extends IBaseResource> nextType : resourceTypes) {
|
||||
|
||||
String chain = ref.getChain();
|
||||
String remainingChain = null;
|
||||
int chainDotIndex = chain.indexOf('.');
|
||||
if (chainDotIndex != -1) {
|
||||
remainingChain = chain.substring(chainDotIndex + 1);
|
||||
chain = chain.substring(0, chainDotIndex);
|
||||
}
|
||||
|
||||
RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType);
|
||||
String subResourceName = typeDef.getName();
|
||||
|
||||
IFhirResourceDao<?> dao = myCallingDao.getDao(nextType);
|
||||
if (dao == null) {
|
||||
ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName());
|
||||
continue;
|
||||
}
|
||||
|
||||
int qualifierIndex = chain.indexOf(':');
|
||||
String qualifier = null;
|
||||
if (qualifierIndex != -1) {
|
||||
qualifier = chain.substring(qualifierIndex);
|
||||
chain = chain.substring(0, qualifierIndex);
|
||||
}
|
||||
|
||||
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
|
||||
RuntimeSearchParam param = null;
|
||||
if (!isMeta) {
|
||||
param = mySearchParamRegistry.getSearchParamByName(typeDef, chain);
|
||||
if (param == null) {
|
||||
ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
|
||||
|
||||
for (IQueryParameterType next : theList) {
|
||||
String nextValue = next.getValueAsQueryToken(myContext);
|
||||
IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue);
|
||||
if (chainValue == null) {
|
||||
continue;
|
||||
}
|
||||
foundChainMatch = true;
|
||||
orValues.add(chainValue);
|
||||
}
|
||||
|
||||
Subquery<Long> subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues);
|
||||
|
||||
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
|
||||
Predicate pidPredicate = join.get("myTargetResourcePid").in(subQ);
|
||||
Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate);
|
||||
codePredicates.add(andPredicate);
|
||||
|
||||
}
|
||||
|
||||
if (!foundChainMatch) {
|
||||
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain()));
|
||||
}
|
||||
|
||||
myPredicates.add(myBuilder.or(toArray(codePredicates)));
|
||||
addPredicateReferenceWithChain(theResourceName, theParamName, theList, join, new ArrayList<>(), ref);
|
||||
return;
|
||||
|
||||
}
|
||||
|
@ -577,7 +436,172 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
|
||||
}
|
||||
|
||||
myPredicates.add(myBuilder.or(toArray(codePredicates)));
|
||||
List<Predicate> codePredicates = new ArrayList<>();
|
||||
|
||||
// Resources by ID
|
||||
List<Long> targetPids = myIdHelperService.translateForcedIdToPids(targetIds);
|
||||
if (!targetPids.isEmpty()) {
|
||||
ourLog.debug("Searching for resource link with target PIDs: {}", targetPids);
|
||||
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
|
||||
Predicate pidPredicate = join.get("myTargetResourcePid").in(targetPids);
|
||||
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
|
||||
}
|
||||
|
||||
// Resources by fully qualified URL
|
||||
if (!targetQualifiedUrls.isEmpty()) {
|
||||
ourLog.debug("Searching for resource link with target URLs: {}", targetQualifiedUrls);
|
||||
Predicate eq = join.get("myTargetResourceUrl").in(targetQualifiedUrls);
|
||||
codePredicates.add(eq);
|
||||
}
|
||||
|
||||
if (codePredicates.size() > 0) {
|
||||
myPredicates.add(myBuilder.or(toArray(codePredicates)));
|
||||
} else {
|
||||
// Add a predicate that will never match
|
||||
Predicate pidPredicate = join.get("myTargetResourcePid").in(-1L);
|
||||
myPredicates.clear();
|
||||
myPredicates.add(pidPredicate);
|
||||
}
|
||||
}
|
||||
|
||||
private void addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, Join<ResourceTable, ResourceLink> theJoin, List<Predicate> theCodePredicates, ReferenceParam theRef) {
|
||||
final List<Class<? extends IBaseResource>> resourceTypes;
|
||||
String resourceId;
|
||||
if (!theRef.getValue().matches("[a-zA-Z]+/.*")) {
|
||||
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
resourceTypes = new ArrayList<>();
|
||||
|
||||
Set<String> targetTypes = param.getTargets();
|
||||
|
||||
if (targetTypes != null && !targetTypes.isEmpty()) {
|
||||
for (String next : targetTypes) {
|
||||
resourceTypes.add(myContext.getResourceDefinition(next).getImplementingClass());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypes.isEmpty()) {
|
||||
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
|
||||
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
|
||||
if (searchParamByName == null) {
|
||||
throw new InternalErrorException("Could not find parameter " + theParamName);
|
||||
}
|
||||
String paramPath = searchParamByName.getPath();
|
||||
if (paramPath.endsWith(".as(Reference)")) {
|
||||
paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
|
||||
}
|
||||
|
||||
if (paramPath.contains(".extension(")) {
|
||||
int startIdx = paramPath.indexOf(".extension(");
|
||||
int endIdx = paramPath.indexOf(')', startIdx);
|
||||
if (startIdx != -1 && endIdx != -1) {
|
||||
paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath);
|
||||
if (def instanceof RuntimeChildChoiceDefinition) {
|
||||
RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
|
||||
resourceTypes.addAll(choiceDef.getResourceTypes());
|
||||
} else if (def instanceof RuntimeChildResourceDefinition) {
|
||||
RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
|
||||
resourceTypes.addAll(resDef.getResourceTypes());
|
||||
if (resourceTypes.size() == 1) {
|
||||
if (resourceTypes.get(0).isInterface()) {
|
||||
throw new InvalidRequestException("Unable to perform search for unqualified chain '" + theParamName + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '" + theParamName + ":[ResourceType]' to perform this search.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypes.isEmpty()) {
|
||||
for (BaseRuntimeElementDefinition<?> next : myContext.getElementDefinitions()) {
|
||||
if (next instanceof RuntimeResourceDefinition) {
|
||||
RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
|
||||
resourceTypes.add(nextResDef.getImplementingClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resourceId = theRef.getValue();
|
||||
|
||||
} else {
|
||||
try {
|
||||
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theRef.getResourceType());
|
||||
resourceTypes = new ArrayList<>(1);
|
||||
resourceTypes.add(resDef.getImplementingClass());
|
||||
resourceId = theRef.getIdPart();
|
||||
} catch (DataFormatException e) {
|
||||
throw new InvalidRequestException("Invalid resource type: " + theRef.getResourceType());
|
||||
}
|
||||
}
|
||||
|
||||
boolean foundChainMatch = false;
|
||||
|
||||
for (Class<? extends IBaseResource> nextType : resourceTypes) {
|
||||
|
||||
String chain = theRef.getChain();
|
||||
String remainingChain = null;
|
||||
int chainDotIndex = chain.indexOf('.');
|
||||
if (chainDotIndex != -1) {
|
||||
remainingChain = chain.substring(chainDotIndex + 1);
|
||||
chain = chain.substring(0, chainDotIndex);
|
||||
}
|
||||
|
||||
RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType);
|
||||
String subResourceName = typeDef.getName();
|
||||
|
||||
IFhirResourceDao<?> dao = myCallingDao.getDao(nextType);
|
||||
if (dao == null) {
|
||||
ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName());
|
||||
continue;
|
||||
}
|
||||
|
||||
int qualifierIndex = chain.indexOf(':');
|
||||
String qualifier = null;
|
||||
if (qualifierIndex != -1) {
|
||||
qualifier = chain.substring(qualifierIndex);
|
||||
chain = chain.substring(0, qualifierIndex);
|
||||
}
|
||||
|
||||
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
|
||||
RuntimeSearchParam param = null;
|
||||
if (!isMeta) {
|
||||
param = mySearchParamRegistry.getSearchParamByName(typeDef, chain);
|
||||
if (param == null) {
|
||||
ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
|
||||
|
||||
for (IQueryParameterType next : theList) {
|
||||
String nextValue = next.getValueAsQueryToken(myContext);
|
||||
IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue);
|
||||
if (chainValue == null) {
|
||||
continue;
|
||||
}
|
||||
foundChainMatch = true;
|
||||
orValues.add(chainValue);
|
||||
}
|
||||
|
||||
Subquery<Long> subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues);
|
||||
|
||||
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin);
|
||||
Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ);
|
||||
Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate);
|
||||
theCodePredicates.add(andPredicate);
|
||||
|
||||
}
|
||||
|
||||
if (!foundChainMatch) {
|
||||
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theRef.getChain()));
|
||||
}
|
||||
|
||||
myPredicates.add(myBuilder.or(toArray(theCodePredicates)));
|
||||
}
|
||||
|
||||
private Subquery<Long> createLinkSubquery(boolean theFoundChainMatch, String theChain, String theSubResourceName, List<IQueryParameterType> theOrValues) {
|
||||
|
|
|
@ -30,16 +30,15 @@ import org.springframework.data.repository.query.Param;
|
|||
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
||||
|
||||
public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
|
||||
|
||||
@Query("SELECT f FROM ForcedId f WHERE myForcedId = :forced_id")
|
||||
public List<ForcedId> findByForcedId(@Param("forced_id") String theForcedId);
|
||||
|
||||
@Query("SELECT f FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id")
|
||||
public List<ForcedId> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
|
||||
// FIXME: JA We should log a performance warning if this is used since it's not indexed
|
||||
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myForcedId IN (:forced_id)")
|
||||
List<Long> findByForcedId(@Param("forced_id") Collection<String> theForcedId);
|
||||
|
||||
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN (:forced_id)")
|
||||
List<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
|
||||
|
||||
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid")
|
||||
public ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
|
||||
ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
|
||||
|
||||
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid in (:pids)")
|
||||
Collection<ForcedId> findByResourcePids(@Param("pids") Collection<Long> pids);
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
Long valueOf;
|
||||
try {
|
||||
valueOf = myIdHelperService.translateForcedIdToPid(theTypeString, theId);
|
||||
ourLog.trace("Translated {}/{} to resource PID {}", theType, theId, valueOf);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
|
||||
return null;
|
||||
|
@ -86,7 +87,9 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
throw new InvalidRequestException("Resource " + resName + "/" + theId + " not found, specified in path: " + theNextPathsUnsplit);
|
||||
}
|
||||
|
||||
ourLog.trace("Resource PID {} is of type {}", valueOf, target.getResourceType());
|
||||
if (!theTypeString.equals(target.getResourceType())) {
|
||||
ourLog.error("Resource {} with PID {} was not of type {}", target.getIdDt().getValue(), target.getId(), theTypeString);
|
||||
throw new UnprocessableEntityException(
|
||||
"Resource contains reference to " + theNextId.getValue() + " but resource with ID " + theNextId.getIdPart() + " is actually of type " + target.getResourceType());
|
||||
}
|
||||
|
|
|
@ -23,16 +23,19 @@ package ca.uhn.fhir.jpa.dao.index;
|
|||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
|
||||
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Specimen;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
@Service
|
||||
public class IdHelperService {
|
||||
|
@ -45,37 +48,54 @@ public class IdHelperService {
|
|||
myForcedIdDao.delete(forcedId);
|
||||
}
|
||||
|
||||
public Long translateForcedIdToPid(String theResourceName, String theResourceId) {
|
||||
return translateForcedIdToPids(myDaoConfig, new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
|
||||
public Long translateForcedIdToPid(String theResourceName, String theResourceId) throws ResourceNotFoundException {
|
||||
// We only pass 1 input in so only 0..1 will come back
|
||||
IdDt id = new IdDt(theResourceName, theResourceId);
|
||||
List<Long> matches = translateForcedIdToPids(myDaoConfig, myForcedIdDao, Collections.singletonList(id));
|
||||
assert matches.size() <= 1;
|
||||
if (matches.isEmpty()) {
|
||||
throw new ResourceNotFoundException(id);
|
||||
}
|
||||
return matches.get(0);
|
||||
}
|
||||
|
||||
public List<Long> translateForcedIdToPids(IIdType theId) {
|
||||
return IdHelperService.translateForcedIdToPids(myDaoConfig, theId, myForcedIdDao);
|
||||
public List<Long> translateForcedIdToPids(Collection<IIdType> theId) {
|
||||
return IdHelperService.translateForcedIdToPids(myDaoConfig, myForcedIdDao, theId);
|
||||
}
|
||||
|
||||
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) {
|
||||
Validate.isTrue(theId.hasIdPart());
|
||||
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IForcedIdDao theForcedIdDao, Collection<IIdType> theId) {
|
||||
theId.forEach(id -> Validate.isTrue(id.hasIdPart()));
|
||||
|
||||
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) {
|
||||
return Collections.singletonList(theId.getIdPartAsLong());
|
||||
} else {
|
||||
List<ForcedId> forcedId;
|
||||
if (theId.hasResourceType()) {
|
||||
forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
|
||||
if (theId.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Long> retVal = new ArrayList<>();
|
||||
|
||||
ListMultimap<String, String> typeToIds = MultimapBuilder.hashKeys().arrayListValues().build();
|
||||
for (IIdType nextId : theId) {
|
||||
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(nextId)) {
|
||||
retVal.add(nextId.getIdPartAsLong());
|
||||
} else {
|
||||
forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
|
||||
}
|
||||
|
||||
if (!forcedId.isEmpty()) {
|
||||
List<Long> retVal = new ArrayList<>(forcedId.size());
|
||||
for (ForcedId next : forcedId) {
|
||||
retVal.add(next.getResourcePid());
|
||||
if (nextId.hasResourceType()) {
|
||||
typeToIds.put(nextId.getResourceType(), nextId.getIdPart());
|
||||
} else {
|
||||
typeToIds.put("", nextId.getIdPart());
|
||||
}
|
||||
return retVal;
|
||||
} else {
|
||||
throw new ResourceNotFoundException(theId);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Collection<String>> nextEntry : typeToIds.asMap().entrySet()) {
|
||||
String nextResourceType = nextEntry.getKey();
|
||||
Collection<String> nextIds = nextEntry.getValue();
|
||||
if (isBlank(nextResourceType)) {
|
||||
retVal.addAll(theForcedIdDao.findByForcedId(nextIds));
|
||||
} else {
|
||||
retVal.addAll(theForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds));
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public String translatePidIdToForcedId(String theResourceType, Long theId) {
|
||||
|
|
|
@ -193,8 +193,7 @@ public class SearchParamWithInlineReferencesExtractor {
|
|||
|
||||
for (String nextQueryString : queryStringsToPopulate) {
|
||||
if (isNotBlank(nextQueryString)) {
|
||||
// FIXME: JA change to trace
|
||||
ourLog.info("Adding composite unique SP: {}", nextQueryString);
|
||||
ourLog.trace("Adding composite unique SP: {}", nextQueryString);
|
||||
theParams.myCompositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,22 +24,36 @@ import net.ttddyy.dsproxy.ExecutionInfo;
|
|||
import net.ttddyy.dsproxy.QueryInfo;
|
||||
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
|
||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.engine.jdbc.internal.BasicFormatterImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.trim;
|
||||
|
||||
public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution {
|
||||
|
||||
private boolean myCaptureQueryStackTrace;
|
||||
|
||||
/**
|
||||
* This has an impact on performance! Use with caution.
|
||||
*/
|
||||
public boolean isCaptureQueryStackTrace() {
|
||||
return myCaptureQueryStackTrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* This has an impact on performance! Use with caution.
|
||||
*/
|
||||
public void setCaptureQueryStackTrace(boolean theCaptureQueryStackTrace) {
|
||||
myCaptureQueryStackTrace = theCaptureQueryStackTrace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ExecutionInfo theExecutionInfo, List<QueryInfo> theQueryInfoList) {
|
||||
final Queue<Query> queryList = provideQueryList();
|
||||
for (QueryInfo next : theQueryInfoList) {
|
||||
String sql = StringUtils.trim(next.getQuery());
|
||||
String sql = trim(next.getQuery());
|
||||
List<String> params;
|
||||
if (next.getParametersList().size() > 0 && next.getParametersList().get(0).size() > 0) {
|
||||
List<ParameterSetOperation> values = next
|
||||
|
@ -53,9 +67,14 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
|
|||
params = Collections.emptyList();
|
||||
}
|
||||
|
||||
StackTraceElement[] stackTraceElements = null;
|
||||
if (isCaptureQueryStackTrace()) {
|
||||
stackTraceElements = Thread.currentThread().getStackTrace();
|
||||
}
|
||||
|
||||
long elapsedTime = theExecutionInfo.getElapsedTime();
|
||||
long startTime = System.currentTimeMillis() - elapsedTime;
|
||||
queryList.add(new Query(sql, params, startTime, elapsedTime));
|
||||
queryList.add(new Query(sql, params, startTime, elapsedTime, stackTraceElements));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,12 +86,14 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
|
|||
private final List<String> myParams;
|
||||
private final long myQueryTimestamp;
|
||||
private final long myElapsedTime;
|
||||
private final StackTraceElement[] myStackTrace;
|
||||
|
||||
Query(String theSql, List<String> theParams, long theQueryTimestamp, long theElapsedTime) {
|
||||
Query(String theSql, List<String> theParams, long theQueryTimestamp, long theElapsedTime, StackTraceElement[] theStackTraceElements) {
|
||||
mySql = theSql;
|
||||
myParams = Collections.unmodifiableList(theParams);
|
||||
myQueryTimestamp = theQueryTimestamp;
|
||||
myElapsedTime = theElapsedTime;
|
||||
myStackTrace = theStackTraceElements;
|
||||
}
|
||||
|
||||
public long getQueryTimestamp() {
|
||||
|
@ -113,10 +134,13 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
|
|||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
return trim(retVal);
|
||||
|
||||
}
|
||||
|
||||
public StackTraceElement[] getStackTrace() {
|
||||
return myStackTrace;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* This is a query listener designed to be plugged into a {@link ProxyDataSourceBuilder proxy DataSource}.
|
||||
|
@ -70,27 +71,77 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||
return Collections.unmodifiableList(retVal);
|
||||
}
|
||||
|
||||
private List<Query> getQueriesForCurrentThreadStartingWith(String theStart) {
|
||||
String threadName = Thread.currentThread().getName();
|
||||
return getQueriesStartingWith(theStart, threadName);
|
||||
}
|
||||
|
||||
private List<Query> getQueriesStartingWith(String theStart, String theThreadName) {
|
||||
return getCapturedQueries()
|
||||
.stream()
|
||||
.filter(t -> theThreadName == null || t.getThreadName().equals(theThreadName))
|
||||
.filter(t -> t.getSql(false, false).toLowerCase().startsWith(theStart))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Query> getQueriesStartingWith(String theStart) {
|
||||
return getQueriesStartingWith(theStart, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getSelectQueries() {
|
||||
return getQueriesStartingWith("select");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all INSERT queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getInsertQueries() {
|
||||
return getQueriesStartingWith("insert");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getUpdateQueries() {
|
||||
return getQueriesStartingWith("update");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getDeleteQueries() {
|
||||
return getQueriesStartingWith("delete");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getSelectQueriesForCurrentThread() {
|
||||
String currentThreadName = Thread.currentThread().getName();
|
||||
return getCapturedQueries()
|
||||
.stream()
|
||||
.filter(t -> t.getThreadName().equals(currentThreadName))
|
||||
.filter(t -> t.getSql(false, false).toLowerCase().contains("select"))
|
||||
.collect(Collectors.toList());
|
||||
return getQueriesForCurrentThreadStartingWith("select");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all INSERT queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getInsertQueriesForCurrentThread() {
|
||||
return getCapturedQueries()
|
||||
.stream()
|
||||
.filter(t -> t.getThreadName().equals(Thread.currentThread().getName()))
|
||||
.filter(t -> t.getSql(false, false).toLowerCase().contains("insert"))
|
||||
.collect(Collectors.toList());
|
||||
return getQueriesForCurrentThreadStartingWith("insert");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getUpdateQueriesForCurrentThread() {
|
||||
return getQueriesForCurrentThreadStartingWith("update");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
|
||||
*/
|
||||
public List<Query> getDeleteQueriesForCurrentThread() {
|
||||
return getQueriesForCurrentThreadStartingWith("delete");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,6 +155,17 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||
ourLog.info("Select Queries:\n{}", String.join("\n", queries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log all captured SELECT queries
|
||||
*/
|
||||
public void logSelectQueries() {
|
||||
List<String> queries = getSelectQueries()
|
||||
.stream()
|
||||
.map(CircularQueueCaptureQueriesListener::formatQueryAsSql)
|
||||
.collect(Collectors.toList());
|
||||
ourLog.info("Select Queries:\n{}", String.join("\n", queries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log first captured SELECT query
|
||||
*/
|
||||
|
@ -127,9 +189,67 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
|
|||
ourLog.info("Insert Queries:\n{}", String.join("\n", queries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log all captured INSERT queries
|
||||
*/
|
||||
public void logInsertQueries() {
|
||||
List<String> queries = getInsertQueries()
|
||||
.stream()
|
||||
.map(CircularQueueCaptureQueriesListener::formatQueryAsSql)
|
||||
.collect(Collectors.toList());
|
||||
ourLog.info("Insert Queries:\n{}", String.join("\n", queries));
|
||||
}
|
||||
|
||||
public int countSelectQueries() {
|
||||
return getSelectQueries().size();
|
||||
}
|
||||
|
||||
public int countInsertQueries() {
|
||||
return getInsertQueries().size();
|
||||
}
|
||||
|
||||
public int countUpdateQueries() {
|
||||
return getUpdateQueries().size();
|
||||
}
|
||||
|
||||
public int countDeleteQueries() {
|
||||
return getDeleteQueries().size();
|
||||
}
|
||||
|
||||
public int countSelectQueriesForCurrentThread() {
|
||||
return getSelectQueriesForCurrentThread().size();
|
||||
}
|
||||
|
||||
public int countInsertQueriesForCurrentThread() {
|
||||
return getInsertQueriesForCurrentThread().size();
|
||||
}
|
||||
|
||||
public int countUpdateQueriesForCurrentThread() {
|
||||
return getUpdateQueriesForCurrentThread().size();
|
||||
}
|
||||
|
||||
public int countDeleteQueriesForCurrentThread() {
|
||||
return getDeleteQueriesForCurrentThread().size();
|
||||
}
|
||||
|
||||
|
||||
private static String formatQueryAsSql(Query theQuery) {
|
||||
String formattedSql = theQuery.getSql(true, true);
|
||||
return "Query at " + new InstantType(new Date(theQuery.getQueryTimestamp())).getValueAsString() + " took " + StopWatch.formatMillis(theQuery.getElapsedTime()) + " on Thread: " + theQuery.getThreadName() + "\nSQL:\n" + formattedSql;
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append("Query at ");
|
||||
b.append(new InstantType(new Date(theQuery.getQueryTimestamp())).getValueAsString());
|
||||
b.append(" took ").append(StopWatch.formatMillis(theQuery.getElapsedTime()));
|
||||
b.append(" on Thread: ").append(theQuery.getThreadName());
|
||||
b.append("\nSQL:\n").append(formattedSql);
|
||||
if (theQuery.getStackTrace() != null) {
|
||||
b.append("\nStack:\n ");
|
||||
Stream<String> stackTraceStream = Arrays.stream(theQuery.getStackTrace())
|
||||
.map(StackTraceElement::toString)
|
||||
.filter(t->t.startsWith("ca."));
|
||||
b.append(stackTraceStream.collect(Collectors.joining("\n ")));
|
||||
}
|
||||
b.append("\n");
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,348 +0,0 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import net.ttddyy.dsproxy.QueryCount;
|
||||
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
||||
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.Organization;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"scheduling_disabled=true"
|
||||
})
|
||||
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
|
||||
@Autowired
|
||||
private SingleQueryCountHolder myCountHolder;
|
||||
|
||||
@After
|
||||
public void afterResetDao() {
|
||||
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());
|
||||
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritesPerformMinimalSqlStatements() {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("sys1").setValue("val1");
|
||||
p.addIdentifier().setSystem("sys2").setValue("val2");
|
||||
|
||||
ourLog.info("** About to perform write");
|
||||
myCountHolder.clear();
|
||||
|
||||
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
ourLog.info("** Done performing write");
|
||||
|
||||
assertEquals(6, getQueryCount().getInsert());
|
||||
assertEquals(0, getQueryCount().getUpdate());
|
||||
|
||||
/*
|
||||
* Not update the value
|
||||
*/
|
||||
|
||||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.addIdentifier().setSystem("sys1").setValue("val3");
|
||||
p.addIdentifier().setSystem("sys2").setValue("val4");
|
||||
|
||||
ourLog.info("** About to perform write 2");
|
||||
myCountHolder.clear();
|
||||
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
ourLog.info("** Done performing write 2");
|
||||
|
||||
assertEquals(1, getQueryCount().getInsert());
|
||||
assertEquals(2, getQueryCount().getUpdate());
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch() {
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("sys1").setValue("val" + i);
|
||||
myPatientDao.create(p);
|
||||
}
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
ourLog.info("** About to perform search");
|
||||
IBundleProvider search = myPatientDao.search(new SearchParameterMap());
|
||||
ourLog.info("** About to retrieve resources");
|
||||
search.getResources(0, 20);
|
||||
ourLog.info("** Done retrieving resources");
|
||||
|
||||
assertEquals(4, getQueryCount().getSelect());
|
||||
assertEquals(2, getQueryCount().getInsert());
|
||||
assertEquals(1, getQueryCount().getUpdate());
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
|
||||
}
|
||||
|
||||
private QueryCount getQueryCount() {
|
||||
return myCountHolder.getQueryCountMap().get("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateClientAssignedId() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
myCountHolder.clear();
|
||||
ourLog.info("** Starting Update Non-Existing resource with client assigned ID");
|
||||
Patient p = new Patient();
|
||||
p.setId("A");
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(1, getQueryCount().getSelect());
|
||||
assertEquals(4, getQueryCount().getInsert());
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
// Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID
|
||||
assertEquals(1, getQueryCount().getUpdate());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(1, myResourceHistoryTableDao.count());
|
||||
assertEquals(1, myForcedIdDao.count());
|
||||
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
|
||||
});
|
||||
|
||||
// Ok how about an update
|
||||
|
||||
myCountHolder.clear();
|
||||
ourLog.info("** Starting Update Existing resource with client assigned ID");
|
||||
p = new Patient();
|
||||
p.setId("A");
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(4, getQueryCount().getSelect());
|
||||
assertEquals(1, getQueryCount().getInsert());
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
assertEquals(1, getQueryCount().getUpdate());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(2, myResourceHistoryTableDao.count());
|
||||
assertEquals(1, myForcedIdDao.count());
|
||||
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testOneRowPerUpdate() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
myCountHolder.clear();
|
||||
Patient p = new Patient();
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
|
||||
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(3, getQueryCount().getInsert());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(1, myResourceHistoryTableDao.count());
|
||||
});
|
||||
|
||||
|
||||
|
||||
myCountHolder.clear();
|
||||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(1, getQueryCount().getInsert());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(2, myResourceHistoryTableDao.count());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexes() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.setActive(true);
|
||||
pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B");
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
ourLog.info("** About to update");
|
||||
|
||||
pt.setId(id);
|
||||
pt.getNameFirstRep().addGiven("GIVEN1C");
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
assertEquals(2, getQueryCount().getInsert());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexesString() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
SearchParameterMap m1 = new SearchParameterMap().add("family", new StringParam("family1")).setLoadSynchronous(true);
|
||||
SearchParameterMap m2 = new SearchParameterMap().add("family", new StringParam("family2")).setLoadSynchronous(true);
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.addName().setFamily("FAMILY1");
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
assertEquals(1, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
ourLog.info("** About to update");
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
pt.addName().setFamily("FAMILY2");
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER
|
||||
assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(1, myPatientDao.search(m2).size().intValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexesToken() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
SearchParameterMap m1 = new SearchParameterMap().add("gender", new TokenParam("male")).setLoadSynchronous(true);
|
||||
SearchParameterMap m2 = new SearchParameterMap().add("gender", new TokenParam("female")).setLoadSynchronous(true);
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(0, getQueryCount().getSelect());
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
assertEquals(3, getQueryCount().getInsert());
|
||||
assertEquals(0, getQueryCount().getUpdate());
|
||||
assertEquals(1, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
/*
|
||||
* Change a value
|
||||
*/
|
||||
|
||||
ourLog.info("** About to update");
|
||||
myCountHolder.clear();
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
pt.setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
myPatientDao.update(pt);
|
||||
|
||||
/*
|
||||
* Current SELECTs:
|
||||
* Select the resource from HFJ_RESOURCE
|
||||
* Select the version from HFJ_RES_VER
|
||||
* Select the current token indexes
|
||||
*/
|
||||
assertEquals(3, getQueryCount().getSelect());
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER
|
||||
assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(1, myPatientDao.search(m2).size().intValue());
|
||||
myCountHolder.clear();
|
||||
|
||||
/*
|
||||
* Drop a value
|
||||
*/
|
||||
|
||||
ourLog.info("** About to update again");
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(1, getQueryCount().getDelete());
|
||||
assertEquals(1, getQueryCount().getInsert());
|
||||
assertEquals(1, getQueryCount().getUpdate());
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexesResourceLink() {
|
||||
Organization org1 = new Organization();
|
||||
org1.setName("org1");
|
||||
IIdType orgId1 = myOrganizationDao.create(org1).getId().toUnqualifiedVersionless();
|
||||
Organization org2 = new Organization();
|
||||
org2.setName("org2");
|
||||
IIdType orgId2 = myOrganizationDao.create(org2).getId().toUnqualifiedVersionless();
|
||||
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
SearchParameterMap m1 = new SearchParameterMap().add("organization", new ReferenceParam(orgId1.getValue())).setLoadSynchronous(true);
|
||||
SearchParameterMap m2 = new SearchParameterMap().add("organization", new ReferenceParam(orgId2.getValue())).setLoadSynchronous(true);
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.getManagingOrganization().setReference(orgId1.getValue());
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCountHolder.clear();
|
||||
|
||||
assertEquals(1, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
ourLog.info("** About to update");
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
pt.getManagingOrganization().setReference(orgId2.getValue());
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(0, getQueryCount().getDelete());
|
||||
assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER
|
||||
assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(1, myPatientDao.search(m2).size().intValue());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
}
|
||||
|
||||
}
|
|
@ -2166,14 +2166,20 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
|
||||
ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", patientId01, patientId02, obsId01, obsId02, drId01);
|
||||
|
||||
List<Observation> result = toList(
|
||||
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId01"))));
|
||||
List<Observation> result;
|
||||
|
||||
// With an ID that exists
|
||||
result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId01"))));
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(obsId01.getIdPart(), result.get(0).getIdElement().getIdPart());
|
||||
|
||||
// Now with an alphanumeric ID that doesn't exist
|
||||
myCaptureQueriesListener.clear();
|
||||
result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId99"))));
|
||||
assertEquals(0, result.size());
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
assertEquals(result.toString(),0, result.size());
|
||||
|
||||
// And with a numeric ID that doesn't exist
|
||||
result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("999999999999999"))));
|
||||
assertEquals(0, result.size());
|
||||
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.entity.Search;
|
||||
import ca.uhn.fhir.jpa.entity.SearchStatusEnum;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
|
||||
import ca.uhn.fhir.rest.api.SortSpec;
|
||||
import ca.uhn.fhir.rest.api.SummaryEnum;
|
||||
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.TokenParam;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.*;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.leftPad;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"scheduling_disabled=true"
|
||||
})
|
||||
@SuppressWarnings({"unchecked", "deprecation", "Duplicates"})
|
||||
public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
||||
|
||||
|
@ -41,6 +51,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
@Before
|
||||
public void before() {
|
||||
mySearchCoordinatorSvcImpl = (SearchCoordinatorSvcImpl) AopProxyUtils.getSingletonTarget(mySearchCoordinatorSvc);
|
||||
myCaptureQueriesListener.setCaptureQueryStackTrace(true);
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -48,10 +59,10 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(null);
|
||||
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
|
||||
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
|
||||
myCaptureQueriesListener.setCaptureQueryStackTrace(false);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void start() {
|
||||
private void create200Patients() {
|
||||
runInTransaction(() -> {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
Patient p = new Patient();
|
||||
|
@ -65,6 +76,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchCountOnly() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
|
||||
|
||||
|
@ -82,6 +94,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchCountWithMultipleIndexesOnOneResource() {
|
||||
create200Patients();
|
||||
|
||||
// Already have 200, let's add number 201 with a bunch of similar names
|
||||
Patient p = new Patient();
|
||||
|
@ -136,6 +149,8 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchTotalAccurateForSlowLoading() {
|
||||
create200Patients();
|
||||
|
||||
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(25);
|
||||
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(10);
|
||||
|
||||
|
@ -164,6 +179,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchCountAndData() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
|
||||
|
||||
|
@ -200,6 +216,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchRightUpToActualNumberExistingThenFetchAnotherPage() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(200, -1));
|
||||
|
||||
|
@ -254,6 +271,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchOnlySmallBatches() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
|
||||
|
||||
|
@ -379,6 +397,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchMoreThanFirstPageSizeInFirstPage() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, -1));
|
||||
|
||||
|
@ -414,6 +433,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchUnlimited() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, -1));
|
||||
|
||||
|
@ -472,7 +492,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testFetchSecondBatchInManyThreads() throws Throwable {
|
||||
|
||||
create200Patients();
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, -1));
|
||||
|
||||
/*
|
||||
|
@ -493,7 +513,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
* 20 should be prefetched since that's the initial page size
|
||||
*/
|
||||
|
||||
waitForSize(20, () -> runInTransaction(()-> mySearchEntityDao.findByUuid(uuid).getNumFound()));
|
||||
waitForSize(20, () -> runInTransaction(() -> mySearchEntityDao.findByUuid(uuid).getNumFound()));
|
||||
runInTransaction(() -> {
|
||||
Search search = mySearchEntityDao.findByUuid(uuid);
|
||||
assertEquals(20, search.getNumFound());
|
||||
|
@ -541,6 +561,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
|
||||
@Test
|
||||
public void testSearchThatOnlyReturnsASmallResult() {
|
||||
create200Patients();
|
||||
|
||||
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
|
||||
|
||||
|
@ -568,6 +589,439 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* A search with a big list of OR clauses for references should use a single SELECT ... WHERE .. IN
|
||||
* and not a whole bunch of SQL ORs.
|
||||
*/
|
||||
@Test
|
||||
public void testReferenceOrLinksUseInList() {
|
||||
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Organization org = new Organization();
|
||||
org.setActive(true);
|
||||
ids.add(myOrganizationDao.create(org).getId().getIdPartAsLong());
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Patient pt = new Patient();
|
||||
pt.setManagingOrganization(new Reference("Organization/" + ids.get(i)));
|
||||
myPatientDao.create(pt).getId().getIdPartAsLong();
|
||||
}
|
||||
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Patient.SP_ORGANIZATION, new ReferenceOrListParam()
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(0)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(1)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(2)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(3)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(4)))
|
||||
);
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider search = myPatientDao.search(map);
|
||||
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
List<String> queries = myCaptureQueriesListener
|
||||
.getSelectQueriesForCurrentThread()
|
||||
.stream()
|
||||
.map(t -> t.getSql(true, false))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
String resultingQueryNotFormatted = queries.get(0);
|
||||
assertEquals(resultingQueryNotFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "Patient.managingOrganization"));
|
||||
assertThat(resultingQueryNotFormatted, containsString("TARGET_RESOURCE_ID in ('" + ids.get(0) + "' , '" + ids.get(1) + "' , '" + ids.get(2) + "' , '" + ids.get(3) + "' , '" + ids.get(4) + "')"));
|
||||
|
||||
// Ensure that the search actually worked
|
||||
assertEquals(5, search.size().intValue());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@After
|
||||
public void afterResetDao() {
|
||||
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());
|
||||
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritesPerformMinimalSqlStatements() {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("sys1").setValue("val1");
|
||||
p.addIdentifier().setSystem("sys2").setValue("val2");
|
||||
|
||||
ourLog.info("** About to perform write");
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
ourLog.info("** Done performing write");
|
||||
|
||||
assertEquals(6, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
|
||||
/*
|
||||
* Not update the value
|
||||
*/
|
||||
|
||||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.addIdentifier().setSystem("sys1").setValue("val3");
|
||||
p.addIdentifier().setSystem("sys2").setValue("val4");
|
||||
|
||||
ourLog.info("** About to perform write 2");
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
ourLog.info("** Done performing write 2");
|
||||
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch() {
|
||||
create200Patients();
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
Patient p = new Patient();
|
||||
p.addIdentifier().setSystem("sys1").setValue("val" + i);
|
||||
myPatientDao.create(p);
|
||||
}
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
ourLog.info("** About to perform search");
|
||||
IBundleProvider search = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(false));
|
||||
ourLog.info("** About to retrieve resources");
|
||||
search.getResources(0, 20);
|
||||
ourLog.info("** Done retrieving resources");
|
||||
|
||||
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
assertEquals(4, myCaptureQueriesListener.countSelectQueries());
|
||||
// Batches of 30 are written for each query - so 9 inserts total
|
||||
assertEquals(9, myCaptureQueriesListener.countInsertQueries());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||
|
||||
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateClientAssignedId() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
ourLog.info("** Starting Update Non-Existing resource with client assigned ID");
|
||||
Patient p = new Patient();
|
||||
p.setId("A");
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
||||
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
// Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(1, myResourceHistoryTableDao.count());
|
||||
assertEquals(1, myForcedIdDao.count());
|
||||
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
|
||||
});
|
||||
|
||||
// Ok how about an update
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
ourLog.info("** Starting Update Existing resource with client assigned ID");
|
||||
p = new Patient();
|
||||
p.setId("A");
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(2, myResourceHistoryTableDao.count());
|
||||
assertEquals(1, myForcedIdDao.count());
|
||||
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testOneRowPerUpdate() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
Patient p = new Patient();
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
|
||||
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(1, myResourceHistoryTableDao.count());
|
||||
});
|
||||
|
||||
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
|
||||
myPatientDao.update(p).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
runInTransaction(() -> {
|
||||
assertEquals(1, myResourceTableDao.count());
|
||||
assertEquals(2, myResourceHistoryTableDao.count());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexes() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.setActive(true);
|
||||
pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B");
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
ourLog.info("** About to update");
|
||||
|
||||
pt.setId(id);
|
||||
pt.getNameFirstRep().addGiven("GIVEN1C");
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexesString() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
SearchParameterMap m1 = new SearchParameterMap().add("family", new StringParam("family1")).setLoadSynchronous(true);
|
||||
SearchParameterMap m2 = new SearchParameterMap().add("family", new StringParam("family2")).setLoadSynchronous(true);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.addName().setFamily("FAMILY1");
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
assertEquals(1, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
ourLog.info("** About to update");
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
pt.addName().setFamily("FAMILY2");
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
|
||||
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(1, myPatientDao.search(m2).size().intValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexesToken() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
SearchParameterMap m1 = new SearchParameterMap().add("gender", new TokenParam("male")).setLoadSynchronous(true);
|
||||
SearchParameterMap m2 = new SearchParameterMap().add("gender", new TokenParam("female")).setLoadSynchronous(true);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
assertEquals(1, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
/*
|
||||
* Change a value
|
||||
*/
|
||||
|
||||
ourLog.info("** About to update");
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
pt.setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
myPatientDao.update(pt);
|
||||
|
||||
/*
|
||||
* Current SELECTs:
|
||||
* Select the resource from HFJ_RESOURCE
|
||||
* Select the version from HFJ_RES_VER
|
||||
* Select the current token indexes
|
||||
*/
|
||||
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
|
||||
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(1, myPatientDao.search(m2).size().intValue());
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
/*
|
||||
* Drop a value
|
||||
*/
|
||||
|
||||
ourLog.info("** About to update again");
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateReusesIndexesResourceLink() {
|
||||
Organization org1 = new Organization();
|
||||
org1.setName("org1");
|
||||
IIdType orgId1 = myOrganizationDao.create(org1).getId().toUnqualifiedVersionless();
|
||||
Organization org2 = new Organization();
|
||||
org2.setName("org2");
|
||||
IIdType orgId2 = myOrganizationDao.create(org2).getId().toUnqualifiedVersionless();
|
||||
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
SearchParameterMap m1 = new SearchParameterMap().add("organization", new ReferenceParam(orgId1.getValue())).setLoadSynchronous(true);
|
||||
SearchParameterMap m2 = new SearchParameterMap().add("organization", new ReferenceParam(orgId2.getValue())).setLoadSynchronous(true);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
Patient pt = new Patient();
|
||||
pt.getManagingOrganization().setReference(orgId1.getValue());
|
||||
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
assertEquals(1, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(0, myPatientDao.search(m2).size().intValue());
|
||||
|
||||
ourLog.info("** About to update");
|
||||
|
||||
pt = new Patient();
|
||||
pt.setId(id);
|
||||
pt.getManagingOrganization().setReference(orgId2.getValue());
|
||||
myPatientDao.update(pt);
|
||||
|
||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
|
||||
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
|
||||
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
|
||||
|
||||
assertEquals(0, myPatientDao.search(m1).size().intValue());
|
||||
assertEquals(1, myPatientDao.search(m2).size().intValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testReferenceOrLinksUseInList_ForcedIds() {
|
||||
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Organization org = new Organization();
|
||||
org.setId("ORG"+i);
|
||||
org.setActive(true);
|
||||
runInTransaction(()->{
|
||||
IIdType id = myOrganizationDao.update(org).getId();
|
||||
ids.add(id.getIdPart());
|
||||
});
|
||||
|
||||
// org = myOrganizationDao.read(id);
|
||||
// assertTrue(org.getActive());
|
||||
}
|
||||
|
||||
runInTransaction(()->{
|
||||
for (ResourceTable next : myResourceTableDao.findAll()) {
|
||||
ourLog.info("Resource pid {} of type {}", next.getId(), next.getResourceType());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Patient pt = new Patient();
|
||||
pt.setManagingOrganization(new Reference("Organization/" + ids.get(i)));
|
||||
myPatientDao.create(pt).getId().getIdPartAsLong();
|
||||
}
|
||||
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Patient.SP_ORGANIZATION, new ReferenceOrListParam()
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(0)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(1)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(2)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(3)))
|
||||
.addOr(new ReferenceParam("Organization/" + ids.get(4)))
|
||||
);
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider search = myPatientDao.search(map);
|
||||
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
List<String> queries = myCaptureQueriesListener
|
||||
.getSelectQueriesForCurrentThread()
|
||||
.stream()
|
||||
.map(t -> t.getSql(true, false))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Forced ID resolution
|
||||
String resultingQueryNotFormatted = queries.get(0);
|
||||
assertThat(resultingQueryNotFormatted, containsString("RESOURCE_TYPE='Organization'"));
|
||||
assertThat(resultingQueryNotFormatted, containsString("FORCED_ID in ('ORG0' , 'ORG1' , 'ORG2' , 'ORG3' , 'ORG4')"));
|
||||
|
||||
// The search itself
|
||||
resultingQueryNotFormatted = queries.get(1);
|
||||
assertEquals(resultingQueryNotFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "Patient.managingOrganization"));
|
||||
assertThat(resultingQueryNotFormatted, matchesPattern(".*TARGET_RESOURCE_ID in \\('[0-9]+' , '[0-9]+' , '[0-9]+' , '[0-9]+' , '[0-9]+'\\).*"));
|
||||
|
||||
// Ensure that the search actually worked
|
||||
assertEquals(5, search.size().intValue());
|
||||
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
|
|
|
@ -1843,10 +1843,9 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
|
|||
assertEquals(id.withVersion("3").getValue(), history.getEntry().get(0).getResource().getId());
|
||||
assertEquals(1, ((Patient) history.getEntry().get(0).getResource()).getName().size());
|
||||
|
||||
assertEquals(id.withVersion("2").getValue(), history.getEntry().get(1).getResource().getId());
|
||||
assertEquals(HTTPVerb.DELETE, history.getEntry().get(1).getRequest().getMethodElement().getValue());
|
||||
assertEquals("http://localhost:" + ourPort + "/fhir/context/Patient/" + id.getIdPart() + "/_history/2", history.getEntry().get(1).getRequest().getUrl());
|
||||
assertEquals(0, ((Patient) history.getEntry().get(1).getResource()).getName().size());
|
||||
assertEquals(null, history.getEntry().get(1).getResource());
|
||||
|
||||
assertEquals(id.withVersion("1").getValue(), history.getEntry().get(2).getResource().getId());
|
||||
assertEquals(1, ((Patient) history.getEntry().get(2).getResource()).getName().size());
|
||||
|
|
|
@ -2161,15 +2161,16 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
|||
patient.setId(id);
|
||||
ourClient.update().resource(patient).execute();
|
||||
|
||||
ourLog.info("Res ID: {}", id);
|
||||
|
||||
Bundle history = ourClient.history().onInstance(id).andReturnBundle(Bundle.class).prettyPrint().summaryMode(SummaryEnum.DATA).execute();
|
||||
assertEquals(3, history.getEntry().size());
|
||||
assertEquals(id.withVersion("3").getValue(), history.getEntry().get(0).getResource().getId());
|
||||
assertEquals(1, ((Patient) history.getEntry().get(0).getResource()).getName().size());
|
||||
|
||||
assertEquals(id.withVersion("2").getValue(), history.getEntry().get(1).getResource().getId());
|
||||
assertEquals(HTTPVerb.DELETE, history.getEntry().get(1).getRequest().getMethodElement().getValue());
|
||||
assertEquals("http://localhost:" + ourPort + "/fhir/context/Patient/" + id.getIdPart() + "/_history/2", history.getEntry().get(1).getRequest().getUrl());
|
||||
assertEquals(0, ((Patient) history.getEntry().get(1).getResource()).getName().size());
|
||||
assertEquals(null, history.getEntry().get(1).getResource());
|
||||
|
||||
assertEquals(id.withVersion("1").getValue(), history.getEntry().get(2).getResource().getId());
|
||||
assertEquals(1, ((Patient) history.getEntry().get(2).getResource()).getName().size());
|
||||
|
|
|
@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.model.entity;
|
|||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.model.primitive.InstantDt;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import org.hibernate.annotations.OptimisticLock;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
@ -61,6 +62,22 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
|
|||
@OptimisticLock(excluded = true)
|
||||
private Date myUpdated;
|
||||
|
||||
/**
|
||||
* This is stored as an optimization to avoid neeind to query for this
|
||||
* after an update
|
||||
*/
|
||||
@Transient
|
||||
private transient String myTransientForcedId;
|
||||
|
||||
public String getTransientForcedId() {
|
||||
return myTransientForcedId;
|
||||
}
|
||||
|
||||
public void setTransientForcedId(String theTransientForcedId) {
|
||||
myTransientForcedId = theTransientForcedId;
|
||||
}
|
||||
|
||||
|
||||
public abstract BaseTag addTag(TagDefinition theDef);
|
||||
|
||||
@Override
|
||||
|
@ -94,7 +111,16 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
|
|||
public abstract Long getId();
|
||||
|
||||
@Override
|
||||
public abstract IdDt getIdDt();
|
||||
public IdDt getIdDt() {
|
||||
if (getForcedId() == null) {
|
||||
Long id = getResourceId();
|
||||
return new IdDt(getResourceType() + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||
} else {
|
||||
// Avoid a join query if possible
|
||||
String forcedId = getTransientForcedId() != null ? getTransientForcedId() : getForcedId().getForcedId();
|
||||
return new IdDt(getResourceType() + '/' + forcedId + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstantDt getPublished() {
|
||||
|
@ -109,6 +135,10 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
|
|||
myPublished = thePublished;
|
||||
}
|
||||
|
||||
public void setPublished(InstantDt thePublished) {
|
||||
myPublished = thePublished.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract Long getResourceId();
|
||||
|
||||
|
@ -126,6 +156,10 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
|
|||
myUpdated = theUpdated;
|
||||
}
|
||||
|
||||
public void setUpdated(InstantDt theUpdated) {
|
||||
myUpdated = theUpdated.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getUpdatedDate() {
|
||||
return myUpdated;
|
||||
|
@ -143,12 +177,4 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
|
|||
myHasTags = theHasTags;
|
||||
}
|
||||
|
||||
public void setPublished(InstantDt thePublished) {
|
||||
myPublished = thePublished.getValue();
|
||||
}
|
||||
|
||||
public void setUpdated(InstantDt theUpdated) {
|
||||
myUpdated = theUpdated.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ public class ForcedId {
|
|||
private Long myId;
|
||||
|
||||
@JoinColumn(name = "RESOURCE_PID", nullable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_FORCEDID_RESOURCE"))
|
||||
@OneToOne()
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
private ResourceTable myResource;
|
||||
|
||||
@Column(name = "RESOURCE_PID", nullable = false, updatable = false, insertable = false)
|
||||
|
|
|
@ -124,16 +124,6 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
|
|||
myId = theId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdDt getIdDt() {
|
||||
if (getForcedId() == null) {
|
||||
Long id = myResourceId;
|
||||
return new IdDt(myResourceType + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||
} else {
|
||||
return new IdDt(getForcedId().getResourceType() + '/' + getForcedId().getForcedId() + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getResource() {
|
||||
return myResource;
|
||||
}
|
||||
|
|
|
@ -260,16 +260,6 @@ public class ResourceTable extends BaseHasResource implements Serializable {
|
|||
myId = theId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdDt getIdDt() {
|
||||
if (getForcedId() == null) {
|
||||
Long id = myId;
|
||||
return new IdDt(myResourceType + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + myVersion);
|
||||
} else {
|
||||
return new IdDt(getForcedId().getResourceType() + '/' + getForcedId().getForcedId() + '/' + Constants.PARAM_HISTORY + '/' + myVersion);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getIndexStatus() {
|
||||
return myIndexStatus;
|
||||
}
|
||||
|
@ -577,6 +567,7 @@ public class ResourceTable extends BaseHasResource implements Serializable {
|
|||
retVal.setResourceId(myId);
|
||||
retVal.setResourceType(myResourceType);
|
||||
retVal.setVersion(myVersion);
|
||||
retVal.setTransientForcedId(getTransientForcedId());
|
||||
|
||||
retVal.setPublished(getPublished());
|
||||
retVal.setUpdated(getUpdated());
|
||||
|
|
|
@ -121,6 +121,10 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory {
|
|||
entry.getRequest().getMethodElement().setValueAsString(httpVerb);
|
||||
entry.getRequest().getUrlElement().setValue(next.getId());
|
||||
}
|
||||
if ("DELETE".equals(httpVerb)) {
|
||||
entry.setResource(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -212,6 +216,9 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory {
|
|||
entry.getRequest().setUrl(id.getValue());
|
||||
}
|
||||
}
|
||||
if ("DELETE".equals(httpVerb)) {
|
||||
entry.setResource(null);
|
||||
}
|
||||
|
||||
String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource);
|
||||
if (searchMode != null) {
|
||||
|
|
|
@ -123,6 +123,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
|
|||
entry.getRequest().getMethodElement().setValueAsString(httpVerb);
|
||||
entry.getRequest().getUrlElement().setValue(next.getId());
|
||||
}
|
||||
if ("DELETE".equals(httpVerb)) {
|
||||
entry.setResource(null);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -216,6 +219,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
|
|||
entry.getRequest().setUrl(id.getValue());
|
||||
}
|
||||
}
|
||||
if ("DELETE".equals(httpVerb)) {
|
||||
entry.setResource(null);
|
||||
}
|
||||
|
||||
// Populate Response
|
||||
if ("1".equals(id.getVersionIdPart())) {
|
||||
|
|
|
@ -100,6 +100,18 @@
|
|||
a REFERENCE search parameter was used with arguments that consisted of
|
||||
unqualified resource IDs.
|
||||
</action>
|
||||
<action type="add">
|
||||
When performing a search in the JPA server where one of the parameters is a
|
||||
reference with multiple values (e.g. Patient?organization=A,B) the generated
|
||||
SQL was previously a set of OR clauses and this has been collapsed into a single
|
||||
IN clause for better performance.
|
||||
</action>
|
||||
<action type="fix">
|
||||
When returning the results of a history operation from a HAPI FHIR server,
|
||||
any entries with a method of DELETE contained a stub resource in
|
||||
Bundle.entry.resource, even though the FHIR spec states that this field
|
||||
should be empty. This was corrected.
|
||||
</action>
|
||||
</release>
|
||||
<release version="3.7.0" date="2019-02-06" description="Gale">
|
||||
<action type="add">
|
||||
|
|
Loading…
Reference in New Issue