Work on JPA performance

This commit is contained in:
James 2017-04-22 06:34:24 -04:00
parent 716fa56b8f
commit c311a0b3bf
10 changed files with 451 additions and 71 deletions

View File

@ -691,6 +691,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
Search search = new Search();
search.setCreated(new Date());
search.setSearchLastReturned(new Date());
search.setLastUpdated(theSince, theUntil);
search.setUuid(UUID.randomUUID().toString());
search.setResourceType(resourceName);

View File

@ -55,6 +55,11 @@ public class DaoConfig {
*/
private static final int DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION = 500;
/**
* Default value for {@link #setReuseCachedSearchResultsForMillis(Long)}: 60000ms (one minute)
*/
public static final Long DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS = DateUtils.MILLIS_PER_MINUTE;
// ***
// update setter javadoc if default changes
// ***
@ -64,9 +69,9 @@ public class DaoConfig {
// update setter javadoc if default changes
// ***
private boolean myAllowInlineMatchUrlReferences = true;
private boolean myAllowMultipleDelete;
private boolean myDefaultSearchParamsCanBeOverridden = false;
// ***
// update setter javadoc if default changes
// ***
@ -82,23 +87,25 @@ public class DaoConfig {
private int myHardTagListLimit = 1000;
private int myIncludeLimit = 2000;
// ***
// update setter javadoc if default changes
// ***
private boolean myIndexContainedResources = true;
private List<IServerInterceptor> myInterceptors;
private List<IServerInterceptor> myInterceptors;
// ***
// update setter javadoc if default changes
// ***
private int myMaximumExpansionSize = 5000;
private int myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION;
private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC;
private Long myReuseCachedSearchResultsForMillis;
private boolean mySchedulingDisabled;
private boolean mySubscriptionEnabled;
private long mySubscriptionPollDelay = 1000;
private Long mySubscriptionPurgeInactiveAfterMillis;
private Set<String> myTreatBaseUrlsAsLocal = new HashSet<String>();
private Set<String> myTreatReferencesAsLogical = new HashSet<String>(DEFAULT_LOGICAL_BASE_URLS);
@ -198,6 +205,21 @@ public class DaoConfig {
return myResourceEncoding;
}
/**
* If set to a non {@literal null} value (default is {@link #DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS non null})
* if an identical search is requested multiple times within this window, the same results will be returned
* to multiple queries. For example, if this value is set to 1 minute and a client searches for all
* patients named "smith", and then a second client also performs the same search within 1 minute,
* the same cached results will be returned.
* <p>
* This approach can improve performance, especially under heavy load, but can also mean that
* searches may potentially return slightly out-of-date results.
* </p>
*/
public Long getReuseCachedSearchResultsForMillis() {
return myReuseCachedSearchResultsForMillis;
}
public long getSubscriptionPollDelay() {
return mySubscriptionPollDelay;
}
@ -536,6 +558,21 @@ public class DaoConfig {
myResourceEncoding = theResourceEncoding;
}
/**
* If set to a non {@literal null} value (default is {@link #DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS non null})
* if an identical search is requested multiple times within this window, the same results will be returned
* to multiple queries. For example, if this value is set to 1 minute and a client searches for all
* patients named "smith", and then a second client also performs the same search within 1 minute,
* the same cached results will be returned.
* <p>
* This approach can improve performance, especially under heavy load, but can also mean that
* searches may potentially return slightly out-of-date results.
* </p>
*/
public void setReuseCachedSearchResultsForMillis(Long theReuseCachedSearchResultsForMillis) {
myReuseCachedSearchResultsForMillis = theReuseCachedSearchResultsForMillis;
}
public void setSchedulingDisabled(boolean theSchedulingDisabled) {
mySchedulingDisabled = theSchedulingDisabled;
}

View File

@ -29,6 +29,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.SearchParameterMap.IncludeComparator;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -121,10 +122,45 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
getIncludes().add(theInclude);
}
private void addLastUpdateParam(StringBuilder b, DateParam date) {
if (date != null && isNotBlank(date.getValueAsString())) {
addUrlParamSeparator(b);
b.append(Constants.PARAM_LASTUPDATED);
b.append('=');
b.append(date.getValueAsString());
}
}
public void addRevInclude(Include theInclude) {
getRevIncludes().add(theInclude);
}
private void addUrlIncludeParams(StringBuilder b, String paramName, Set<Include> theList) {
ArrayList<Include> list = new ArrayList<Include>(theList);
Collections.sort(list, new IncludeComparator());
for (Include nextInclude : list) {
addUrlParamSeparator(b);
b.append(paramName);
b.append('=');
b.append(UrlUtil.escape(nextInclude.getParamType()));
b.append(':');
b.append(UrlUtil.escape(nextInclude.getParamName()));
if (isNotBlank(nextInclude.getParamTargetType())) {
b.append(':');
b.append(nextInclude.getParamTargetType());
}
}
}
private void addUrlParamSeparator(StringBuilder theB) {
if (theB.length() == 0) {
theB.append('?');
} else {
theB.append('&');
}
}
public Integer getCount() {
return myCount;
}
@ -299,7 +335,9 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
if (i > 0) {
b.append(',');
}
b.append(ParameterUtil.escapeAndUrlEncode(nextValueOr.getValueAsQueryToken(theCtx)));
String valueAsQueryToken = nextValueOr.getValueAsQueryToken(theCtx);
// b.append(ParameterUtil.escapeAndUrlEncode(valueAsQueryToken));
b.append(UrlUtil.escape(valueAsQueryToken));
}
}
@ -311,6 +349,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
if (isNotBlank(sort.getParamName())) {
if (first) {
addUrlParamSeparator(b);
b.append(Constants.PARAM_SORT);
b.append('=');
first = false;
@ -338,6 +377,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
}
if (getCount() != null) {
addUrlParamSeparator(b);
b.append(Constants.PARAM_COUNT);
b.append('=');
b.append(getCount());
@ -347,36 +387,6 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
return b.toString();
}
private void addLastUpdateParam(StringBuilder b, DateParam date) {
if (isNotBlank(date.getValueAsString())) {
b.append(Constants.PARAM_LASTUPDATED);
b.append('=');
b.append(date.getValueAsString());
}
}
private void addUrlIncludeParams(StringBuilder b, String paramName, Set<Include> list) {
for (Include nextInclude : list) {
b.append(paramName);
b.append('=');
b.append(UrlUtil.escape(nextInclude.getParamType()));
b.append(':');
b.append(UrlUtil.escape(nextInclude.getParamName()));
if (isNotBlank(nextInclude.getParamTargetType())) {
b.append(':');
b.append(nextInclude.getParamTargetType());
}
}
}
private void addUrlParamSeparator(StringBuilder theB) {
if (theB.length() == 0) {
theB.append('?');
} else {
theB.append('&');
}
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
@ -453,6 +463,22 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
}
}
public class IncludeComparator implements Comparator<Include> {
@Override
public int compare(Include theO1, Include theO2) {
int retVal = StringUtils.compare(theO1.getParamType(), theO2.getParamType());
if (retVal == 0) {
retVal = StringUtils.compare(theO1.getParamName(), theO2.getParamName());
}
if (retVal == 0) {
retVal = StringUtils.compare(theO1.getParamTargetType(), theO2.getParamTargetType());
}
return retVal;
}
}
public class QueryParameterOrComparator implements Comparator<List<IQueryParameterType>> {
private final FhirContext myCtx;

View File

@ -13,7 +13,7 @@ import java.util.Date;
* 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
* 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,
@ -24,6 +24,7 @@ import java.util.Date;
*/
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@ -34,7 +35,17 @@ public interface ISearchDao extends JpaRepository<Search, Long> {
@Query("SELECT s FROM Search s WHERE s.myUuid = :uuid")
public Search findByUuid(@Param("uuid") String theUuid);
@Query("SELECT s FROM Search s WHERE s.myCreated < :cutoff")
public Collection<Search> findWhereCreatedBefore(@Param("cutoff") Date theCutoff);
@Query("SELECT s FROM Search s WHERE s.mySearchLastReturned < :cutoff")
public Collection<Search> findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff);
// @Query("SELECT s FROM Search s WHERE s.myCreated < :cutoff")
// public Collection<Search> findWhereCreatedBefore(@Param("cutoff") Date theCutoff);
@Query("SELECT s FROM Search s WHERE s.myResourceType = :type AND mySearchQueryStringHash = :hash AND s.myCreated > :cutoff")
public Collection<Search> find(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff);
@Modifying
@Query("UPDATE Search s SET s.mySearchLastReturned = :last WHERE s.myId = :pid")
public void updateSearchLastReturned(@Param("pid") long thePid, @Param("last") Date theDate);
}

