Improve efficiency of JPA queries with _lastUpdated
This commit is contained in:
parent
8c0b665565
commit
b827823004
|
@ -58,20 +58,6 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
||||||
setRangeFromDatesInclusive(theLowerBound, theUpperBound);
|
setRangeFromDatesInclusive(theLowerBound, theUpperBound);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
|
|
||||||
*
|
|
||||||
* @param theLowerBound
|
|
||||||
* A qualified date param representing the lower date bound (optionally may include time), e.g.
|
|
||||||
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
|
|
||||||
* @param theUpperBound
|
|
||||||
* A qualified date param representing the upper date bound (optionally may include time), e.g.
|
|
||||||
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
|
|
||||||
*/
|
|
||||||
public DateRangeParam(DateTimeDt theLowerBound, DateTimeDt theUpperBound) {
|
|
||||||
setRangeFromDatesInclusive(theLowerBound, theUpperBound);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound
|
* Sets the range from a single date param. If theDateParam has no qualifier, treats it as the lower and upper bound
|
||||||
* (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the
|
* (e.g. 2011-01-02 would match any time on that day). If theDateParam has a qualifier, treats it as either the
|
||||||
|
@ -106,6 +92,20 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
||||||
validateAndThrowDataFormatExceptionIfInvalid();
|
validateAndThrowDataFormatExceptionIfInvalid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
|
||||||
|
*
|
||||||
|
* @param theLowerBound
|
||||||
|
* A qualified date param representing the lower date bound (optionally may include time), e.g.
|
||||||
|
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
|
||||||
|
* @param theUpperBound
|
||||||
|
* A qualified date param representing the upper date bound (optionally may include time), e.g.
|
||||||
|
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
|
||||||
|
*/
|
||||||
|
public DateRangeParam(DateTimeDt theLowerBound, DateTimeDt theUpperBound) {
|
||||||
|
setRangeFromDatesInclusive(theLowerBound, theUpperBound);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
|
* Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
|
||||||
*
|
*
|
||||||
|
@ -120,6 +120,39 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
||||||
setRangeFromDatesInclusive(theLowerBound, theUpperBound);
|
setRangeFromDatesInclusive(theLowerBound, theUpperBound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addParam(DateParam theParsed) throws InvalidRequestException {
|
||||||
|
if (theParsed.getComparator() == null) {
|
||||||
|
if (myLowerBound != null || myUpperBound != null) {
|
||||||
|
throw new InvalidRequestException("Can not have multiple date range parameters for the same param without a qualifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
myLowerBound = new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theParsed.getValueAsString());
|
||||||
|
myUpperBound = new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theParsed.getValueAsString());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
switch (theParsed.getComparator()) {
|
||||||
|
case GREATERTHAN:
|
||||||
|
case GREATERTHAN_OR_EQUALS:
|
||||||
|
if (myLowerBound != null) {
|
||||||
|
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
|
||||||
|
}
|
||||||
|
myLowerBound = theParsed;
|
||||||
|
break;
|
||||||
|
case LESSTHAN:
|
||||||
|
case LESSTHAN_OR_EQUALS:
|
||||||
|
if (myUpperBound != null) {
|
||||||
|
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
|
||||||
|
}
|
||||||
|
myUpperBound = theParsed;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidRequestException("Unknown comparator: " + theParsed.getComparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public DateParam getLowerBound() {
|
public DateParam getLowerBound() {
|
||||||
return myLowerBound;
|
return myLowerBound;
|
||||||
}
|
}
|
||||||
|
@ -182,6 +215,18 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean haveLowerBound() {
|
||||||
|
return myLowerBound != null && myLowerBound.isEmpty() == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean haveUpperBound() {
|
||||||
|
return myUpperBound != null && myUpperBound.isEmpty() == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
|
||||||
|
}
|
||||||
|
|
||||||
public void setLowerBound(DateParam theLowerBound) {
|
public void setLowerBound(DateParam theLowerBound) {
|
||||||
myLowerBound = theLowerBound;
|
myLowerBound = theLowerBound;
|
||||||
validateAndThrowDataFormatExceptionIfInvalid();
|
validateAndThrowDataFormatExceptionIfInvalid();
|
||||||
|
@ -275,37 +320,32 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addParam(DateParam theParsed) throws InvalidRequestException {
|
@Override
|
||||||
if (theParsed.getComparator() == null) {
|
public String toString() {
|
||||||
if (myLowerBound != null || myUpperBound != null) {
|
StringBuilder b = new StringBuilder();
|
||||||
throw new InvalidRequestException("Can not have multiple date range parameters for the same param without a qualifier");
|
b.append(getClass().getSimpleName());
|
||||||
|
b.append("[");
|
||||||
|
if (haveLowerBound()) {
|
||||||
|
if (myLowerBound.getComparator() != null) {
|
||||||
|
b.append(myLowerBound.getComparator().getCode());
|
||||||
}
|
}
|
||||||
|
b.append(myLowerBound.getValueAsString());
|
||||||
myLowerBound = new DateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, theParsed.getValueAsString());
|
|
||||||
myUpperBound = new DateParam(QuantityCompararatorEnum.LESSTHAN_OR_EQUALS, theParsed.getValueAsString());
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
switch (theParsed.getComparator()) {
|
|
||||||
case GREATERTHAN:
|
|
||||||
case GREATERTHAN_OR_EQUALS:
|
|
||||||
if (myLowerBound != null) {
|
|
||||||
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
|
|
||||||
}
|
|
||||||
myLowerBound = theParsed;
|
|
||||||
break;
|
|
||||||
case LESSTHAN:
|
|
||||||
case LESSTHAN_OR_EQUALS:
|
|
||||||
if (myUpperBound != null) {
|
|
||||||
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
|
|
||||||
}
|
|
||||||
myUpperBound = theParsed;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidRequestException("Unknown comparator: " + theParsed.getComparator());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (haveUpperBound()) {
|
||||||
|
if(haveLowerBound()) {
|
||||||
|
b.append(" ");
|
||||||
|
}
|
||||||
|
if (myUpperBound.getComparator() != null) {
|
||||||
|
b.append(myUpperBound.getComparator().getCode());
|
||||||
|
}
|
||||||
|
b.append(myUpperBound.getValueAsString());
|
||||||
|
} else {
|
||||||
|
if (!haveLowerBound()) {
|
||||||
|
b.append("empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.append("]");
|
||||||
|
return b.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateAndThrowDataFormatExceptionIfInvalid() {
|
private void validateAndThrowDataFormatExceptionIfInvalid() {
|
||||||
|
@ -354,40 +394,4 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean haveUpperBound() {
|
|
||||||
return myUpperBound != null && myUpperBound.isEmpty() == false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean haveLowerBound() {
|
|
||||||
return myLowerBound != null && myLowerBound.isEmpty() == false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringBuilder b = new StringBuilder();
|
|
||||||
b.append(getClass().getSimpleName());
|
|
||||||
b.append("[");
|
|
||||||
if (haveLowerBound()) {
|
|
||||||
if (myLowerBound.getComparator() != null) {
|
|
||||||
b.append(myLowerBound.getComparator().getCode());
|
|
||||||
}
|
|
||||||
b.append(myLowerBound.getValueAsString());
|
|
||||||
}
|
|
||||||
if (haveUpperBound()) {
|
|
||||||
if(haveLowerBound()) {
|
|
||||||
b.append(" ");
|
|
||||||
}
|
|
||||||
if (myUpperBound.getComparator() != null) {
|
|
||||||
b.append(myUpperBound.getComparator().getCode());
|
|
||||||
}
|
|
||||||
b.append(myUpperBound.getValueAsString());
|
|
||||||
} else {
|
|
||||||
if (!haveLowerBound()) {
|
|
||||||
b.append("empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.append("]");
|
|
||||||
return b.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -772,7 +772,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao {
|
||||||
}
|
}
|
||||||
|
|
||||||
IFhirResourceDao<R> dao = getDao(theResourceType);
|
IFhirResourceDao<R> dao = getDao(theResourceType);
|
||||||
Set<Long> ids = dao.searchForIdsWithAndOr(paramMap, new HashSet<Long>());
|
Set<Long> ids = dao.searchForIdsWithAndOr(paramMap, new HashSet<Long>(), paramMap.getLastUpdated());
|
||||||
|
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,7 +228,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
codePredicates.add(p);
|
codePredicates.add(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
||||||
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
||||||
|
@ -243,7 +243,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
return new HashSet<Long>(q.getResultList());
|
return new HashSet<Long>(q.getResultList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Long> addPredicateId(Set<Long> theExistingPids, Set<Long> thePids) {
|
private Set<Long> addPredicateId(Set<Long> theExistingPids, Set<Long> thePids, DateRangeParam theLastUpdated) {
|
||||||
if (thePids == null || thePids.isEmpty()) {
|
if (thePids == null || thePids.isEmpty()) {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
@ -253,10 +253,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
||||||
cq.select(from.get("myId").as(Long.class));
|
cq.select(from.get("myId").as(Long.class));
|
||||||
|
|
||||||
Predicate typePredicate = builder.equal(from.get("myResourceType"), myResourceName);
|
List<Predicate> predicates = new ArrayList<Predicate>();
|
||||||
Predicate idPrecidate = from.get("myId").in(thePids);
|
predicates.add(builder.equal(from.get("myResourceType"), myResourceName));
|
||||||
|
predicates.add(from.get("myId").in(thePids));
|
||||||
cq.where(builder.and(typePredicate, idPrecidate));
|
predicates.addAll(createLastUpdatedPredicates(theLastUpdated, builder, from));
|
||||||
|
|
||||||
|
cq.where(toArray(predicates));
|
||||||
|
|
||||||
TypedQuery<Long> q = myEntityManager.createQuery(cq);
|
TypedQuery<Long> q = myEntityManager.createQuery(cq);
|
||||||
HashSet<Long> found = new HashSet<Long>(q.getResultList());
|
HashSet<Long> found = new HashSet<Long>(q.getResultList());
|
||||||
|
@ -272,7 +274,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
// IQueryParameterType> theList) {
|
// IQueryParameterType> theList) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
private Set<Long> addPredicateLanguage(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList) {
|
private Set<Long> addPredicateLanguage(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList, DateRangeParam theLastUpdated) {
|
||||||
Set<Long> retVal = thePids;
|
Set<Long> retVal = thePids;
|
||||||
if (theList == null || theList.isEmpty()) {
|
if (theList == null || theList.isEmpty()) {
|
||||||
return retVal;
|
return retVal;
|
||||||
|
@ -301,17 +303,18 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate typePredicate = builder.equal(from.get("myResourceType"), myResourceName);
|
List<Predicate> predicates = new ArrayList<Predicate>();
|
||||||
Predicate langPredicate = from.get("myLanguage").as(String.class).in(values);
|
predicates.add(builder.equal(from.get("myResourceType"), myResourceName));
|
||||||
Predicate masterCodePredicate = builder.and(typePredicate, langPredicate);
|
predicates.add(from.get("myLanguage").as(String.class).in(values));
|
||||||
Predicate notDeletedPredicate = builder.isNull(from.get("myDeleted"));
|
|
||||||
|
|
||||||
if (retVal.size() > 0) {
|
if (retVal.size() > 0) {
|
||||||
Predicate inPids = (from.get("myId").in(retVal));
|
Predicate inPids = (from.get("myId").in(retVal));
|
||||||
cq.where(builder.and(masterCodePredicate, inPids, notDeletedPredicate));
|
predicates.add(inPids);
|
||||||
} else {
|
}
|
||||||
cq.where(builder.and(masterCodePredicate, notDeletedPredicate));
|
|
||||||
}
|
predicates.add(builder.isNull(from.get("myDeleted")));
|
||||||
|
|
||||||
|
cq.where(toArray(predicates));
|
||||||
|
|
||||||
TypedQuery<Long> q = myEntityManager.createQuery(cq);
|
TypedQuery<Long> q = myEntityManager.createQuery(cq);
|
||||||
retVal = new HashSet<Long>(q.getResultList());
|
retVal = new HashSet<Long>(q.getResultList());
|
||||||
|
@ -413,7 +416,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
||||||
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
||||||
|
@ -592,7 +595,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
||||||
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
||||||
|
@ -746,7 +749,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = createResourceLinkPathPredicate(theParamName, builder, from);
|
Predicate type = createResourceLinkPathPredicate(theParamName, builder, from);
|
||||||
if (pidsToRetain.size() > 0) {
|
if (pidsToRetain.size() > 0) {
|
||||||
|
@ -785,7 +788,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
codePredicates.add(singleCode);
|
codePredicates.add(singleCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
||||||
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
||||||
|
@ -800,7 +803,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
return new HashSet<Long>(q.getResultList());
|
return new HashSet<Long>(q.getResultList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Long> addPredicateTag(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList, String theParamName) {
|
private Set<Long> addPredicateTag(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList, String theParamName, DateRangeParam theLastUpdated) {
|
||||||
Set<Long> pids = thePids;
|
Set<Long> pids = thePids;
|
||||||
if (theList == null || theList.isEmpty()) {
|
if (theList == null || theList.isEmpty()) {
|
||||||
return pids;
|
return pids;
|
||||||
|
@ -874,14 +877,17 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
|
|
||||||
}
|
}
|
||||||
if (orPredicates.isEmpty() == false) {
|
if (orPredicates.isEmpty() == false) {
|
||||||
andPredicates.add(builder.or(orPredicates.toArray(new Predicate[0])));
|
andPredicates.add(builder.or(toArray(orPredicates)));
|
||||||
}
|
}
|
||||||
|
|
||||||
From<ResourceTag, ResourceTable> defJoin = from.join("myResource");
|
From<ResourceTag, ResourceTable> defJoin = from.join("myResource");
|
||||||
Predicate notDeletedPredicatePrediate = builder.isNull(defJoin.get("myDeleted"));
|
Predicate notDeletedPredicatePrediate = builder.isNull(defJoin.get("myDeleted"));
|
||||||
andPredicates.add(notDeletedPredicatePrediate);
|
andPredicates.add(notDeletedPredicatePrediate);
|
||||||
|
if (theLastUpdated != null) {
|
||||||
Predicate masterCodePredicate = builder.and(andPredicates.toArray(new Predicate[0]));
|
andPredicates.addAll(createLastUpdatedPredicates(theLastUpdated, builder, defJoin));
|
||||||
|
}
|
||||||
|
|
||||||
|
Predicate masterCodePredicate = builder.and(toArray(andPredicates));
|
||||||
|
|
||||||
if (pids.size() > 0) {
|
if (pids.size() > 0) {
|
||||||
Predicate inPids = (from.get("myResourceId").in(pids));
|
Predicate inPids = (from.get("myResourceId").in(pids));
|
||||||
|
@ -928,7 +934,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
codePredicates.add(singleCode);
|
codePredicates.add(singleCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
||||||
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
||||||
|
@ -943,6 +949,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
return new HashSet<Long>(q.getResultList());
|
return new HashSet<Long>(q.getResultList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Predicate[] toArray(List<Predicate> thePredicates) {
|
||||||
|
return thePredicates.toArray(new Predicate[thePredicates.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
private Set<Long> addPredicateUri(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
|
private Set<Long> addPredicateUri(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
|
||||||
if (theList == null || theList.isEmpty()) {
|
if (theList == null || theList.isEmpty()) {
|
||||||
return thePids;
|
return thePids;
|
||||||
|
@ -981,7 +991,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate masterCodePredicate = builder.or(codePredicates.toArray(new Predicate[0]));
|
Predicate masterCodePredicate = builder.or(toArray(codePredicates));
|
||||||
|
|
||||||
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
Predicate type = builder.equal(from.get("myResourceType"), myResourceName);
|
||||||
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
Predicate name = builder.equal(from.get("myParamName"), theParamName);
|
||||||
|
@ -1227,7 +1237,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
} else {
|
} else {
|
||||||
singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
|
singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
|
||||||
}
|
}
|
||||||
Predicate singleCode = theBuilder.and(singleCodePredicates.toArray(new Predicate[0]));
|
Predicate singleCode = theBuilder.and(toArray(singleCodePredicates));
|
||||||
return singleCode;
|
return singleCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1662,87 +1672,87 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// @Override
|
||||||
public IBundleProvider everything(IIdType theId) {
|
// public IBundleProvider everything(IIdType theId) {
|
||||||
Search search = new Search();
|
// Search search = new Search();
|
||||||
search.setUuid(UUID.randomUUID().toString());
|
// search.setUuid(UUID.randomUUID().toString());
|
||||||
search.setCreated(new Date());
|
// search.setCreated(new Date());
|
||||||
myEntityManager.persist(search);
|
// myEntityManager.persist(search);
|
||||||
|
//
|
||||||
List<SearchResult> results = new ArrayList<SearchResult>();
|
// List<SearchResult> results = new ArrayList<SearchResult>();
|
||||||
if (theId != null) {
|
// if (theId != null) {
|
||||||
Long pid = translateForcedIdToPid(theId);
|
// Long pid = translateForcedIdToPid(theId);
|
||||||
ResourceTable entity = myEntityManager.find(ResourceTable.class, pid);
|
// ResourceTable entity = myEntityManager.find(ResourceTable.class, pid);
|
||||||
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
|
// validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
|
||||||
SearchResult res = new SearchResult(search);
|
// SearchResult res = new SearchResult(search);
|
||||||
res.setResourcePid(pid);
|
// res.setResourcePid(pid);
|
||||||
results.add(res);
|
// results.add(res);
|
||||||
} else {
|
// } else {
|
||||||
TypedQuery<Tuple> query = createSearchAllByTypeQuery();
|
// TypedQuery<Tuple> query = createSearchAllByTypeQuery();
|
||||||
for (Tuple next : query.getResultList()) {
|
// for (Tuple next : query.getResultList()) {
|
||||||
SearchResult res = new SearchResult(search);
|
// SearchResult res = new SearchResult(search);
|
||||||
res.setResourcePid(next.get(0, Long.class));
|
// res.setResourcePid(next.get(0, Long.class));
|
||||||
results.add(res);
|
// results.add(res);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
int totalCount = results.size();
|
// int totalCount = results.size();
|
||||||
mySearchResultDao.save(results);
|
// mySearchResultDao.save(results);
|
||||||
mySearchResultDao.flush();
|
// mySearchResultDao.flush();
|
||||||
|
//
|
||||||
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
|
// CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
|
||||||
|
//
|
||||||
// Load _revincludes
|
// // Load _revincludes
|
||||||
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
|
// CriteriaQuery<Long> cq = builder.createQuery(Long.class);
|
||||||
Root<ResourceLink> from = cq.from(ResourceLink.class);
|
// Root<ResourceLink> from = cq.from(ResourceLink.class);
|
||||||
cq.select(from.get("mySourceResourcePid").as(Long.class));
|
// cq.select(from.get("mySourceResourcePid").as(Long.class));
|
||||||
|
//
|
||||||
Subquery<Long> pidsSubquery = cq.subquery(Long.class);
|
// Subquery<Long> pidsSubquery = cq.subquery(Long.class);
|
||||||
Root<SearchResult> pidsSubqueryFrom = pidsSubquery.from(SearchResult.class);
|
// Root<SearchResult> pidsSubqueryFrom = pidsSubquery.from(SearchResult.class);
|
||||||
pidsSubquery.select(pidsSubqueryFrom.get("myResourcePid").as(Long.class));
|
// pidsSubquery.select(pidsSubqueryFrom.get("myResourcePid").as(Long.class));
|
||||||
pidsSubquery.where(pidsSubqueryFrom.get("mySearch").in(search));
|
// pidsSubquery.where(pidsSubqueryFrom.get("mySearch").in(search));
|
||||||
|
//
|
||||||
cq.where(from.get("myTargetResourceId").in(pidsSubquery));
|
// cq.where(from.get("myTargetResourceId").in(pidsSubquery));
|
||||||
TypedQuery<Long> query = myEntityManager.createQuery(cq);
|
// TypedQuery<Long> query = myEntityManager.createQuery(cq);
|
||||||
|
//
|
||||||
results = new ArrayList<SearchResult>();
|
// results = new ArrayList<SearchResult>();
|
||||||
for (Long next : query.getResultList()) {
|
// for (Long next : query.getResultList()) {
|
||||||
SearchResult res = new SearchResult(search);
|
// SearchResult res = new SearchResult(search);
|
||||||
res.setResourcePid(next);
|
// res.setResourcePid(next);
|
||||||
results.add(res);
|
// results.add(res);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Save _revincludes
|
// // Save _revincludes
|
||||||
totalCount += results.size();
|
// totalCount += results.size();
|
||||||
mySearchResultDao.save(results);
|
// mySearchResultDao.save(results);
|
||||||
mySearchResultDao.flush();
|
// mySearchResultDao.flush();
|
||||||
|
//
|
||||||
final int finalTotalCount = totalCount;
|
// final int finalTotalCount = totalCount;
|
||||||
return new IBundleProvider() {
|
// return new IBundleProvider() {
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public int size() {
|
// public int size() {
|
||||||
return finalTotalCount;
|
// return finalTotalCount;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public Integer preferredPageSize() {
|
// public Integer preferredPageSize() {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
|
// public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
|
||||||
// TODO Auto-generated method stub
|
// // TODO Auto-generated method stub
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public InstantDt getPublished() {
|
// public InstantDt getPublished() {
|
||||||
// TODO Auto-generated method stub
|
// // TODO Auto-generated method stub
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* THIS SHOULD RETURN HASHSET and not jsut Set because we add to it later (so it can't be Collections.emptySet())
|
* THIS SHOULD RETURN HASHSET and not jsut Set because we add to it later (so it can't be Collections.emptySet())
|
||||||
|
@ -2150,6 +2160,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
StopWatch w = new StopWatch();
|
StopWatch w = new StopWatch();
|
||||||
final InstantDt now = InstantDt.withCurrentTime();
|
final InstantDt now = InstantDt.withCurrentTime();
|
||||||
|
|
||||||
|
DateRangeParam lu = theParams.getLastUpdated();
|
||||||
|
if (lu != null && lu.isEmpty()) {
|
||||||
|
lu = null;
|
||||||
|
}
|
||||||
|
|
||||||
Collection<Long> loadPids;
|
Collection<Long> loadPids;
|
||||||
if (theParams.getEverythingMode() != null) {
|
if (theParams.getEverythingMode() != null) {
|
||||||
|
|
||||||
|
@ -2163,7 +2178,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
}
|
}
|
||||||
predicates.add(builder.equal(from.get("myResourceType"), myResourceName));
|
predicates.add(builder.equal(from.get("myResourceType"), myResourceName));
|
||||||
predicates.add(builder.isNull(from.get("myDeleted")));
|
predicates.add(builder.isNull(from.get("myDeleted")));
|
||||||
cq.where(builder.and(predicates.toArray(new Predicate[predicates.size()])));
|
cq.where(builder.and(toArray(predicates)));
|
||||||
|
|
||||||
Join<Object, Object> join = from.join("myIncomingResourceLinks", JoinType.LEFT);
|
Join<Object, Object> join = from.join("myIncomingResourceLinks", JoinType.LEFT);
|
||||||
cq.multiselect(from.get("myId").as(Long.class), join.get("mySourceResourcePid").as(Long.class));
|
cq.multiselect(from.get("myId").as(Long.class), join.get("mySourceResourcePid").as(Long.class));
|
||||||
|
@ -2181,7 +2196,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
} else if (theParams.isEmpty()) {
|
} else if (theParams.isEmpty()) {
|
||||||
|
|
||||||
loadPids = new HashSet<Long>();
|
loadPids = new HashSet<Long>();
|
||||||
TypedQuery<Tuple> query = createSearchAllByTypeQuery();
|
TypedQuery<Tuple> query = createSearchAllByTypeQuery(lu);
|
||||||
|
lu = null;
|
||||||
for (Tuple next : query.getResultList()) {
|
for (Tuple next : query.getResultList()) {
|
||||||
loadPids.add(next.get(0, Long.class));
|
loadPids.add(next.get(0, Long.class));
|
||||||
}
|
}
|
||||||
|
@ -2205,7 +2221,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
if (theParams.isEmpty()) {
|
if (theParams.isEmpty()) {
|
||||||
loadPids = searchResultPids;
|
loadPids = searchResultPids;
|
||||||
} else {
|
} else {
|
||||||
loadPids = searchForIdsWithAndOr(theParams, searchResultPids);
|
loadPids = searchForIdsWithAndOr(theParams, searchResultPids, lu);
|
||||||
}
|
}
|
||||||
if (loadPids.isEmpty()) {
|
if (loadPids.isEmpty()) {
|
||||||
return new SimpleBundleProvider();
|
return new SimpleBundleProvider();
|
||||||
|
@ -2222,9 +2238,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Handle _lastUpdated
|
// Handle _lastUpdated
|
||||||
final DateRangeParam lu = theParams.getLastUpdated();
|
if (lu != null) {
|
||||||
if (lu != null && (lu.getLowerBoundAsInstant() != null || lu.getUpperBoundAsInstant() != null)) {
|
|
||||||
|
|
||||||
List<Long> resultList = filterResourceIdsByLastUpdated(loadPids, lu);
|
List<Long> resultList = filterResourceIdsByLastUpdated(loadPids, lu);
|
||||||
loadPids.clear();
|
loadPids.clear();
|
||||||
for (Long next : resultList) {
|
for (Long next : resultList) {
|
||||||
|
@ -2271,7 +2285,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
|
|
||||||
// Load includes
|
// Load includes
|
||||||
pidsSubList = new ArrayList<Long>(pidsSubList);
|
pidsSubList = new ArrayList<Long>(pidsSubList);
|
||||||
revIncludedPids.addAll(loadReverseIncludes(pidsSubList, theParams.getIncludes(), false, null, lu));
|
revIncludedPids.addAll(loadReverseIncludes(pidsSubList, theParams.getIncludes(), false, null, theParams.getLastUpdated()));
|
||||||
|
|
||||||
// Execute the query and make sure we return distinct results
|
// Execute the query and make sure we return distinct results
|
||||||
List<IBaseResource> resources = new ArrayList<IBaseResource>();
|
List<IBaseResource> resources = new ArrayList<IBaseResource>();
|
||||||
|
@ -2305,29 +2319,44 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
||||||
cq.select(from.get("myId").as(Long.class));
|
cq.select(from.get("myId").as(Long.class));
|
||||||
|
|
||||||
Predicate predicateIds = (from.get("myId").in(thePids));
|
List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
|
||||||
Predicate predicateLower = theLastUpdated.getLowerBoundAsInstant() != null ? builder.greaterThanOrEqualTo(from.<Date> get("myUpdated"), theLastUpdated.getLowerBoundAsInstant()) : null;
|
lastUpdatedPredicates.add(0, from.get("myId").in(thePids));
|
||||||
Predicate predicateUpper = theLastUpdated.getUpperBoundAsInstant() != null ? builder.lessThanOrEqualTo(from.<Date> get("myUpdated"), theLastUpdated.getUpperBoundAsInstant()) : null;
|
|
||||||
if (predicateLower != null && predicateUpper != null) {
|
cq.where(toArray(lastUpdatedPredicates));
|
||||||
cq.where(predicateIds, predicateLower, predicateUpper);
|
|
||||||
} else if (predicateLower != null) {
|
|
||||||
cq.where(predicateIds, predicateLower);
|
|
||||||
} else {
|
|
||||||
cq.where(predicateIds, predicateUpper);
|
|
||||||
}
|
|
||||||
TypedQuery<Long> query = myEntityManager.createQuery(cq);
|
TypedQuery<Long> query = myEntityManager.createQuery(cq);
|
||||||
List<Long> resultList = query.getResultList();
|
List<Long> resultList = query.getResultList();
|
||||||
return resultList;
|
return resultList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypedQuery<Tuple> createSearchAllByTypeQuery() {
|
private List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
|
||||||
|
List<Predicate> lastUpdatedPredicates = new ArrayList<Predicate>();
|
||||||
|
if (theLastUpdated != null) {
|
||||||
|
if (theLastUpdated.getLowerBoundAsInstant() != null) {
|
||||||
|
Predicate predicateLower = builder.greaterThanOrEqualTo(from.<Date> get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
|
||||||
|
lastUpdatedPredicates.add(predicateLower);
|
||||||
|
}
|
||||||
|
if (theLastUpdated.getUpperBoundAsInstant() != null) {
|
||||||
|
Predicate predicateUpper = builder.lessThanOrEqualTo(from.<Date> get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
|
||||||
|
lastUpdatedPredicates.add(predicateUpper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastUpdatedPredicates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypedQuery<Tuple> createSearchAllByTypeQuery(DateRangeParam theLastUpdated) {
|
||||||
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
|
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
|
||||||
CriteriaQuery<Tuple> cq = builder.createTupleQuery();
|
CriteriaQuery<Tuple> cq = builder.createTupleQuery();
|
||||||
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
||||||
cq.multiselect(from.get("myId").as(Long.class));
|
cq.multiselect(from.get("myId").as(Long.class));
|
||||||
Predicate typeEquals = builder.equal(from.get("myResourceType"), myResourceName);
|
List<Predicate> predicates = new ArrayList<Predicate>();
|
||||||
Predicate notDeleted = builder.isNull(from.get("myDeleted"));
|
predicates.add(builder.equal(from.get("myResourceType"), myResourceName));
|
||||||
cq.where(builder.and(typeEquals, notDeleted));
|
predicates.add(builder.isNull(from.get("myDeleted")));
|
||||||
|
|
||||||
|
if (theLastUpdated != null) {
|
||||||
|
predicates.addAll(createLastUpdatedPredicates(theLastUpdated, builder, from));
|
||||||
|
}
|
||||||
|
|
||||||
|
cq.where(toArray(predicates));
|
||||||
|
|
||||||
TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
|
TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
|
||||||
return query;
|
return query;
|
||||||
|
@ -2348,7 +2377,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
Collection<Long> originalPids = theLoadPids;
|
Collection<Long> originalPids = theLoadPids;
|
||||||
LinkedHashSet<Long> loadPids = new LinkedHashSet<Long>();
|
LinkedHashSet<Long> loadPids = new LinkedHashSet<Long>();
|
||||||
cq.multiselect(from.get("myId").as(Long.class));
|
cq.multiselect(from.get("myId").as(Long.class));
|
||||||
cq.where(predicates.toArray(new Predicate[0]));
|
cq.where(toArray(predicates));
|
||||||
cq.orderBy(orders);
|
cq.orderBy(orders);
|
||||||
|
|
||||||
TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
|
TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
|
||||||
|
@ -2398,7 +2427,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
for (Entry<String, IQueryParameterType> nextEntry : theParams.entrySet()) {
|
for (Entry<String, IQueryParameterType> nextEntry : theParams.entrySet()) {
|
||||||
map.add(nextEntry.getKey(), (nextEntry.getValue()));
|
map.add(nextEntry.getKey(), (nextEntry.getValue()));
|
||||||
}
|
}
|
||||||
return searchForIdsWithAndOr(map, null);
|
return searchForIdsWithAndOr(map, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -2407,7 +2436,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Long> searchForIdsWithAndOr(SearchParameterMap theParams, Collection<Long> theInitialPids) {
|
public Set<Long> searchForIdsWithAndOr(SearchParameterMap theParams, Collection<Long> theInitialPids, DateRangeParam theLastUpdated) {
|
||||||
SearchParameterMap params = theParams;
|
SearchParameterMap params = theParams;
|
||||||
if (params == null) {
|
if (params == null) {
|
||||||
params = new SearchParameterMap();
|
params = new SearchParameterMap();
|
||||||
|
@ -2451,7 +2480,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pids = addPredicateId(pids, joinPids);
|
pids = addPredicateId(pids, joinPids, theLastUpdated);
|
||||||
if (pids.isEmpty()) {
|
if (pids.isEmpty()) {
|
||||||
return new HashSet<Long>();
|
return new HashSet<Long>();
|
||||||
}
|
}
|
||||||
|
@ -2466,11 +2495,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
|
||||||
|
|
||||||
} else if (nextParamName.equals(BaseResource.SP_RES_LANGUAGE)) {
|
} else if (nextParamName.equals(BaseResource.SP_RES_LANGUAGE)) {
|
||||||
|
|
||||||
pids = addPredicateLanguage(pids, nextParamEntry.getValue());
|
pids = addPredicateLanguage(pids, nextParamEntry.getValue(), theLastUpdated);
|
||||||
|
|
||||||
} else if (nextParamName.equals(Constants.PARAM_TAG) || nextParamName.equals(Constants.PARAM_PROFILE) || nextParamName.equals(Constants.PARAM_SECURITY)) {
|
} else if (nextParamName.equals(Constants.PARAM_TAG) || nextParamName.equals(Constants.PARAM_PROFILE) || nextParamName.equals(Constants.PARAM_SECURITY)) {
|
||||||
|
|
||||||
pids = addPredicateTag(pids, nextParamEntry.getValue(), nextParamName);
|
pids = addPredicateTag(pids, nextParamEntry.getValue(), nextParamName, theLastUpdated);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.persistence.Query;
|
import javax.persistence.Query;
|
||||||
import javax.persistence.TypedQuery;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
@ -77,6 +76,9 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISubscriptionTableDao mySubscriptionTableDao;
|
private ISubscriptionTableDao mySubscriptionTableDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PlatformTransactionManager myTxManager;
|
||||||
|
|
||||||
private void createSubscriptionTable(ResourceTable theEntity, Subscription theSubscription) {
|
private void createSubscriptionTable(ResourceTable theEntity, Subscription theSubscription) {
|
||||||
SubscriptionTable subscriptionEntity = new SubscriptionTable();
|
SubscriptionTable subscriptionEntity = new SubscriptionTable();
|
||||||
subscriptionEntity.setCreated(new Date());
|
subscriptionEntity.setCreated(new Date());
|
||||||
|
@ -87,15 +89,32 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
||||||
myEntityManager.persist(subscriptionEntity);
|
myEntityManager.persist(subscriptionEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
@Override
|
||||||
private PlatformTransactionManager myTxManager;
|
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId) {
|
||||||
|
ResourceTable entity = readEntityLatestVersion(theId);
|
||||||
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
|
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
|
||||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
if (table == null) {
|
||||||
public synchronized void pollForNewUndeliveredResourcesScheduler() {
|
return null;
|
||||||
pollForNewUndeliveredResources();
|
}
|
||||||
|
return table.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized List<IBaseResource> getUndeliveredResourcesAndPurge(Long theSubscriptionPid) {
|
||||||
|
List<IBaseResource> retVal = new ArrayList<IBaseResource>();
|
||||||
|
Page<SubscriptionFlaggedResource> flaggedResources = mySubscriptionFlaggedResourceDataDao.findAllBySubscriptionId(theSubscriptionPid, new PageRequest(0, 100));
|
||||||
|
for (SubscriptionFlaggedResource nextFlaggedResource : flaggedResources) {
|
||||||
|
retVal.add(toResource(nextFlaggedResource.getResource(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
mySubscriptionFlaggedResourceDataDao.delete(flaggedResources);
|
||||||
|
mySubscriptionFlaggedResourceDataDao.flush();
|
||||||
|
|
||||||
|
mySubscriptionTableDao.updateLastClientPoll(new Date());
|
||||||
|
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
public synchronized int pollForNewUndeliveredResources() {
|
public synchronized int pollForNewUndeliveredResources() {
|
||||||
|
@ -189,6 +208,12 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
||||||
return results.size();
|
return results.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
|
||||||
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
|
public synchronized void pollForNewUndeliveredResourcesScheduler() {
|
||||||
|
pollForNewUndeliveredResources();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void postPersist(ResourceTable theEntity, Subscription theSubscription) {
|
protected void postPersist(ResourceTable theEntity, Subscription theSubscription) {
|
||||||
super.postPersist(theEntity, theSubscription);
|
super.postPersist(theEntity, theSubscription);
|
||||||
|
@ -196,105 +221,6 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
||||||
createSubscriptionTable(theEntity, theSubscription);
|
createSubscriptionTable(theEntity, theSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ResourceTable updateEntity(IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion,
|
|
||||||
Date theUpdateTime) {
|
|
||||||
ResourceTable retVal = super.updateEntity(theResource, theEntity, theUpdateHistory, theDeletedTimestampOrNull, thePerformIndexing, theUpdateVersion, theUpdateTime);
|
|
||||||
|
|
||||||
Subscription resource = (Subscription) theResource;
|
|
||||||
Long resourceId = theEntity.getId();
|
|
||||||
if (theDeletedTimestampOrNull != null) {
|
|
||||||
Long subscriptionId = getSubscriptionTablePidForSubscriptionResource(theEntity.getIdDt());
|
|
||||||
if (subscriptionId != null) {
|
|
||||||
mySubscriptionFlaggedResourceDataDao.deleteAllForSubscription(subscriptionId);
|
|
||||||
mySubscriptionTableDao.deleteAllForSubscription(subscriptionId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Query q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_SET_STATUS");
|
|
||||||
q.setParameter("res_id", resourceId);
|
|
||||||
q.setParameter("status", resource.getStatusElement().getValueAsEnum());
|
|
||||||
if (q.executeUpdate() > 0) {
|
|
||||||
ourLog.info("Updated subscription status for subscription {} to {}", resourceId, resource.getStatusElement().getValueAsEnum());
|
|
||||||
} else {
|
|
||||||
createSubscriptionTable(retVal, resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void validateResourceForStorage(Subscription theResource, ResourceTable theEntityToSave) {
|
|
||||||
super.validateResourceForStorage(theResource, theEntityToSave);
|
|
||||||
|
|
||||||
RuntimeResourceDefinition resDef = validateCriteriaAndReturnResourceDefinition(theResource);
|
|
||||||
|
|
||||||
IFhirResourceDao<? extends IBaseResource> dao = getDao(resDef.getImplementingClass());
|
|
||||||
if (dao == null) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resDef);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (theResource.getChannel().getType() == null) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.channel.type must be populated on this server");
|
|
||||||
}
|
|
||||||
|
|
||||||
SubscriptionStatusEnum status = theResource.getStatusElement().getValueAsEnum();
|
|
||||||
if (status == null) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.status must be populated on this server");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
|
|
||||||
String query = theResource.getCriteria();
|
|
||||||
if (isBlank(query)) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be populated");
|
|
||||||
}
|
|
||||||
|
|
||||||
int sep = query.indexOf('?');
|
|
||||||
if (sep <= 1) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
String resType = query.substring(0, sep);
|
|
||||||
if (resType.contains("/")) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeResourceDefinition resDef;
|
|
||||||
try {
|
|
||||||
resDef = getContext().getResourceDefinition(resType);
|
|
||||||
} catch (DataFormatException e) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resType);
|
|
||||||
}
|
|
||||||
return resDef;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized List<IBaseResource> getUndeliveredResourcesAndPurge(Long theSubscriptionPid) {
|
|
||||||
List<IBaseResource> retVal = new ArrayList<IBaseResource>();
|
|
||||||
Page<SubscriptionFlaggedResource> flaggedResources = mySubscriptionFlaggedResourceDataDao.findAllBySubscriptionId(theSubscriptionPid, new PageRequest(0, 100));
|
|
||||||
for (SubscriptionFlaggedResource nextFlaggedResource : flaggedResources) {
|
|
||||||
retVal.add(toResource(nextFlaggedResource.getResource(), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
mySubscriptionFlaggedResourceDataDao.delete(flaggedResources);
|
|
||||||
mySubscriptionFlaggedResourceDataDao.flush();
|
|
||||||
|
|
||||||
mySubscriptionTableDao.updateLastClientPoll(new Date());
|
|
||||||
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId) {
|
|
||||||
ResourceTable entity = readEntityLatestVersion(theId);
|
|
||||||
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
|
|
||||||
if (table == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return table.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Scheduled(fixedDelay = DateUtils.MILLIS_PER_MINUTE)
|
@Scheduled(fixedDelay = DateUtils.MILLIS_PER_MINUTE)
|
||||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
@Override
|
@Override
|
||||||
|
@ -323,4 +249,77 @@ public class FhirResourceDaoSubscriptionDstu2 extends FhirResourceDaoDstu2<Subsc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ResourceTable updateEntity(IResource theResource, ResourceTable theEntity, boolean theUpdateHistory, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion,
|
||||||
|
Date theUpdateTime) {
|
||||||
|
ResourceTable retVal = super.updateEntity(theResource, theEntity, theUpdateHistory, theDeletedTimestampOrNull, thePerformIndexing, theUpdateVersion, theUpdateTime);
|
||||||
|
|
||||||
|
Subscription resource = (Subscription) theResource;
|
||||||
|
Long resourceId = theEntity.getId();
|
||||||
|
if (theDeletedTimestampOrNull != null) {
|
||||||
|
Long subscriptionId = getSubscriptionTablePidForSubscriptionResource(theEntity.getIdDt());
|
||||||
|
if (subscriptionId != null) {
|
||||||
|
mySubscriptionFlaggedResourceDataDao.deleteAllForSubscription(subscriptionId);
|
||||||
|
mySubscriptionTableDao.deleteAllForSubscription(subscriptionId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Query q = myEntityManager.createNamedQuery("Q_HFJ_SUBSCRIPTION_SET_STATUS");
|
||||||
|
q.setParameter("res_id", resourceId);
|
||||||
|
q.setParameter("status", resource.getStatusElement().getValueAsEnum());
|
||||||
|
if (q.executeUpdate() > 0) {
|
||||||
|
ourLog.info("Updated subscription status for subscription {} to {}", resourceId, resource.getStatusElement().getValueAsEnum());
|
||||||
|
} else {
|
||||||
|
createSubscriptionTable(retVal, resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(Subscription theResource) {
|
||||||
|
String query = theResource.getCriteria();
|
||||||
|
if (isBlank(query)) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.criteria must be populated");
|
||||||
|
}
|
||||||
|
|
||||||
|
int sep = query.indexOf('?');
|
||||||
|
if (sep <= 1) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
String resType = query.substring(0, sep);
|
||||||
|
if (resType.contains("/")) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeResourceDefinition resDef;
|
||||||
|
try {
|
||||||
|
resDef = getContext().getResourceDefinition(resType);
|
||||||
|
} catch (DataFormatException e) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resType);
|
||||||
|
}
|
||||||
|
return resDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validateResourceForStorage(Subscription theResource, ResourceTable theEntityToSave) {
|
||||||
|
super.validateResourceForStorage(theResource, theEntityToSave);
|
||||||
|
|
||||||
|
RuntimeResourceDefinition resDef = validateCriteriaAndReturnResourceDefinition(theResource);
|
||||||
|
|
||||||
|
IFhirResourceDao<? extends IBaseResource> dao = getDao(resDef.getImplementingClass());
|
||||||
|
if (dao == null) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theResource.getChannel().getType() == null) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.channel.type must be populated on this server");
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionStatusEnum status = theResource.getStatusElement().getValueAsEnum();
|
||||||
|
if (status == null) {
|
||||||
|
throw new UnprocessableEntityException("Subscription.status must be populated on this server");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import ca.uhn.fhir.model.api.TagList;
|
||||||
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
|
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.api.ValidationModeEnum;
|
import ca.uhn.fhir.rest.api.ValidationModeEnum;
|
||||||
|
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||||
import ca.uhn.fhir.rest.server.EncodingEnum;
|
import ca.uhn.fhir.rest.server.EncodingEnum;
|
||||||
import ca.uhn.fhir.rest.server.IBundleProvider;
|
import ca.uhn.fhir.rest.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
|
@ -123,7 +124,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
|
||||||
|
|
||||||
Set<Long> searchForIds(String theParameterName, IQueryParameterType theValue);
|
Set<Long> searchForIds(String theParameterName, IQueryParameterType theValue);
|
||||||
|
|
||||||
Set<Long> searchForIdsWithAndOr(SearchParameterMap theParams, Collection<Long> theInitialPids);
|
Set<Long> searchForIdsWithAndOr(SearchParameterMap theParams, Collection<Long> theInitialPids, DateRangeParam theLastUpdated);
|
||||||
|
|
||||||
DaoMethodOutcome update(T theResource);
|
DaoMethodOutcome update(T theResource);
|
||||||
|
|
||||||
|
@ -146,9 +147,9 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
|
||||||
*/
|
*/
|
||||||
DaoMethodOutcome deleteByUrl(String theUrl, boolean theTransaction);
|
DaoMethodOutcome deleteByUrl(String theUrl, boolean theTransaction);
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Invoke the everything operation
|
// * Invoke the everything operation
|
||||||
*/
|
// */
|
||||||
IBundleProvider everything(IIdType theId);
|
// IBundleProvider everything(IIdType theId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -399,6 +399,7 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
|
||||||
patient.addIdentifier().setSystem("urn:system").setValue("001");
|
patient.addIdentifier().setSystem("urn:system").setValue("001");
|
||||||
id1 = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
|
id1 = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
|
||||||
}
|
}
|
||||||
|
long betweenTime = System.currentTimeMillis();
|
||||||
IIdType id2;
|
IIdType id2;
|
||||||
{
|
{
|
||||||
Patient patient = new Patient();
|
Patient patient = new Patient();
|
||||||
|
@ -418,6 +419,13 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
|
||||||
params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam("999999999999")));
|
params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam("999999999999")));
|
||||||
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1));
|
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1));
|
||||||
|
|
||||||
|
// With lastupdated
|
||||||
|
|
||||||
|
params = new SearchParameterMap();
|
||||||
|
params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam(id2.getIdPart())));
|
||||||
|
params.setLastUpdated(new DateRangeParam(new Date(betweenTime), null));
|
||||||
|
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id2));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -610,6 +618,9 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
|
||||||
patient.addName().addFamily("testSearchLanguageParam").addGiven("Joe");
|
patient.addName().addFamily("testSearchLanguageParam").addGiven("Joe");
|
||||||
id1 = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
|
id1 = myPatientDao.create(patient).getId().toUnqualifiedVersionless();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Date betweenTime = new Date();
|
||||||
|
|
||||||
IIdType id2;
|
IIdType id2;
|
||||||
{
|
{
|
||||||
Patient patient = new Patient();
|
Patient patient = new Patient();
|
||||||
|
@ -623,6 +634,12 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
|
||||||
params.add(BaseResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("en_US")));
|
params.add(BaseResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("en_US")));
|
||||||
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1, id2));
|
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1, id2));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
|
params.add(BaseResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("en_US")));
|
||||||
|
params.setLastUpdated(new DateRangeParam(betweenTime, null));
|
||||||
|
assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id2));
|
||||||
|
}
|
||||||
{
|
{
|
||||||
SearchParameterMap params = new SearchParameterMap();
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
params.add(BaseResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ")));
|
params.add(BaseResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ")));
|
||||||
|
@ -1943,6 +1960,9 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
|
||||||
ResourceMetadataKeyEnum.TAG_LIST.put(org, tagList);
|
ResourceMetadataKeyEnum.TAG_LIST.put(org, tagList);
|
||||||
tag1id = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
|
tag1id = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Date betweenDate = new Date();
|
||||||
|
|
||||||
IIdType tag2id;
|
IIdType tag2id;
|
||||||
{
|
{
|
||||||
Organization org = new Organization();
|
Organization org = new Organization();
|
||||||
|
@ -1978,6 +1998,17 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test {
|
||||||
List<IIdType> patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params));
|
List<IIdType> patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params));
|
||||||
assertThat(patients, containsInAnyOrder(tag1id, tag2id));
|
assertThat(patients, containsInAnyOrder(tag1id, tag2id));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// Or tags with lastupdated
|
||||||
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
|
TokenOrListParam orListParam = new TokenOrListParam();
|
||||||
|
orListParam.add(new TokenParam("urn:taglist", methodName + "1a"));
|
||||||
|
orListParam.add(new TokenParam("urn:taglist", methodName + "2a"));
|
||||||
|
params.add("_tag", orListParam);
|
||||||
|
params.setLastUpdated(new DateRangeParam(betweenDate, null));
|
||||||
|
List<IIdType> patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params));
|
||||||
|
assertThat(patients, containsInAnyOrder(tag2id));
|
||||||
|
}
|
||||||
// TODO: get multiple/AND working
|
// TODO: get multiple/AND working
|
||||||
{
|
{
|
||||||
// And tags
|
// And tags
|
||||||
|
|
|
@ -162,6 +162,9 @@
|
||||||
meaning that the same operation can also be invoked
|
meaning that the same operation can also be invoked
|
||||||
at the type level.
|
at the type level.
|
||||||
</action>
|
</action>
|
||||||
|
<action type="add">
|
||||||
|
Make JPA search queries with _lastUpdated parameter a bit more efficient
|
||||||
|
</action>
|
||||||
</release>
|
</release>
|
||||||
<release version="1.2" date="2015-09-18">
|
<release version="1.2" date="2015-09-18">
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
|
Loading…
Reference in New Issue