mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
582 lines
15 KiB
Java
582 lines
15 KiB
Java
package com.avaje.ebeaninternal.server.querydefn;
|
|
|
|
import java.io.Serializable;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import com.avaje.ebean.ExpressionFactory;
|
|
import com.avaje.ebean.FetchConfig;
|
|
import com.avaje.ebean.OrderBy;
|
|
import com.avaje.ebean.Query;
|
|
import com.avaje.ebean.event.BeanQueryRequest;
|
|
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
|
|
import com.avaje.ebeaninternal.api.SpiExpression;
|
|
import com.avaje.ebeaninternal.api.SpiExpressionFactory;
|
|
import com.avaje.ebeaninternal.api.SpiExpressionList;
|
|
import com.avaje.ebeaninternal.api.SpiQuery;
|
|
import com.avaje.ebeaninternal.server.expression.FilterExprPath;
|
|
import com.avaje.ebeaninternal.server.lib.util.StringHelper;
|
|
import com.avaje.ebeaninternal.server.query.SplitName;
|
|
import com.avaje.ebeaninternal.util.FilterExpressionList;
|
|
|
|
/**
|
|
* Represents the Properties of an Object Relational query.
|
|
*/
|
|
public class OrmQueryProperties implements Serializable {
|
|
|
|
private static final long serialVersionUID = -8785582703966455658L;
|
|
|
|
private String parentPath;
|
|
private String path;
|
|
|
|
private String properties;
|
|
|
|
private String trimmedProperties;
|
|
|
|
/**
|
|
* NB: -1 means no +query, 0 means use the default batch size.
|
|
*/
|
|
private int queryFetchBatch = -1;
|
|
private boolean queryFetchAll;
|
|
|
|
/**
|
|
* NB: -1 means no +lazy, 0 means use the default batch size.
|
|
*/
|
|
private int lazyFetchBatch = -1;
|
|
|
|
private FetchConfig fetchConfig;
|
|
|
|
private boolean cache;
|
|
|
|
private boolean readOnly;
|
|
|
|
/**
|
|
* Note this SHOULD be a LinkedHashSet to preserve order of the properties. This is to make using
|
|
* SqlSelect easier with predictable property/column ordering.
|
|
*/
|
|
private Set<String> included;
|
|
|
|
/**
|
|
* Included bean joins.
|
|
*/
|
|
private Set<String> includedBeanJoin;
|
|
|
|
/**
|
|
* Add these properties to the select so that the foreign key columns are included in the query.
|
|
*/
|
|
private Set<String> secondaryQueryJoins;
|
|
|
|
private List<OrmQueryProperties> secondaryChildren;
|
|
|
|
/**
|
|
* OrderBy properties that where on the main query but moved here as they relate to this (query
|
|
* join).
|
|
*/
|
|
@SuppressWarnings("rawtypes")
|
|
private OrderBy orderBy;
|
|
|
|
/**
|
|
* A filter that can be applied to the fetch of this path in the object graph.
|
|
*/
|
|
@SuppressWarnings("rawtypes")
|
|
private SpiExpressionList filterMany;
|
|
|
|
/**
|
|
* Construct with a given path (null == root path).
|
|
*/
|
|
public OrmQueryProperties(String path) {
|
|
this.path = path;
|
|
this.parentPath = SplitName.parent(path);
|
|
}
|
|
|
|
public OrmQueryProperties() {
|
|
this(null);
|
|
}
|
|
|
|
/**
|
|
* Used by query language parser.
|
|
*/
|
|
public OrmQueryProperties(String path, String properties) {
|
|
this(path);
|
|
setProperties(properties);
|
|
}
|
|
|
|
/**
|
|
* Move a OrderBy.Property from the main query to this query join.
|
|
*/
|
|
@SuppressWarnings("rawtypes")
|
|
public void addSecJoinOrderProperty(OrderBy.Property orderProp) {
|
|
if (orderBy == null) {
|
|
orderBy = new OrderBy();
|
|
}
|
|
orderBy.add(orderProp);
|
|
}
|
|
|
|
/**
|
|
* Set the Fetch configuration options for this path.
|
|
*/
|
|
public void setFetchConfig(FetchConfig fetchConfig) {
|
|
if (fetchConfig != null) {
|
|
this.fetchConfig = fetchConfig;
|
|
lazyFetchBatch = fetchConfig.getLazyBatchSize();
|
|
queryFetchBatch = fetchConfig.getQueryBatchSize();
|
|
queryFetchAll = fetchConfig.isQueryAll();
|
|
}
|
|
}
|
|
|
|
public FetchConfig getFetchConfig() {
|
|
return fetchConfig;
|
|
}
|
|
|
|
/**
|
|
* Set the comma delimited properties to fetch for this path.
|
|
* <p>
|
|
* This can include the +query and +lazy type hints.
|
|
* </p>
|
|
*/
|
|
public void setProperties(String properties) {
|
|
this.properties = properties;
|
|
this.trimmedProperties = properties;
|
|
parseProperties();
|
|
|
|
if (!isAllProperties()) {
|
|
Set<String> parsed = parseIncluded(trimmedProperties);
|
|
if (parsed.contains("*")) {
|
|
this.included = null;
|
|
} else {
|
|
this.included = parsed;
|
|
}
|
|
} else {
|
|
this.included = null;
|
|
}
|
|
}
|
|
|
|
private boolean isAllProperties() {
|
|
return (trimmedProperties == null) || (trimmedProperties.length() == 0) || "*".equals(trimmedProperties);
|
|
}
|
|
|
|
/**
|
|
* Return the expressions used to filter on this path. This should be a many path to use this
|
|
* method.
|
|
*/
|
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
public <T> SpiExpressionList<T> filterMany(Query<T> rootQuery) {
|
|
if (filterMany == null) {
|
|
FilterExprPath exprPath = new FilterExprPath(path);
|
|
SpiExpressionFactory queryEf = (SpiExpressionFactory) rootQuery.getExpressionFactory();
|
|
ExpressionFactory filterEf = queryEf.createExpressionFactory();// exprPath);
|
|
filterMany = new FilterExpressionList(exprPath, filterEf, rootQuery);
|
|
// by default we need to make this a 'query join' now
|
|
queryFetchAll = true;
|
|
queryFetchBatch = 100;
|
|
lazyFetchBatch = 100;
|
|
}
|
|
return filterMany;
|
|
}
|
|
|
|
/**
|
|
* Return the filterMany expression list (can be null).
|
|
*/
|
|
public SpiExpressionList<?> getFilterManyTrimPath(int trimPath) {
|
|
if (filterMany == null) {
|
|
return null;
|
|
}
|
|
return filterMany.trimPath(trimPath);
|
|
}
|
|
|
|
/**
|
|
* Return the filterMany expression list (can be null).
|
|
*/
|
|
public SpiExpressionList<?> getFilterMany() {
|
|
return filterMany;
|
|
}
|
|
|
|
/**
|
|
* Set the filterMany expression list.
|
|
*/
|
|
public void setFilterMany(SpiExpressionList<?> filterMany) {
|
|
this.filterMany = filterMany;
|
|
}
|
|
|
|
/**
|
|
* Set the properties from deployment default FetchTypes.
|
|
*/
|
|
public void setDefaultProperties(String properties, Set<String> included) {
|
|
this.properties = properties;
|
|
this.trimmedProperties = properties;
|
|
this.included = included;
|
|
}
|
|
|
|
/**
|
|
* Set the properties from a matching autofetch tuned properties.
|
|
*/
|
|
public void setTunedProperties(OrmQueryProperties tunedProperties) {
|
|
this.properties = tunedProperties.properties;
|
|
this.trimmedProperties = tunedProperties.trimmedProperties;
|
|
this.included = tunedProperties.included;
|
|
}
|
|
|
|
/**
|
|
* Define the select and joins for this query.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public void configureBeanQuery(SpiQuery<?> query) {
|
|
|
|
if (trimmedProperties != null && trimmedProperties.length() > 0) {
|
|
query.select(trimmedProperties);
|
|
}
|
|
|
|
if (filterMany != null) {
|
|
SpiExpressionList<?> trimPath = filterMany.trimPath(path.length() + 1);
|
|
List<SpiExpression> underlyingList = trimPath.getUnderlyingList();
|
|
for (SpiExpression spiExpression : underlyingList) {
|
|
query.where().add(spiExpression);
|
|
}
|
|
}
|
|
|
|
if (secondaryChildren != null) {
|
|
int trimPath = path.length() + 1;
|
|
for (int i = 0; i < secondaryChildren.size(); i++) {
|
|
OrmQueryProperties p = secondaryChildren.get(i);
|
|
String path = p.getPath();
|
|
path = path.substring(trimPath);
|
|
query.fetch(path, p.getProperties(), p.getFetchConfig());
|
|
query.setFilterMany(path, p.getFilterManyTrimPath(trimPath));
|
|
}
|
|
}
|
|
|
|
if (orderBy != null) {
|
|
query.setOrder(orderBy.copyWithTrim(path));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a copy of the OrmQueryProperties.
|
|
*/
|
|
public OrmQueryProperties copy() {
|
|
OrmQueryProperties copy = new OrmQueryProperties();
|
|
copy.parentPath = parentPath;
|
|
copy.path = path;
|
|
copy.properties = properties;
|
|
copy.cache = cache;
|
|
copy.readOnly = readOnly;
|
|
copy.queryFetchAll = queryFetchAll;
|
|
copy.queryFetchBatch = queryFetchBatch;
|
|
copy.lazyFetchBatch = lazyFetchBatch;
|
|
copy.filterMany = filterMany;
|
|
if (included != null) {
|
|
copy.included = new HashSet<String>(included);
|
|
}
|
|
if (includedBeanJoin != null) {
|
|
copy.includedBeanJoin = new HashSet<String>(includedBeanJoin);
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
public boolean hasSelectClause() {
|
|
if ("*".equals(trimmedProperties)) {
|
|
// explicitly selected all properties
|
|
return true;
|
|
}
|
|
// explicitly selected some properties
|
|
return included != null;
|
|
}
|
|
|
|
public String toString() {
|
|
String s = "";
|
|
if (path != null) {
|
|
s += path + " ";
|
|
}
|
|
if (properties != null) {
|
|
s += "(" + properties + ") ";
|
|
} else if (included != null) {
|
|
s += "(" + included + ") ";
|
|
}
|
|
return s;
|
|
}
|
|
|
|
public boolean isChild(OrmQueryProperties possibleChild) {
|
|
return possibleChild.getPath().startsWith(path + ".");
|
|
}
|
|
|
|
/**
|
|
* For secondary queries add a child element.
|
|
*/
|
|
public void add(OrmQueryProperties child) {
|
|
if (secondaryChildren == null) {
|
|
secondaryChildren = new ArrayList<OrmQueryProperties>();
|
|
}
|
|
secondaryChildren.add(child);
|
|
}
|
|
|
|
public int autofetchPlanHash() {
|
|
|
|
int hc = (path != null ? path.hashCode() : 1);
|
|
if (properties != null) {
|
|
hc = hc * 31 + properties.hashCode();
|
|
} else {
|
|
hc = hc * 31 + (included != null ? included.hashCode() : 1);
|
|
}
|
|
|
|
return hc;
|
|
}
|
|
|
|
/**
|
|
* Calculate the query plan hash.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public void queryPlanHash(BeanQueryRequest<?> request, HashQueryPlanBuilder builder) {
|
|
|
|
builder.add(path);
|
|
if (properties != null) {
|
|
builder.add(properties);
|
|
} else {
|
|
builder.add(included);
|
|
}
|
|
builder.add(filterMany != null);
|
|
if (filterMany != null) {
|
|
filterMany.queryPlanHash(request, builder);
|
|
}
|
|
}
|
|
|
|
public String getProperties() {
|
|
return properties;
|
|
}
|
|
|
|
/**
|
|
* Return true if this has properties.
|
|
*/
|
|
public boolean hasProperties() {
|
|
return properties != null || included != null;
|
|
}
|
|
|
|
/**
|
|
* Return true if this property is included as a bean join.
|
|
* <p>
|
|
* If a property is included as a bean join then it should not be included as a reference/proxy to
|
|
* avoid duplication.
|
|
* </p>
|
|
*/
|
|
public boolean isIncludedBeanJoin(String propertyName) {
|
|
if (includedBeanJoin == null) {
|
|
return false;
|
|
} else {
|
|
return includedBeanJoin.contains(propertyName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a bean join property.
|
|
*/
|
|
public void includeBeanJoin(String propertyName) {
|
|
if (includedBeanJoin == null) {
|
|
includedBeanJoin = new HashSet<String>();
|
|
}
|
|
includedBeanJoin.add(propertyName);
|
|
}
|
|
|
|
public boolean allProperties() {
|
|
return included == null;
|
|
}
|
|
|
|
/**
|
|
* This excludes the bean joined properties.
|
|
* <p>
|
|
* This is because bean joins will have there own node in the SqlTree.
|
|
* </p>
|
|
*/
|
|
public Set<String> getSelectProperties() {
|
|
|
|
if (secondaryQueryJoins == null) {
|
|
return included;
|
|
}
|
|
|
|
LinkedHashSet<String> temp = new LinkedHashSet<String>(secondaryQueryJoins.size() + included.size());
|
|
temp.addAll(included);
|
|
temp.addAll(secondaryQueryJoins);
|
|
return temp;
|
|
}
|
|
|
|
public void addSecondaryQueryJoin(String property) {
|
|
if (secondaryQueryJoins == null) {
|
|
secondaryQueryJoins = new HashSet<String>(4);
|
|
}
|
|
secondaryQueryJoins.add(property);
|
|
}
|
|
|
|
/**
|
|
* Return all the properties including the bean joins. This is the set that will be used by
|
|
* EntityBeanIntercept to determine if a property needs to be lazy loaded.
|
|
*/
|
|
public Set<String> getAllIncludedProperties() {
|
|
|
|
if (included == null) {
|
|
return null;
|
|
}
|
|
|
|
if (includedBeanJoin == null && secondaryQueryJoins == null) {
|
|
return new LinkedHashSet<String>(included);
|
|
}
|
|
|
|
LinkedHashSet<String> s = new LinkedHashSet<String>(2 * (included.size() + 5));
|
|
if (included != null) {
|
|
s.addAll(included);
|
|
}
|
|
if (includedBeanJoin != null) {
|
|
s.addAll(includedBeanJoin);
|
|
}
|
|
if (secondaryQueryJoins != null) {
|
|
s.addAll(secondaryQueryJoins);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
public boolean isIncluded(String propName) {
|
|
|
|
if (includedBeanJoin != null && includedBeanJoin.contains(propName)) {
|
|
return false;
|
|
}
|
|
if (included == null) {
|
|
// all properties included
|
|
return true;
|
|
}
|
|
return included.contains(propName);
|
|
}
|
|
|
|
public OrmQueryProperties setQueryFetch(int batch, boolean queryFetchAll) {
|
|
this.queryFetchBatch = batch;
|
|
this.queryFetchAll = queryFetchAll;
|
|
return this;
|
|
}
|
|
|
|
public boolean isFetchJoin() {
|
|
return !isQueryFetch() && !isLazyFetch();
|
|
}
|
|
|
|
public boolean isQueryFetch() {
|
|
return queryFetchBatch > -1;
|
|
}
|
|
|
|
public int getQueryFetchBatch() {
|
|
return queryFetchBatch;
|
|
}
|
|
|
|
public boolean isQueryFetchAll() {
|
|
return queryFetchAll;
|
|
}
|
|
|
|
public boolean isLazyFetch() {
|
|
return lazyFetchBatch > -1;
|
|
}
|
|
|
|
public int getLazyFetchBatch() {
|
|
return lazyFetchBatch;
|
|
}
|
|
|
|
public boolean isReadOnly() {
|
|
return readOnly;
|
|
}
|
|
|
|
public boolean isCache() {
|
|
return cache;
|
|
}
|
|
|
|
public String getParentPath() {
|
|
return parentPath;
|
|
}
|
|
|
|
public String getPath() {
|
|
return path;
|
|
}
|
|
|
|
private void parseProperties() {
|
|
if (trimmedProperties == null) {
|
|
return;
|
|
}
|
|
int pos = trimmedProperties.indexOf("+readonly");
|
|
if (pos > -1) {
|
|
trimmedProperties = StringHelper.replaceString(trimmedProperties, "+readonly", "");
|
|
this.readOnly = true;
|
|
}
|
|
pos = trimmedProperties.indexOf("+cache");
|
|
if (pos > -1) {
|
|
trimmedProperties = StringHelper.replaceString(trimmedProperties, "+cache", "");
|
|
this.cache = true;
|
|
}
|
|
pos = trimmedProperties.indexOf("+query");
|
|
if (pos > -1) {
|
|
queryFetchBatch = parseBatchHint(pos, "+query");
|
|
}
|
|
pos = trimmedProperties.indexOf("+lazy");
|
|
if (pos > -1) {
|
|
lazyFetchBatch = parseBatchHint(pos, "+lazy");
|
|
}
|
|
|
|
trimmedProperties = trimmedProperties.trim();
|
|
while (trimmedProperties.startsWith(",")) {
|
|
trimmedProperties = trimmedProperties.substring(1).trim();
|
|
}
|
|
}
|
|
|
|
private int parseBatchHint(int pos, String option) {
|
|
|
|
int startPos = pos + option.length();
|
|
|
|
int endPos = findEndPos(startPos, trimmedProperties);
|
|
if (endPos == -1) {
|
|
trimmedProperties = StringHelper.replaceString(trimmedProperties, option, "");
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
String batchParam = trimmedProperties.substring(startPos + 1, endPos);
|
|
|
|
if (endPos + 1 >= trimmedProperties.length()) {
|
|
trimmedProperties = trimmedProperties.substring(0, pos);
|
|
} else {
|
|
trimmedProperties = trimmedProperties.substring(0, pos) + trimmedProperties.substring(endPos + 1);
|
|
}
|
|
return Integer.parseInt(batchParam);
|
|
}
|
|
}
|
|
|
|
private int findEndPos(int pos, String props) {
|
|
|
|
if (pos < props.length()) {
|
|
if (props.charAt(pos) == '(') {
|
|
int endPara = props.indexOf(')', pos + 1);
|
|
if (endPara == -1) {
|
|
String m = "Error could not find ')' in " + props + " after position " + pos;
|
|
throw new RuntimeException(m);
|
|
}
|
|
return endPara;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Parse the include separating by comma or semicolon.
|
|
*/
|
|
private static Set<String> parseIncluded(String rawList) {
|
|
|
|
String[] res = rawList.split(",");
|
|
|
|
LinkedHashSet<String> set = new LinkedHashSet<String>(res.length + 3);
|
|
|
|
String temp;
|
|
for (int i = 0; i < res.length; i++) {
|
|
temp = res[i].trim();
|
|
if (temp.length() > 0) {
|
|
set.add(temp);
|
|
}
|
|
}
|
|
|
|
return Collections.unmodifiableSet(set);
|
|
}
|
|
}
|