View File

@ -31,19 +31,20 @@ import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import org.hibernate.annotations.Fetch;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.param.DateRangeParam;
//@formatter:off
@Entity
@Table(name = "HFJ_SEARCH", uniqueConstraints= {
@UniqueConstraint(name="IDX_SEARCH_UUID", columnNames="SEARCH_UUID")
}, indexes= {
@Index(name="JDX_SEARCH_CREATED", columnList="CREATED"),
@Index(name="JDX_SEARCH_STRING", columnList="SEARCH_STRING")
@Index(name="JDX_SEARCH_LASTRETURNED", columnList="SEARCH_LAST_RETURNED"),
@Index(name="JDX_SEARCH_RESTYPE_STRINGHASHCREATED", columnList="RESOURCE_TYPE,SEARCH_QUERY_STRING_HASH,CREATED")
})
//@formatter:on
public class Search implements Serializable {
private static final int FAILURE_MESSAGE_LENGTH = 500;
@ -92,8 +93,19 @@ public class Search implements Serializable {
@OneToMany(mappedBy="mySearch")
private Collection<SearchResult> myResults;
@Column(name="SEARCH_STRING", length=1000, nullable=true)
private String mySearchString;
// TODO: change nullable to false after 2.5
@NotNull
@Temporal(TemporalType.TIMESTAMP)
@Column(name="SEARCH_LAST_RETURNED", nullable=true, updatable=false)
private Date mySearchLastReturned;
@Lob()
@Basic(fetch=FetchType.LAZY)
@Column(name="SEARCH_QUERY_STRING", nullable=true, updatable=false)
private String mySearchQueryString;
@Column(name="SEARCH_QUERY_STRING_HASH", nullable=true, updatable=false)
private Integer mySearchQueryStringHash;
@Enumerated(EnumType.ORDINAL)
@Column(name="SEARCH_TYPE", nullable=false)
@ -109,6 +121,13 @@ public class Search implements Serializable {
@Column(name="SEARCH_UUID", length=40, nullable=false, updatable=false)
private String myUuid;
/**
* Constructor
*/
public Search() {
super();
}
public Date getCreated() {
return myCreated;
}
@ -164,6 +183,14 @@ public class Search implements Serializable {
return myResourceType;
}
public Date getSearchLastReturned() {
return mySearchLastReturned;
}
public String getSearchQueryString() {
return mySearchQueryString;
}
public SearchTypeEnum getSearchType() {
return mySearchType;
}
@ -184,11 +211,11 @@ public class Search implements Serializable {
myCreated = theCreated;
}
public void setFailureCode(Integer theFailureCode) {
myFailureCode = theFailureCode;
}
public void setFailureMessage(String theFailureMessage) {
myFailureMessage = left(theFailureMessage, FAILURE_MESSAGE_LENGTH);
}
@ -197,7 +224,6 @@ public class Search implements Serializable {
myLastUpdatedLow = theLowerBound;
myLastUpdatedHigh = theUpperBound;
}
public void setLastUpdated(DateRangeParam theLastUpdated) {
if (theLastUpdated == null) {
myLastUpdatedLow = null;
@ -207,6 +233,7 @@ public class Search implements Serializable {
myLastUpdatedHigh = theLastUpdated.getUpperBoundAsInstant();
}
}
public void setNumFound(int theNumFound) {
myNumFound = theNumFound;
}
@ -219,10 +246,22 @@ public class Search implements Serializable {
myResourceId = theResourceId;
}
public void setResourceType(String theResourceType) {
myResourceType = theResourceType;
}
public void setSearchLastReturned(Date theDate) {
mySearchLastReturned = theDate;
}
public void setSearchQueryString(String theSearchQueryString) {
mySearchQueryString = theSearchQueryString;
}
public void setSearchQueryStringHash(Integer theSearchQueryStringHash) {
mySearchQueryStringHash = theSearchQueryStringHash;
}
public void setSearchType(SearchTypeEnum theSearchType) {
mySearchType = theSearchType;

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.search;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
@ -39,6 +40,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IDao;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
@ -78,7 +80,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private ISearchDao mySearchDao;
@Autowired
private ISearchIncludeDao mySearchIncludeDao;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private ISearchResultDao mySearchResultDao;
@ -161,13 +164,14 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
for (SearchResult next : searchResults) {
resultPids.add(next.getResourcePid());
}
return resultPids; }
return resultPids;
}
});
return retVal;
}
@Override
public IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType) {
public IBundleProvider registerSearch(final IDao theCallingDao, SearchParameterMap theParams, String theResourceType) {
StopWatch w = new StopWatch();
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
@ -206,9 +210,54 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
return new SimpleBundleProvider(resources);
}
/*
* See if there are any cached searches whose results we can return
* instead
*/
final String queryString = theParams.toNormalizedQueryString(myContext);
if (theParams.getEverythingMode() == null) {
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) {
final Date createdCutoff = new Date(System.currentTimeMillis() - myDaoConfig.getReuseCachedSearchResultsForMillis());
final String resourceType = theResourceType;
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
PersistedJpaBundleProvider foundSearchProvider = txTemplate.execute(new TransactionCallback<PersistedJpaBundleProvider>() {
@Override
public PersistedJpaBundleProvider doInTransaction(TransactionStatus theStatus) {
Search searchToUse = null;
Collection<Search> candidates = mySearchDao.find(resourceType, queryString.hashCode(), createdCutoff);
for (Search nextCandidateSearch : candidates) {
if (queryString.equals(nextCandidateSearch.getSearchQueryString())) {
searchToUse = nextCandidateSearch;
}
}
PersistedJpaBundleProvider retVal = null;
if (searchToUse != null) {
ourLog.info("Reusing search {} from cache", searchToUse.getUuid());
searchToUse.setSearchLastReturned(new Date());
mySearchDao.updateSearchLastReturned(searchToUse.getId(), new Date());
retVal = new PersistedJpaBundleProvider(searchToUse.getUuid(), theCallingDao);
populateBundleProvider(retVal);
}
return retVal;
}
});
if (foundSearchProvider != null) {
return foundSearchProvider;
}
}
}
Search search = new Search();
search.setUuid(UUID.randomUUID().toString());
search.setCreated(new Date());
search.setSearchLastReturned(new Date());
search.setTotalCount(null);
search.setNumFound(0);
search.setPreferredPageSize(theParams.getCount());
@ -217,6 +266,9 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
search.setResourceType(theResourceType);
search.setStatus(SearchStatusEnum.LOADING);
search.setSearchQueryString(queryString);
search.setSearchQueryStringHash(queryString.hashCode());
for (Include next : theParams.getIncludes()) {
search.getIncludes().add(new SearchInclude(search, next.getValue(), false, next.isRecurse()));
}
@ -229,17 +281,21 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myExecutor.submit(task);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, theCallingDao, task, sb, myTxManager);
retVal.setContext(myContext);
retVal.setEntityManager(myEntityManager);
retVal.setPlatformTransactionManager(myTxManager);
retVal.setSearchDao(mySearchDao);
retVal.setSearchCoordinatorSvc(this);
populateBundleProvider(retVal);
ourLog.info("Search initial phase completed in {}ms", w);
return retVal;
}
private void populateBundleProvider(PersistedJpaBundleProvider theRetVal) {
theRetVal.setContext(myContext);
theRetVal.setEntityManager(myEntityManager);
theRetVal.setPlatformTransactionManager(myTxManager);
theRetVal.setSearchDao(mySearchDao);
theRetVal.setSearchCoordinatorSvc(this);
}
@VisibleForTesting
void setContextForUnitTest(FhirContext theCtx) {
myContext = theCtx;

View File

@ -74,10 +74,22 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void pollForStaleSearchesAndDeleteThem() {
Date cutoff = new Date(System.currentTimeMillis() - myDaoConfig.getExpireSearchResultsAfterMillis());
/*
* We give a bit of extra leeway just to avoid race conditions where a query result
* is being reused (because a new client request came in with the same params) right before
* the result is to be deleted
*/
long slack = 10 * DateUtils.MILLIS_PER_SECOND;
long cutoffMillis = myDaoConfig.getExpireSearchResultsAfterMillis();
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) {
cutoffMillis = Math.max(cutoffMillis, myDaoConfig.getReuseCachedSearchResultsForMillis());
}
Date cutoff = new Date((System.currentTimeMillis() - cutoffMillis) - slack);
ourLog.debug("Searching for searches which are before {}", cutoff);
Collection<Search> toDelete = mySearchDao.findWhereCreatedBefore(cutoff);
Collection<Search> toDelete = mySearchDao.findWhereLastReturnedBefore(cutoff);
if (!toDelete.isEmpty()) {
for (final Search next : toDelete) {

View File

@ -9,8 +9,12 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
public class SearchParameterMapTest {
@ -23,7 +27,7 @@ public class SearchParameterMapTest {
@Test
public void testToQueryString() {
public void testToQueryStringAndOr() {
SearchParameterMap map = new SearchParameterMap();
StringAndListParam familyAnd = new StringAndListParam()
@ -39,6 +43,72 @@ public class SearchParameterMapTest {
String queryString = map.toNormalizedQueryString(ourCtx);
ourLog.info(queryString);
assertEquals("?birthdate=ge2001&birthdate=lt2002&name=bouvier,simpson&name=homer,jay&name:exact=ZZZ%3F", queryString);
assertEquals("?birthdate=ge2001&birthdate=lt2002&name=bouvier,simpson&name=homer,jay&name:exact=ZZZ?", UrlUtil.unescape(queryString));
}
@Test
public void testToQueryStringEmpty() {
SearchParameterMap map = new SearchParameterMap();
String queryString = map.toNormalizedQueryString(ourCtx);
ourLog.info(queryString);
assertEquals("", queryString);
assertEquals("", UrlUtil.unescape(queryString));
}
@Test
public void testToQueryStringInclude() {
SearchParameterMap map = new SearchParameterMap();
map.add("birthdate", new DateParam(ParamPrefixEnum.APPROXIMATE, "2011"));
map.addInclude(new Include("Patient:subject"));
map.addInclude(new Include("Patient:aartvark", true));
map.addInclude(new Include("Patient:aartvark:z"));
map.addInclude(new Include("Patient:aartvark:a"));
String queryString = map.toNormalizedQueryString(ourCtx);
ourLog.info(queryString);
ourLog.info(UrlUtil.unescape(queryString));
assertEquals("?birthdate=ap2011&_include=Patient:aartvark&_include=Patient:aartvark:a&_include=Patient:aartvark:z&_include=Patient:subject", queryString);
assertEquals("?birthdate=ap2011&_include=Patient:aartvark&_include=Patient:aartvark:a&_include=Patient:aartvark:z&_include=Patient:subject", UrlUtil.unescape(queryString));
}
@Test
public void testToQueryStringRevInclude() {
SearchParameterMap map = new SearchParameterMap();
map.add("birthdate", new DateParam(ParamPrefixEnum.APPROXIMATE, "2011"));
map.addRevInclude(new Include("Patient:subject"));
map.addRevInclude(new Include("Patient:aartvark", true));
map.addRevInclude(new Include("Patient:aartvark:z"));
map.addRevInclude(new Include("Patient:aartvark:a"));
String queryString = map.toNormalizedQueryString(ourCtx);
ourLog.info(queryString);
ourLog.info(UrlUtil.unescape(queryString));
assertEquals("?birthdate=ap2011&_revinclude=Patient:aartvark&_revinclude=Patient:aartvark:a&_revinclude=Patient:aartvark:z&_revinclude=Patient:subject", queryString);
assertEquals("?birthdate=ap2011&_revinclude=Patient:aartvark&_revinclude=Patient:aartvark:a&_revinclude=Patient:aartvark:z&_revinclude=Patient:subject", UrlUtil.unescape(queryString));
}
@Test
public void testToQueryStringSort() {
SearchParameterMap map = new SearchParameterMap();
TokenAndListParam tokenAnd = new TokenAndListParam()
.addAnd(new TokenOrListParam().add(new TokenParam("SYS", "|VAL"))); // | needs escaping
map.add("identifier", tokenAnd);
map.setSort(new SortSpec("name").setChain(new SortSpec("identifier", SortOrderEnum.DESC)));
String queryString = map.toNormalizedQueryString(ourCtx);
ourLog.info(queryString);
ourLog.info(UrlUtil.unescape(queryString));
assertEquals("?identifier=SYS%7C%5C%7CVAL&_sort=name,-identifier", queryString);
assertEquals("?identifier=SYS|\\|VAL&_sort=name,-identifier", UrlUtil.unescape(queryString));
}
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMapTest.class);

View File

@ -29,6 +29,7 @@ import org.springframework.web.servlet.DispatcherServlet;
import ca.uhn.fhir.jpa.config.dstu3.WebsocketDstu3Config;
import ca.uhn.fhir.jpa.config.dstu3.WebsocketDstu3DispatcherConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -54,6 +55,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
protected static String ourServerBase;
private static GenericWebApplicationContext ourWebApplicationContext;
private TerminologyUploaderProviderDstu3 myTerminologyUploaderProvider;
protected static ISearchDao mySearchEntityDao;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
public BaseResourceProviderDstu3Test() {
@ -141,6 +143,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(subsServletHolder.getServlet().getServletConfig().getServletContext());
myValidationSupport = wac.getBean(JpaValidationSupportChainDstu3.class);
mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class);
mySearchEntityDao = wac.getBean(ISearchDao.class);
ourClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
ourClient.registerInterceptor(new LoggingInterceptor(true));

View File

@ -34,6 +34,7 @@ import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
@ -116,10 +117,14 @@ import org.junit.After;
import org.junit.AfterClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import com.google.common.collect.Lists;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.parser.IParser;
@ -153,6 +158,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
}
@Override
@ -316,18 +322,137 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
}
ourClient.transaction().withResources(resources).prettyPrint().encodedXml().execute();
//@formatter:on
Bundle found = ourClient.search().forResource(Organization.class).where(Organization.NAME.matches().value("rpdstu2_testCountParam_01")).count(10).returnBundle(Bundle.class).execute();
assertEquals(100, found.getTotal());
assertEquals(10, found.getEntry().size());
found = ourClient.search().forResource(Organization.class).where(Organization.NAME.matches().value("rpdstu2_testCountParam_01")).count(999).returnBundle(Bundle.class).execute();
//@formatter:on
assertEquals(100, found.getTotal());
assertEquals(50, found.getEntry().size());
}
@Test
public void testSearchReusesResultsEnabled() throws Exception {
List<IBaseResource> resources = new ArrayList<IBaseResource>();
for (int i = 0; i < 50; i++) {
Organization org = new Organization();
org.setName("HELLO");
resources.add(org);
}
ourClient.transaction().withResources(resources).prettyPrint().encodedXml().execute();
myDaoConfig.setReuseCachedSearchResultsForMillis(1000L);
Bundle result1 = ourClient
.search()
.forResource("Organization")
.where(Organization.NAME.matches().value("HELLO"))
.count(5)
.returnBundle(Bundle.class)
.execute();
final String uuid1 = toSearchUuidFromLinkNext(result1);
Search search1 = newTxTemplate().execute(new TransactionCallback<Search>() {
@Override
public Search doInTransaction(TransactionStatus theStatus) {
return mySearchEntityDao.findByUuid(uuid1);
}
});
Date lastReturned1 = search1.getSearchLastReturned();
Bundle result2 = ourClient
.search()
.forResource("Organization")
.where(Organization.NAME.matches().value("HELLO"))
.count(5)
.returnBundle(Bundle.class)
.execute();
final String uuid2 = toSearchUuidFromLinkNext(result2);
Search search2 = newTxTemplate().execute(new TransactionCallback<Search>() {
@Override
public Search doInTransaction(TransactionStatus theStatus) {
return mySearchEntityDao.findByUuid(uuid2);
}
});
Date lastReturned2 = search2.getSearchLastReturned();
assertTrue(lastReturned2.getTime() > lastReturned1.getTime());
Thread.sleep(1500);
Bundle result3 = ourClient
.search()
.forResource("Organization")
.where(Organization.NAME.matches().value("HELLO"))
.count(5)
.returnBundle(Bundle.class)
.execute();
String uuid3 = toSearchUuidFromLinkNext(result3);
assertEquals(uuid1, uuid2);
assertNotEquals(uuid1, uuid3);
}
@Test
public void testSearchReusesResultsDisabled() throws Exception {
List<IBaseResource> resources = new ArrayList<IBaseResource>();
for (int i = 0; i < 50; i++) {
Organization org = new Organization();
org.setName("HELLO");
resources.add(org);
}
ourClient.transaction().withResources(resources).prettyPrint().encodedXml().execute();
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
Bundle result1 = ourClient
.search()
.forResource("Organization")
.where(Organization.NAME.matches().value("HELLO"))
.count(5)
.returnBundle(Bundle.class)
.execute();
final String uuid1 = toSearchUuidFromLinkNext(result1);
Bundle result2 = ourClient
.search()
.forResource("Organization")
.where(Organization.NAME.matches().value("HELLO"))
.count(5)
.returnBundle(Bundle.class)
.execute();
final String uuid2 = toSearchUuidFromLinkNext(result2);
Bundle result3 = ourClient
.search()
.forResource("Organization")
.where(Organization.NAME.matches().value("HELLO"))
.count(5)
.returnBundle(Bundle.class)
.execute();
String uuid3 = toSearchUuidFromLinkNext(result3);
assertNotEquals(uuid1, uuid2);
assertNotEquals(uuid1, uuid3);
}
private String toSearchUuidFromLinkNext(Bundle theBundle) {
String linkNext = theBundle.getLink("next").getUrl();
linkNext = linkNext.substring(linkNext.indexOf('?'));
Map<String, String[]> params = UrlUtil.parseQueryString(linkNext);
String[] uuidParams = params.get(Constants.PARAM_PAGINGACTION);
String uuid = uuidParams[0];
return uuid;
}
/**
* See #438
*/