Work on JPA performance
This commit is contained in:
parent
716fa56b8f
commit
c311a0b3bf
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -535,6 +557,21 @@ public class DaoConfig {
|
|||
public void setResourceEncoding(ResourceEncodingEnum theResourceEncoding) {
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -54,7 +55,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
private Integer myLoadSynchronousUpTo;
|
||||
private Set<Include> myRevIncludes;
|
||||
private SortSpec mySort;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -56,7 +57,7 @@ public class Search implements Serializable {
|
|||
|
||||
@Column(name="FAILURE_CODE", nullable=true)
|
||||
private Integer myFailureCode;
|
||||
|
||||
|
||||
@Column(name="FAILURE_MESSAGE", length=FAILURE_MESSAGE_LENGTH, nullable=true)
|
||||
private String myFailureMessage;
|
||||
|
||||
|
@ -76,7 +77,7 @@ public class Search implements Serializable {
|
|||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name="LAST_UPDATED_LOW", nullable=true, insertable=true, updatable=false)
|
||||
private Date myLastUpdatedLow;
|
||||
|
||||
|
||||
@Column(name="NUM_FOUND", nullable=false)
|
||||
private int myNumFound;
|
||||
|
||||
|
@ -85,29 +86,47 @@ public class Search implements Serializable {
|
|||
|
||||
@Column(name="RESOURCE_ID", nullable=true)
|
||||
private Long myResourceId;
|
||||
|
||||
|
||||
@Column(name="RESOURCE_TYPE", length=200, nullable=true)
|
||||
private String myResourceType;
|
||||
|
||||
@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)
|
||||
private SearchTypeEnum mySearchType;
|
||||
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name="SEARCH_STATUS", nullable=false, length=10)
|
||||
private SearchStatusEnum myStatus;
|
||||
|
||||
|
||||
@Column(name="TOTAL_COUNT", nullable=true)
|
||||
private Integer myTotalCount;
|
||||
|
||||
|
||||
@Column(name="SEARCH_UUID", length=40, nullable=false, updatable=false)
|
||||
private String myUuid;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public Search() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return myCreated;
|
||||
|
@ -163,7 +182,15 @@ public class Search implements Serializable {
|
|||
public String getResourceType() {
|
||||
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,22 +233,35 @@ public class Search implements Serializable {
|
|||
myLastUpdatedHigh = theLastUpdated.getUpperBoundAsInstant();
|
||||
}
|
||||
}
|
||||
|
||||
public void setNumFound(int theNumFound) {
|
||||
myNumFound = theNumFound;
|
||||
}
|
||||
|
||||
|
||||
public void setPreferredPageSize(Integer thePreferredPageSize) {
|
||||
myPreferredPageSize = thePreferredPageSize;
|
||||
}
|
||||
|
||||
|
||||
public void setResourceId(Long theResourceId) {
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -152,7 +155,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
}
|
||||
|
||||
final Search foundSearch = search;
|
||||
|
||||
|
||||
List<Long> retVal = txTemplate.execute(new TransactionCallback<List<Long>>() {
|
||||
@Override
|
||||
public List<Long> doInTransaction(TransactionStatus theStatus) {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,8 +43,74 @@ 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);
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,7 +143,8 @@ 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));
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue