mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-11-01 08:18:43 +00:00
#919 - io.ebean package initial
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.EbeanServer;
|
||||
import io.ebeaninternal.api.BindParams;
|
||||
import io.ebeaninternal.api.SpiUpdate;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Default implementation of OrmUpdate.
|
||||
*/
|
||||
public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = -8791423602246515438L;
|
||||
|
||||
|
||||
private transient final EbeanServer server;
|
||||
|
||||
private final Class<?> beanType;
|
||||
|
||||
/**
|
||||
* The name of the update.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The parameters used to bind to the sql.
|
||||
*/
|
||||
private final BindParams bindParams = new BindParams();
|
||||
|
||||
/**
|
||||
* The sql update or delete statement.
|
||||
*/
|
||||
private final String updateStatement;
|
||||
|
||||
/**
|
||||
* Automatically detect the table being modified by this sql. This will
|
||||
* register this information so that eBean invalidates cached objects if
|
||||
* required.
|
||||
*/
|
||||
private boolean notifyCache = true;
|
||||
|
||||
private int timeout;
|
||||
|
||||
private String generatedSql;
|
||||
|
||||
private final String baseTable;
|
||||
|
||||
private final OrmUpdateType type;
|
||||
|
||||
/**
|
||||
* Create with a specific server. This means you can use the
|
||||
* UpdateSql.execute() method.
|
||||
*/
|
||||
public DefaultOrmUpdate(Class<?> beanType, EbeanServer server, String baseTable, String updateStatement) {
|
||||
this.beanType = beanType;
|
||||
this.server = server;
|
||||
this.baseTable = baseTable;
|
||||
this.name = "";
|
||||
this.updateStatement = updateStatement;
|
||||
this.type = deriveType(updateStatement);
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setTimeout(int secs) {
|
||||
this.timeout = secs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Class<?> getBeanType() {
|
||||
return beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the timeout in seconds.
|
||||
*/
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
private SpiUpdate.OrmUpdateType deriveType(String updateStatement) {
|
||||
|
||||
updateStatement = updateStatement.trim();
|
||||
int spacepos = updateStatement.indexOf(' ');
|
||||
if (spacepos == -1) {
|
||||
return SpiUpdate.OrmUpdateType.UNKNOWN;
|
||||
|
||||
} else {
|
||||
String firstWord = updateStatement.substring(0, spacepos);
|
||||
if (firstWord.equalsIgnoreCase("update")) {
|
||||
return SpiUpdate.OrmUpdateType.UPDATE;
|
||||
|
||||
} else if (firstWord.equalsIgnoreCase("insert")) {
|
||||
return SpiUpdate.OrmUpdateType.INSERT;
|
||||
|
||||
} else if (firstWord.equalsIgnoreCase("delete")) {
|
||||
return SpiUpdate.OrmUpdateType.DELETE;
|
||||
} else {
|
||||
return SpiUpdate.OrmUpdateType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int execute() {
|
||||
return server.execute(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to false if you don't want eBean to automatically deduce the
|
||||
* table modification information and process it.
|
||||
* <p>
|
||||
* Set this to false if you don't want any cache invalidation or text index
|
||||
* management to occur. You may do this when say you update only one column
|
||||
* and you know that it is not important for cached objects or text indexes.
|
||||
* </p>
|
||||
*/
|
||||
public DefaultOrmUpdate<T> setNotifyCache(boolean notifyCache) {
|
||||
this.notifyCache = notifyCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the cache should be notified so that invalidates
|
||||
* appropriate objects.
|
||||
*/
|
||||
public boolean isNotifyCache() {
|
||||
return notifyCache;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getUpdateStatement() {
|
||||
return updateStatement;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> set(int position, Object value) {
|
||||
bindParams.setParameter(position, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setParameter(int position, Object value) {
|
||||
bindParams.setParameter(position, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setNull(int position, int jdbcType) {
|
||||
bindParams.setNullParameter(position, jdbcType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setNullParameter(int position, int jdbcType) {
|
||||
bindParams.setNullParameter(position, jdbcType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> set(String name, Object value) {
|
||||
bindParams.setParameter(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setParameter(String name, Object param) {
|
||||
bindParams.setParameter(name, param);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setNull(String name, int jdbcType) {
|
||||
bindParams.setNullParameter(name, jdbcType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOrmUpdate<T> setNullParameter(String name, int jdbcType) {
|
||||
bindParams.setNullParameter(name, jdbcType);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bind parameters.
|
||||
*/
|
||||
public BindParams getBindParams() {
|
||||
return bindParams;
|
||||
}
|
||||
|
||||
public String getGeneratedSql() {
|
||||
return generatedSql;
|
||||
}
|
||||
|
||||
public void setGeneratedSql(String generatedSql) {
|
||||
this.generatedSql = generatedSql;
|
||||
}
|
||||
|
||||
public String getBaseTable() {
|
||||
return baseTable;
|
||||
}
|
||||
|
||||
public OrmUpdateType getOrmUpdateType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.EbeanServer;
|
||||
import io.ebean.SqlRow;
|
||||
import io.ebeaninternal.api.BindParams;
|
||||
import io.ebeaninternal.api.SpiSqlQuery;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Default implementation of SQuery - SQL Query.
|
||||
*/
|
||||
public class DefaultRelationalQuery implements SpiSqlQuery {
|
||||
|
||||
private static final long serialVersionUID = -1098305779779591068L;
|
||||
|
||||
private final transient EbeanServer server;
|
||||
|
||||
private String query;
|
||||
|
||||
private int firstRow;
|
||||
|
||||
private int maxRows;
|
||||
|
||||
private int timeout;
|
||||
|
||||
private int bufferFetchSizeHint;
|
||||
|
||||
/**
|
||||
* Bind parameters when using the query language.
|
||||
*/
|
||||
private final BindParams bindParams = new BindParams();
|
||||
|
||||
/**
|
||||
* Additional supply a query detail object.
|
||||
*/
|
||||
public DefaultRelationalQuery(EbeanServer server, String query) {
|
||||
this.server = server;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setQuery(String query) {
|
||||
this.query = query;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findEach(Consumer<SqlRow> consumer) {
|
||||
server.findEach(this, consumer, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findEachWhile(Predicate<SqlRow> consumer) {
|
||||
server.findEachWhile(this, consumer, null);
|
||||
}
|
||||
|
||||
public List<SqlRow> findList() {
|
||||
return server.findList(this, null);
|
||||
}
|
||||
|
||||
public SqlRow findUnique() {
|
||||
return server.findUnique(this, null);
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setParameter(int position, Object value) {
|
||||
bindParams.setParameter(position, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setParameter(String paramName, Object value) {
|
||||
bindParams.setParameter(paramName, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "SqlQuery [" + query + "]";
|
||||
}
|
||||
|
||||
public int getFirstRow() {
|
||||
return firstRow;
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setFirstRow(int firstRow) {
|
||||
this.firstRow = firstRow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMaxRows() {
|
||||
return maxRows;
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setMaxRows(int maxRows) {
|
||||
this.maxRows = maxRows;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setTimeout(int secs) {
|
||||
this.timeout = secs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BindParams getBindParams() {
|
||||
return bindParams;
|
||||
}
|
||||
|
||||
public DefaultRelationalQuery setBufferFetchSizeHint(int bufferFetchSizeHint) {
|
||||
this.bufferFetchSizeHint = bufferFetchSizeHint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getBufferFetchSizeHint() {
|
||||
return bufferFetchSizeHint;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.ExpressionList;
|
||||
import io.ebean.UpdateQuery;
|
||||
|
||||
/**
|
||||
* Default implementation of UpdateQuery.
|
||||
*/
|
||||
public class DefaultUpdateQuery<T> implements UpdateQuery<T> {
|
||||
|
||||
private final OrmUpdateProperties values = new OrmUpdateProperties();
|
||||
|
||||
private final DefaultOrmQuery<T> query;
|
||||
|
||||
public DefaultUpdateQuery(DefaultOrmQuery<T> query) {
|
||||
this.query = query;
|
||||
query.setUpdateProperties(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateQuery<T> set(String property, Object value) {
|
||||
values.set(property, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateQuery<T> setNull(String property) {
|
||||
values.set(property, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateQuery<T> setRaw(String propertyExpression) {
|
||||
values.setRaw(propertyExpression);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateQuery<T> setRaw(String propertyExpression, Object... vals) {
|
||||
values.setRaw(propertyExpression, vals);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> where() {
|
||||
return query.where();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
public class NaturalKeyBindParam {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Object value;
|
||||
|
||||
public NaturalKeyBindParam(String name, Object value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebeaninternal.api.SpiNamedParam;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
|
||||
/**
|
||||
* Named parameter used as placeholder in expressions created by EQL language parsing.
|
||||
*/
|
||||
class ONamedParam implements SpiNamedParam {
|
||||
|
||||
private final String name;
|
||||
|
||||
private Object value;
|
||||
|
||||
/**
|
||||
* Create with the given name.
|
||||
*/
|
||||
ONamedParam(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bind value for this named parameter.
|
||||
*/
|
||||
public void setValue(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bind value for this named parameter.
|
||||
*/
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the bind value has been set (so does not support null value).
|
||||
*/
|
||||
void checkValueSet() {
|
||||
if (value == null) {
|
||||
throw new PersistenceException("Named parameter [" + name + "] has not had it's value set.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.FetchConfig;
|
||||
import io.ebeaninternal.api.HashQueryPlanBuilder;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
|
||||
import io.ebeaninternal.server.el.ElPropertyDeploy;
|
||||
import io.ebeaninternal.server.el.ElPropertyValue;
|
||||
import io.ebeaninternal.server.query.SplitName;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents the internal structure of an Object Relational query.
|
||||
* <p>
|
||||
* Holds the select() and join() details of a ORM query.
|
||||
* </p>
|
||||
* <p>
|
||||
* It is worth noting that for AutoTune a "tuned fetch info" builds an instance of OrmQueryDetail.
|
||||
* Tuning a query is a matter of replacing an instance of this class with one that has been tuned
|
||||
* with select() and join() set.
|
||||
* </p>
|
||||
*/
|
||||
public class OrmQueryDetail implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -2510486880141461807L;
|
||||
|
||||
/**
|
||||
* Root level properties.
|
||||
*/
|
||||
private OrmQueryProperties baseProps = new OrmQueryProperties();
|
||||
|
||||
/**
|
||||
* Contains the fetch/lazy/query joins and their properties.
|
||||
*/
|
||||
private LinkedHashMap<String, OrmQueryProperties> fetchPaths = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Return a deep copy of the OrmQueryDetail.
|
||||
*/
|
||||
public OrmQueryDetail copy() {
|
||||
OrmQueryDetail copy = new OrmQueryDetail();
|
||||
copy.baseProps = baseProps.copy();
|
||||
for (Map.Entry<String, OrmQueryProperties> entry : fetchPaths.entrySet()) {
|
||||
copy.fetchPaths.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
public int queryPlanHash() {
|
||||
HashQueryPlanBuilder builder = new HashQueryPlanBuilder();
|
||||
queryPlanHash(builder);
|
||||
return builder.getPlanHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the hash for the query plan.
|
||||
*/
|
||||
public void queryPlanHash(HashQueryPlanBuilder builder) {
|
||||
baseProps.queryPlanHash(builder);
|
||||
if (fetchPaths != null) {
|
||||
for (OrmQueryProperties p : fetchPaths.values()) {
|
||||
p.queryPlanHash(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the details are the same for query plan purposes.
|
||||
*/
|
||||
public boolean isSameByPlan(OrmQueryDetail otherDetail) {
|
||||
if (!isSameByPlan(baseProps, otherDetail.baseProps)) {
|
||||
return false;
|
||||
}
|
||||
if (fetchPaths == null) {
|
||||
return otherDetail.fetchPaths == null;
|
||||
}
|
||||
if (fetchPaths.size() != otherDetail.fetchPaths.size()) {
|
||||
return false;
|
||||
}
|
||||
// check with ordering being important
|
||||
Iterator<Map.Entry<String, OrmQueryProperties>> thisIt = fetchPaths.entrySet().iterator();
|
||||
Iterator<Map.Entry<String, OrmQueryProperties>> thatIt = otherDetail.fetchPaths.entrySet().iterator();
|
||||
while (thisIt.hasNext() && thatIt.hasNext()) {
|
||||
Map.Entry<String, OrmQueryProperties> thisEntry = thisIt.next();
|
||||
Map.Entry<String, OrmQueryProperties> thatEntry = thatIt.next();
|
||||
if (!thisEntry.getKey().equals(thatEntry.getKey())) {
|
||||
return false;
|
||||
}
|
||||
if (!thisEntry.getValue().isSameByPlan(thatEntry.getValue())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if equal in terms of autoTune (select and fetch without property ordering).
|
||||
*/
|
||||
public boolean isAutoTuneEqual(OrmQueryDetail otherDetail) {
|
||||
|
||||
if (!isSameByAutoTune(baseProps, otherDetail.baseProps)) {
|
||||
return false;
|
||||
}
|
||||
if (fetchPaths == null) {
|
||||
return otherDetail.fetchPaths == null;
|
||||
}
|
||||
if (fetchPaths.size() != otherDetail.fetchPaths.size()) {
|
||||
return false;
|
||||
}
|
||||
// check without regard to ordering
|
||||
for (Map.Entry<String, OrmQueryProperties> entry : fetchPaths.entrySet()) {
|
||||
OrmQueryProperties chunk = otherDetail.getChunk(entry.getKey(), false);
|
||||
if (!isSameByAutoTune(entry.getValue(), chunk)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isSameByAutoTune(OrmQueryProperties p1, OrmQueryProperties p2) {
|
||||
return p1 == null ? p2 == null : p1.isSameByAutoTune(p2);
|
||||
}
|
||||
|
||||
private boolean isSameByPlan(OrmQueryProperties p1, OrmQueryProperties p2) {
|
||||
return p1 == null ? p2 == null : p1.isSameByPlan(p2);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the detail in string form.
|
||||
*/
|
||||
public String asString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!baseProps.isEmpty()) {
|
||||
baseProps.append("select ", sb);
|
||||
}
|
||||
if (fetchPaths != null) {
|
||||
for (OrmQueryProperties join : fetchPaths.values()) {
|
||||
join.append(" fetch ", sb);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
throw new RuntimeException("should not use");
|
||||
}
|
||||
|
||||
/**
|
||||
* set the properties to include on the base / root entity.
|
||||
*/
|
||||
public void select(String columns) {
|
||||
baseProps = new OrmQueryProperties(null, columns, null);
|
||||
}
|
||||
|
||||
boolean containsProperty(String property) {
|
||||
return baseProps.isIncluded(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base / root query properties.
|
||||
*/
|
||||
public void setBase(OrmQueryProperties baseProps) {
|
||||
this.baseProps = baseProps;
|
||||
}
|
||||
|
||||
List<OrmQueryProperties> removeSecondaryQueries() {
|
||||
return removeSecondaryQueries(false);
|
||||
}
|
||||
|
||||
List<OrmQueryProperties> removeSecondaryLazyQueries() {
|
||||
return removeSecondaryQueries(true);
|
||||
}
|
||||
|
||||
private List<OrmQueryProperties> removeSecondaryQueries(boolean lazyQuery) {
|
||||
|
||||
ArrayList<String> matchingPaths = new ArrayList<>(2);
|
||||
|
||||
for (OrmQueryProperties chunk : fetchPaths.values()) {
|
||||
boolean match = lazyQuery ? chunk.isLazyFetch() : chunk.isQueryFetch();
|
||||
if (match) {
|
||||
matchingPaths.add(chunk.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingPaths.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// sort into depth order to remove
|
||||
Collections.sort(matchingPaths);
|
||||
|
||||
// the list of secondary queries
|
||||
ArrayList<OrmQueryProperties> props = new ArrayList<>();
|
||||
|
||||
for (String path : matchingPaths) {
|
||||
OrmQueryProperties secQuery = fetchPaths.remove(path);
|
||||
if (secQuery != null) {
|
||||
props.add(secQuery);
|
||||
|
||||
// remove any child properties for this path
|
||||
Iterator<OrmQueryProperties> pass2It = fetchPaths.values().iterator();
|
||||
while (pass2It.hasNext()) {
|
||||
OrmQueryProperties pass2Prop = pass2It.next();
|
||||
if (secQuery.isChild(pass2Prop)) {
|
||||
// remove join to secondary query from the main query
|
||||
// and add to this secondary query
|
||||
pass2It.remove();
|
||||
secQuery.add(pass2Prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the secondary queries as select properties
|
||||
// to the parent chunk to ensure the foreign keys
|
||||
// are included in the query
|
||||
for (OrmQueryProperties prop : props) {
|
||||
String path = prop.getPath();
|
||||
// split into parent and property
|
||||
String[] split = SplitName.split(path);
|
||||
// add property to parent chunk
|
||||
OrmQueryProperties chunk = getChunk(split[0], true);
|
||||
chunk.addSecondaryQueryJoin(split[1]);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
boolean tuneFetchProperties(OrmQueryDetail tunedDetail) {
|
||||
|
||||
boolean tuned = false;
|
||||
|
||||
OrmQueryProperties tunedRoot = tunedDetail.getChunk(null, false);
|
||||
if (tunedRoot != null) {
|
||||
tuned = true;
|
||||
baseProps = tunedRoot;
|
||||
for (OrmQueryProperties tunedChunk : tunedDetail.fetchPaths.values()) {
|
||||
fetch(tunedChunk.copy());
|
||||
}
|
||||
}
|
||||
return tuned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or replace the fetch detail.
|
||||
*/
|
||||
protected void fetch(OrmQueryProperties chunk) {
|
||||
String path = chunk.getPath();
|
||||
if (path == null) {
|
||||
baseProps = chunk;
|
||||
} else {
|
||||
fetchPaths.put(path, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all joins and properties.
|
||||
* <p>
|
||||
* Typically for the row count query.
|
||||
* </p>
|
||||
*/
|
||||
public void clear() {
|
||||
fetchPaths.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fetch properties and configuration for a given path.
|
||||
*
|
||||
* @param path the property to join
|
||||
* @param partialProps the properties on the join property to include
|
||||
*/
|
||||
public void fetch(String path, String partialProps, FetchConfig fetchConfig) {
|
||||
|
||||
fetch(new OrmQueryProperties(path, partialProps, fetchConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add for raw sql etc when the properties are already parsed into a set.
|
||||
*/
|
||||
public void fetch(String path, LinkedHashSet<String> properties) {
|
||||
fetch(new OrmQueryProperties(path, properties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the fetch paths into depth order adding any missing parent paths if necessary.
|
||||
*/
|
||||
public void sortFetchPaths(BeanDescriptor<?> d) {
|
||||
|
||||
if (!fetchPaths.isEmpty()) {
|
||||
LinkedHashMap<String, OrmQueryProperties> sorted = new LinkedHashMap<>();
|
||||
for (OrmQueryProperties p : fetchPaths.values()) {
|
||||
sortFetchPaths(d, p, sorted);
|
||||
}
|
||||
fetchPaths = sorted;
|
||||
}
|
||||
}
|
||||
|
||||
private void sortFetchPaths(BeanDescriptor<?> d, OrmQueryProperties p, LinkedHashMap<String, OrmQueryProperties> sorted) {
|
||||
|
||||
String path = p.getPath();
|
||||
if (!sorted.containsKey(path)) {
|
||||
String parentPath = p.getParentPath();
|
||||
if (parentPath == null || sorted.containsKey(parentPath)) {
|
||||
// off root path or parent already ahead in fetch order
|
||||
sorted.put(path, p);
|
||||
} else {
|
||||
OrmQueryProperties parentProp = fetchPaths.get(parentPath);
|
||||
if (parentProp == null) {
|
||||
ElPropertyValue el = d.getElGetValue(parentPath);
|
||||
if (el == null) {
|
||||
throw new PersistenceException("Path [" + parentPath + "] not valid from " + d.getFullName());
|
||||
}
|
||||
// add a missing parent path just fetching the Id property
|
||||
BeanPropertyAssoc<?> assocOne = (BeanPropertyAssoc<?>) el.getBeanProperty();
|
||||
parentProp = new OrmQueryProperties(parentPath, assocOne.getTargetIdProperty());
|
||||
}
|
||||
|
||||
sortFetchPaths(d, parentProp, sorted);
|
||||
sorted.put(path, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark 'fetch joins' to 'many' properties over to 'query joins' where needed.
|
||||
*/
|
||||
void markQueryJoins(BeanDescriptor<?> beanDescriptor, String lazyLoadManyPath, boolean allowOne) {
|
||||
|
||||
// the name of the many fetch property if there is one
|
||||
String manyFetchProperty = null;
|
||||
|
||||
// flag that is set once the many fetch property is chosen
|
||||
boolean fetchJoinFirstMany = allowOne;
|
||||
|
||||
sortFetchPaths(beanDescriptor);
|
||||
|
||||
for (String fetchPath : fetchPaths.keySet()) {
|
||||
ElPropertyDeploy elProp = beanDescriptor.getElPropertyDeploy(fetchPath);
|
||||
if (elProp == null) {
|
||||
throw new PersistenceException("Invalid fetch path " + fetchPath + " from " + beanDescriptor.getFullName());
|
||||
}
|
||||
if (elProp.containsManySince(manyFetchProperty)) {
|
||||
|
||||
// this is a join to a *ToMany
|
||||
OrmQueryProperties chunk = fetchPaths.get(fetchPath);
|
||||
if (isQueryJoinCandidate(lazyLoadManyPath, chunk)) {
|
||||
// this is a 'fetch join' (included in main query)
|
||||
if (fetchJoinFirstMany) {
|
||||
// letting the first one remain a 'fetch join'
|
||||
fetchJoinFirstMany = false;
|
||||
manyFetchProperty = fetchPath;
|
||||
} else {
|
||||
// convert this one over to a 'query join'
|
||||
chunk.markForQueryJoin();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this path is a candidate for converting to a query join.
|
||||
*/
|
||||
private boolean isQueryJoinCandidate(String lazyLoadManyPath, OrmQueryProperties chunk) {
|
||||
return chunk.isFetchJoin()
|
||||
&& !isLazyLoadManyRoot(lazyLoadManyPath, chunk)
|
||||
&& !hasParentSecJoin(lazyLoadManyPath, chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is actually the root level of a +query/+lazy loading query.
|
||||
*/
|
||||
private boolean isLazyLoadManyRoot(String lazyLoadManyPath, OrmQueryProperties chunk) {
|
||||
return lazyLoadManyPath != null && lazyLoadManyPath.equals(chunk.getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* If the chunk has a parent that is a query or lazy join. In this case it does not need to be
|
||||
* converted.
|
||||
*/
|
||||
private boolean hasParentSecJoin(String lazyLoadManyPath, OrmQueryProperties chunk) {
|
||||
OrmQueryProperties parent = getParent(chunk);
|
||||
if (parent == null) {
|
||||
return false;
|
||||
} else {
|
||||
if (lazyLoadManyPath != null && lazyLoadManyPath.equals(parent.getPath())) {
|
||||
return false;
|
||||
} else {
|
||||
return !parent.isFetchJoin() || hasParentSecJoin(lazyLoadManyPath, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent chunk.
|
||||
*/
|
||||
private OrmQueryProperties getParent(OrmQueryProperties chunk) {
|
||||
String parentPath = chunk.getParentPath();
|
||||
return parentPath == null ? null : fetchPaths.get(parentPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set any default select clauses for the main bean and any joins that have not explicitly defined
|
||||
* a select clause.
|
||||
* <p>
|
||||
* That is this will use FetchType.LAZY to exclude some properties by default.
|
||||
* </p>
|
||||
*/
|
||||
public void setDefaultSelectClause(BeanDescriptor<?> desc) {
|
||||
|
||||
if (desc.hasDefaultSelectClause() && !hasSelectClause()) {
|
||||
baseProps = new OrmQueryProperties(null, desc.getDefaultSelectClause());
|
||||
}
|
||||
|
||||
for (OrmQueryProperties joinProps : fetchPaths.values()) {
|
||||
if (!joinProps.hasSelectClause()) {
|
||||
BeanDescriptor<?> assocDesc = desc.getBeanDescriptor(joinProps.getPath());
|
||||
if (assocDesc.hasDefaultSelectClause()) {
|
||||
fetch(joinProps.getPath(), assocDesc.getDefaultSelectClause(), joinProps.getFetchConfig());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasSelectClause() {
|
||||
return baseProps.hasSelectClause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the query detail has neither select properties specified or any joins defined.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return fetchPaths.isEmpty() && baseProps.allProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if there are no joins.
|
||||
*/
|
||||
public boolean isJoinsEmpty() {
|
||||
return fetchPaths.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the explicit bean join.
|
||||
* <p>
|
||||
* This is also used to Exclude the matching property from the parent select (aka remove the
|
||||
* foreign key) because it is now included in it's on node in the SqlTree.
|
||||
* </p>
|
||||
*/
|
||||
public void includeBeanJoin(String parentPath, String propertyName) {
|
||||
OrmQueryProperties parentChunk = getChunk(parentPath, true);
|
||||
parentChunk.includeBeanJoin(propertyName);
|
||||
}
|
||||
|
||||
public OrmQueryProperties getChunk(String path, boolean create) {
|
||||
if (path == null) {
|
||||
return baseProps;
|
||||
}
|
||||
OrmQueryProperties props = fetchPaths.get(path);
|
||||
if (create && props == null) {
|
||||
props = new OrmQueryProperties(path);
|
||||
fetch(props);
|
||||
return props;
|
||||
|
||||
} else {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the fetch path is included.
|
||||
*/
|
||||
public boolean includesPath(String path) {
|
||||
|
||||
OrmQueryProperties chunk = fetchPaths.get(path);
|
||||
|
||||
// may not have fetch properties if just +cache etc
|
||||
return chunk != null && !chunk.isCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fetch paths for this detail.
|
||||
*/
|
||||
public Set<String> getFetchPaths() {
|
||||
return fetchPaths.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying fetch path entries.
|
||||
*/
|
||||
public Set<Map.Entry<String, OrmQueryProperties>> entries() {
|
||||
return fetchPaths.entrySet();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
|
||||
/**
|
||||
* Parses a Object relational query statement into a OrmQueryDetail and OrmQueryAttributes.
|
||||
* <p>
|
||||
* The reason they are split into detail and attributes is that the AutoTune feature is used to
|
||||
* replace the OrmQueryDetail leaving the attributes unchanged.
|
||||
* </p>
|
||||
*/
|
||||
public class OrmQueryDetailParser {
|
||||
|
||||
private final OrmQueryDetail detail = new OrmQueryDetail();
|
||||
|
||||
private final SimpleTextParser parser;
|
||||
|
||||
public OrmQueryDetailParser(String oql) {
|
||||
this.parser = new SimpleTextParser(oql);
|
||||
}
|
||||
|
||||
public OrmQueryDetail parse() throws PersistenceException {
|
||||
|
||||
if (parser.isEmpty()) return detail;
|
||||
|
||||
parser.nextWord();
|
||||
processInitial();
|
||||
return detail;
|
||||
}
|
||||
|
||||
private void processInitial() {
|
||||
if (parser.isMatch("select")) {
|
||||
readSelect();
|
||||
} else if (parser.isMatch("find")) {
|
||||
OrmQueryProperties props = readFindFetch();
|
||||
detail.setBase(props);
|
||||
} else {
|
||||
process();
|
||||
}
|
||||
while (!parser.isFinished()) {
|
||||
process();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFetch() {
|
||||
return parser.isMatch("fetch") || parser.isMatch("join");
|
||||
}
|
||||
|
||||
private void process() {
|
||||
if (isFetch()) {
|
||||
detail.fetch(readFindFetch());
|
||||
} else {
|
||||
throw new PersistenceException("Query expected 'fetch', 'where','order by' or 'limit' keyword but got ["
|
||||
+ parser.getWord() + "] \r " + parser.getOql());
|
||||
}
|
||||
}
|
||||
|
||||
private void readSelect() {
|
||||
String props = parser.nextWord();
|
||||
if (props.startsWith("(")) {
|
||||
props = props.substring(1, props.length() - 1);
|
||||
OrmQueryProperties base = new OrmQueryProperties(null, props);
|
||||
detail.setBase(base);
|
||||
parser.nextWord();
|
||||
} else {
|
||||
process();
|
||||
}
|
||||
}
|
||||
|
||||
private OrmQueryProperties readFindFetch() {
|
||||
|
||||
boolean readAlias = false;
|
||||
|
||||
String props = null;
|
||||
String path = parser.nextWord();
|
||||
String token;
|
||||
while ((token = parser.nextWord()) != null) {
|
||||
if (!readAlias && parser.isMatch("as")) {
|
||||
// next token is alias
|
||||
parser.nextWord();
|
||||
readAlias = true;
|
||||
|
||||
} else if ('(' == token.charAt(0)) {
|
||||
props = token;
|
||||
parser.nextWord();
|
||||
break;
|
||||
|
||||
} else if (isFindFetchEnd()) {
|
||||
break;
|
||||
|
||||
} else if (!readAlias) {
|
||||
readAlias = true;
|
||||
|
||||
} else {
|
||||
throw new PersistenceException("Expected (props) or new 'fetch' 'where' but got " + token);
|
||||
}
|
||||
}
|
||||
if (props != null) {
|
||||
props = props.substring(1, props.length() - 1);
|
||||
}
|
||||
return new OrmQueryProperties(path, props);
|
||||
}
|
||||
|
||||
private boolean isFindFetchEnd() {
|
||||
return isFetch() || parser.isMatch("where") || parser.isMatch("order", "by");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.config.dbplatform.DatabasePlatform;
|
||||
import io.ebean.config.dbplatform.SqlLimitRequest;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
|
||||
public class OrmQueryLimitRequest implements SqlLimitRequest {
|
||||
|
||||
private final SpiQuery<?> ormQuery;
|
||||
|
||||
private final DatabasePlatform dbPlatform;
|
||||
|
||||
private final String sql;
|
||||
|
||||
private final String sqlOrderBy;
|
||||
|
||||
public OrmQueryLimitRequest(String sql, String sqlOrderBy, SpiQuery<?> ormQuery, DatabasePlatform dbPlatform) {
|
||||
this.sql = sql;
|
||||
this.sqlOrderBy = sqlOrderBy;
|
||||
this.ormQuery = ormQuery;
|
||||
this.dbPlatform = dbPlatform;
|
||||
}
|
||||
|
||||
public String getDbOrderBy() {
|
||||
return sqlOrderBy;
|
||||
}
|
||||
|
||||
public String getDbSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public int getFirstRow() {
|
||||
return ormQuery.getFirstRow();
|
||||
}
|
||||
|
||||
public int getMaxRows() {
|
||||
return ormQuery.getMaxRows();
|
||||
}
|
||||
|
||||
public boolean isDistinct() {
|
||||
return ormQuery.isDistinctQuery();
|
||||
}
|
||||
|
||||
public SpiQuery<?> getOrmQuery() {
|
||||
return ormQuery;
|
||||
}
|
||||
|
||||
public DatabasePlatform getDbPlatform() {
|
||||
return dbPlatform;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.OrderBy;
|
||||
import io.ebean.RawSql;
|
||||
import io.ebeaninternal.api.BindParams;
|
||||
import io.ebeaninternal.api.CQueryPlanKey;
|
||||
import io.ebeaninternal.api.HashQueryPlanBuilder;
|
||||
import io.ebeaninternal.api.SpiExpression;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import io.ebeaninternal.server.deploy.TableJoin;
|
||||
|
||||
/**
|
||||
* Query plan key for ORM queries.
|
||||
*/
|
||||
class OrmQueryPlanKey implements CQueryPlanKey {
|
||||
|
||||
private final String m2mIncludeTable;
|
||||
private final String orderByAsSting;
|
||||
private final SpiExpression where;
|
||||
private final SpiExpression having;
|
||||
private final RawSql.Key rawSqlKey;
|
||||
private final boolean hasIdValue;
|
||||
private final SpiQuery.Type type;
|
||||
private final int maxRows;
|
||||
private final int firstRow;
|
||||
private final boolean disableLazyLoading;
|
||||
private final boolean distinct;
|
||||
private final boolean sqlDistinct;
|
||||
private final String mapKey;
|
||||
private final SpiQuery.TemporalMode temporalMode;
|
||||
private final boolean forUpdate;
|
||||
private final String rootTableAlias;
|
||||
private final OrmUpdateProperties updateProperties;
|
||||
|
||||
private final int planHash;
|
||||
private final int bindCount;
|
||||
|
||||
OrmQueryPlanKey(TableJoin m2mIncludeTable, SpiQuery.Type type, OrmQueryDetail detail, int maxRows, int firstRow, boolean disableLazyLoading, OrderBy<?> orderBy, boolean distinct, boolean sqlDistinct, String mapKey, Object id, BindParams bindParams, SpiExpression whereExpressions, SpiExpression havingExpressions, SpiQuery.TemporalMode temporalMode, boolean forUpdate, String rootTableAlias, RawSql rawSql, OrmUpdateProperties updateProperties) {
|
||||
|
||||
this.m2mIncludeTable = m2mIncludeTable == null ? null : m2mIncludeTable.getTable();
|
||||
this.type = type;
|
||||
this.maxRows = maxRows;
|
||||
this.firstRow = firstRow;
|
||||
this.disableLazyLoading = disableLazyLoading;
|
||||
this.orderByAsSting = (orderBy == null) ? null : orderBy.toStringFormat();
|
||||
this.distinct = distinct;
|
||||
this.sqlDistinct = sqlDistinct;
|
||||
this.mapKey = mapKey;
|
||||
this.hasIdValue = (id != null);
|
||||
this.where = (whereExpressions == null) ? null : whereExpressions.copyForPlanKey();
|
||||
this.having = (havingExpressions == null) ? null : havingExpressions.copyForPlanKey();
|
||||
this.temporalMode = temporalMode;
|
||||
this.forUpdate = forUpdate;
|
||||
this.rootTableAlias = rootTableAlias;
|
||||
this.updateProperties = updateProperties;
|
||||
this.rawSqlKey = (rawSql == null) ? null : rawSql.getKey();
|
||||
|
||||
// exclude bind values and things unrelated to the sql being generated
|
||||
HashQueryPlanBuilder builder = new HashQueryPlanBuilder();
|
||||
|
||||
builder.add((type == null ? 0 : type.ordinal() + 1));
|
||||
builder.add(distinct).add(sqlDistinct);
|
||||
builder.add(firstRow).add(maxRows);
|
||||
builder.add(orderBy).add(forUpdate);
|
||||
builder.add(mapKey);
|
||||
builder.add(disableLazyLoading);
|
||||
builder.add(hasIdValue);
|
||||
builder.add(temporalMode);
|
||||
builder.add(rawSqlKey == null ? 0 : rawSqlKey.hashCode());
|
||||
builder.add(this.m2mIncludeTable);
|
||||
builder.add(rootTableAlias);
|
||||
|
||||
if (detail != null) {
|
||||
detail.queryPlanHash(builder);
|
||||
}
|
||||
if (bindParams != null) {
|
||||
bindParams.buildQueryPlanHash(builder);
|
||||
}
|
||||
if (where != null) {
|
||||
where.queryPlanHash(builder);
|
||||
}
|
||||
if (having != null) {
|
||||
having.queryPlanHash(builder);
|
||||
}
|
||||
if (updateProperties != null) {
|
||||
updateProperties.buildQueryPlanHash(builder);
|
||||
}
|
||||
|
||||
this.planHash = builder.getPlanHash();
|
||||
this.bindCount = builder.getBindCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPartialKey() {
|
||||
return planHash + "_" + bindCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return planHash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
OrmQueryPlanKey that = (OrmQueryPlanKey) o;
|
||||
|
||||
if (planHash != that.planHash) return false;
|
||||
if (bindCount != that.bindCount) return false;
|
||||
if (maxRows != that.maxRows) return false;
|
||||
if (firstRow != that.firstRow) return false;
|
||||
if (disableLazyLoading != that.disableLazyLoading) return false;
|
||||
if (distinct != that.distinct) return false;
|
||||
if (sqlDistinct != that.sqlDistinct) return false;
|
||||
if (forUpdate != that.forUpdate) return false;
|
||||
if (hasIdValue != that.hasIdValue) return false;
|
||||
if (type != that.type) return false;
|
||||
if (temporalMode != that.temporalMode) return false;
|
||||
if (m2mIncludeTable != null ? !m2mIncludeTable.equals(that.m2mIncludeTable) : that.m2mIncludeTable != null)
|
||||
return false;
|
||||
if (orderByAsSting != null ? !orderByAsSting.equals(that.orderByAsSting) : that.orderByAsSting != null)
|
||||
return false;
|
||||
if (where != null ? !where.isSameByPlan(that.where) : that.where != null) return false;
|
||||
if (having != null ? !having.isSameByPlan(that.having) : that.having != null) return false;
|
||||
if (updateProperties != null ? !updateProperties.isSameByPlan(that.updateProperties) : that.updateProperties != null)
|
||||
return false;
|
||||
if (rawSqlKey != null ? !rawSqlKey.equals(that.rawSqlKey) : that.rawSqlKey != null) return false;
|
||||
if (mapKey != null ? !mapKey.equals(that.mapKey) : that.mapKey != null) return false;
|
||||
return rootTableAlias != null ? rootTableAlias.equals(that.rootTableAlias) : that.rootTableAlias == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.ExpressionFactory;
|
||||
import io.ebean.FetchConfig;
|
||||
import io.ebean.OrderBy;
|
||||
import io.ebean.Query;
|
||||
import io.ebeaninternal.api.HashQueryPlanBuilder;
|
||||
import io.ebeaninternal.api.SpiExpression;
|
||||
import io.ebeaninternal.api.SpiExpressionFactory;
|
||||
import io.ebeaninternal.api.SpiExpressionList;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import io.ebeaninternal.server.expression.FilterExprPath;
|
||||
import io.ebeaninternal.server.expression.FilterExpressionList;
|
||||
import io.ebeaninternal.server.expression.Same;
|
||||
import io.ebeaninternal.server.query.SplitName;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents the Properties of an Object Relational query.
|
||||
*/
|
||||
public class OrmQueryProperties implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -8785582703966455658L;
|
||||
|
||||
static final FetchConfig DEFAULT_FETCH = new FetchConfig();
|
||||
|
||||
private final String parentPath;
|
||||
private final String path;
|
||||
|
||||
private final String rawProperties;
|
||||
private final String trimmedProperties;
|
||||
|
||||
private final LinkedHashSet<String> included;
|
||||
|
||||
private final FetchConfig fetchConfig;
|
||||
|
||||
/**
|
||||
* Flag set when this fetch path needs to be a query join.
|
||||
*/
|
||||
private boolean markForQueryJoin;
|
||||
|
||||
private boolean cache;
|
||||
|
||||
private boolean readOnly;
|
||||
|
||||
/**
|
||||
* 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 for root so path (and parentPath) are null.
|
||||
*/
|
||||
public OrmQueryProperties() {
|
||||
this((String) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct with a given path.
|
||||
*/
|
||||
public OrmQueryProperties(String path) {
|
||||
this.path = path;
|
||||
this.parentPath = SplitName.parent(path);
|
||||
this.rawProperties = null;
|
||||
this.trimmedProperties = null;
|
||||
this.included = null;
|
||||
this.fetchConfig = DEFAULT_FETCH;
|
||||
}
|
||||
|
||||
public OrmQueryProperties(String path, String rawProperties) {
|
||||
this(path, rawProperties, null);
|
||||
}
|
||||
|
||||
public OrmQueryProperties(String path, String rawProperties, FetchConfig fetchConfig) {
|
||||
|
||||
OrmQueryPropertiesParser.Response response = OrmQueryPropertiesParser.parse(rawProperties);
|
||||
|
||||
this.path = path;
|
||||
this.parentPath = SplitName.parent(path);
|
||||
this.rawProperties = rawProperties;
|
||||
this.trimmedProperties = response.properties;
|
||||
this.included = response.included;
|
||||
this.cache = response.cache;
|
||||
this.readOnly = response.readOnly;
|
||||
if (fetchConfig != null) {
|
||||
this.fetchConfig = fetchConfig;
|
||||
} else {
|
||||
this.fetchConfig = response.fetchConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public OrmQueryProperties(String path, LinkedHashSet<String> parsedProperties) {
|
||||
if (parsedProperties == null) {
|
||||
throw new IllegalArgumentException("parsedProperties is null");
|
||||
}
|
||||
|
||||
this.path = path;
|
||||
this.parentPath = SplitName.parent(path);
|
||||
// for rawSql parsedProperties can be empty (when only fetching Id property)
|
||||
this.included = parsedProperties;
|
||||
this.rawProperties = join(parsedProperties);
|
||||
this.trimmedProperties = rawProperties;
|
||||
this.cache = false;
|
||||
this.readOnly = false;
|
||||
this.fetchConfig = DEFAULT_FETCH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the set of properties into a comma delimited string.
|
||||
*/
|
||||
private String join(LinkedHashSet<String> parsedProperties) {
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
boolean first = true;
|
||||
for (String property : parsedProperties) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(property);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor.
|
||||
*/
|
||||
private OrmQueryProperties(OrmQueryProperties source) {
|
||||
|
||||
this.parentPath = source.parentPath;
|
||||
this.path = source.path;
|
||||
this.rawProperties = source.rawProperties;
|
||||
this.trimmedProperties = source.trimmedProperties;
|
||||
this.cache = source.cache;
|
||||
this.readOnly = source.readOnly;
|
||||
this.fetchConfig = source.fetchConfig;
|
||||
this.filterMany = source.filterMany;
|
||||
this.included = (source.included == null) ? null : new LinkedHashSet<>(source.included);
|
||||
if (includedBeanJoin != null) {
|
||||
this.includedBeanJoin = new HashSet<>(source.includedBeanJoin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the OrmQueryProperties.
|
||||
*/
|
||||
public OrmQueryProperties copy() {
|
||||
return new OrmQueryProperties(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a OrderBy.Property from the main query to this query join.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
void addSecJoinOrderProperty(OrderBy.Property orderProp) {
|
||||
if (orderBy == null) {
|
||||
orderBy = new OrderBy();
|
||||
}
|
||||
orderBy.add(orderProp);
|
||||
}
|
||||
|
||||
public FetchConfig getFetchConfig() {
|
||||
return fetchConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
markForQueryJoin = true;
|
||||
}
|
||||
return filterMany;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filterMany expression list (can be null).
|
||||
*/
|
||||
private 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;
|
||||
this.markForQueryJoin = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the select and joins for this query.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void configureBeanQuery(SpiQuery<?> query) {
|
||||
|
||||
if (trimmedProperties != null && !trimmedProperties.isEmpty()) {
|
||||
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 (OrmQueryProperties p : secondaryChildren) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSelectClause() {
|
||||
if ("*".equals(trimmedProperties)) {
|
||||
// explicitly selected all properties
|
||||
return true;
|
||||
}
|
||||
// explicitly selected some properties
|
||||
return included != null || filterMany != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the properties and configuration are empty.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return rawProperties == null || rawProperties.isEmpty();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder(40);
|
||||
append("", sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String append(String prefix, StringBuilder sb) {
|
||||
sb.append(prefix);
|
||||
if (path != null) {
|
||||
sb.append(path).append(" ");
|
||||
}
|
||||
if (!isEmpty()) {
|
||||
sb.append("(").append(rawProperties).append(") ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
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<>();
|
||||
}
|
||||
secondaryChildren.add(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw properties.
|
||||
*/
|
||||
public String getProperties() {
|
||||
return rawProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this includes all properties on the path.
|
||||
*/
|
||||
public boolean allProperties() {
|
||||
return 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) {
|
||||
return includedBeanJoin != null && includedBeanJoin.contains(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a bean join property.
|
||||
*/
|
||||
void includeBeanJoin(String propertyName) {
|
||||
if (includedBeanJoin == null) {
|
||||
includedBeanJoin = new HashSet<>();
|
||||
}
|
||||
includedBeanJoin.add(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<>(2 * (secondaryQueryJoins.size() + included.size()));
|
||||
temp.addAll(included);
|
||||
temp.addAll(secondaryQueryJoins);
|
||||
return temp;
|
||||
}
|
||||
|
||||
void addSecondaryQueryJoin(String property) {
|
||||
if (secondaryQueryJoins == null) {
|
||||
secondaryQueryJoins = new HashSet<>(4);
|
||||
}
|
||||
secondaryQueryJoins.add(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property set.
|
||||
*/
|
||||
public Set<String> getIncluded() {
|
||||
return included;
|
||||
}
|
||||
|
||||
boolean isIncluded(String propName) {
|
||||
|
||||
if (includedBeanJoin != null && includedBeanJoin.contains(propName)) {
|
||||
return false;
|
||||
}
|
||||
// all properties included
|
||||
return included == null || included.contains(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this path as needing to be a query join.
|
||||
*/
|
||||
void markForQueryJoin() {
|
||||
markForQueryJoin = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this path is a 'query join'.
|
||||
*/
|
||||
public boolean isQueryFetch() {
|
||||
return markForQueryJoin || getQueryFetchBatch() > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this path is a 'fetch join'.
|
||||
*/
|
||||
boolean isFetchJoin() {
|
||||
return !isQueryFetch() && !isLazyFetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this path is a lazy fetch.
|
||||
*/
|
||||
boolean isLazyFetch() {
|
||||
return getLazyFetchBatch() > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the batch size to use for the query join.
|
||||
*/
|
||||
public int getQueryFetchBatch() {
|
||||
return fetchConfig.getQueryBatchSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a query join should eagerly fetch 'all' rather than the 'first'.
|
||||
*/
|
||||
public boolean isQueryFetchAll() {
|
||||
return fetchConfig.isQueryAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the batch size to use for lazy loading.
|
||||
*/
|
||||
public int getLazyFetchBatch() {
|
||||
return fetchConfig.getLazyBatchSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this path has the +readonly option.
|
||||
*/
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this path has the +cache option to hit the cache.
|
||||
*/
|
||||
public boolean isCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent path.
|
||||
*/
|
||||
String getParentPath() {
|
||||
return parentPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path relative to the root of the graph.
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the properties are the same for autoTune purposes.
|
||||
*/
|
||||
boolean isSameByAutoTune(OrmQueryProperties p2) {
|
||||
if (included == null) {
|
||||
return p2 == null || p2.included == null;
|
||||
} else if (p2 == null) {
|
||||
return false;
|
||||
}
|
||||
return included.equals(p2.included);
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties are the same for query plan purposes.
|
||||
*/
|
||||
public boolean isSameByPlan(OrmQueryProperties p2) {
|
||||
|
||||
if (!Same.sameByValue(secondaryQueryJoins, p2.secondaryQueryJoins)) return false;
|
||||
if (!Same.sameByValue(included, p2.included)) return false;
|
||||
if (!Same.sameByNull(filterMany, p2.filterMany)) return false;
|
||||
if (filterMany != null && !filterMany.isSameByPlan(p2.filterMany)) return false;
|
||||
|
||||
return fetchConfig.equals(p2.fetchConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the query plan hash.
|
||||
*/
|
||||
public void queryPlanHash(HashQueryPlanBuilder builder) {
|
||||
|
||||
builder.add(path);
|
||||
builder.addOrdered(included);
|
||||
builder.add(secondaryQueryJoins);
|
||||
|
||||
builder.add(filterMany != null);
|
||||
if (filterMany != null) {
|
||||
filterMany.queryPlanHash(builder);
|
||||
}
|
||||
builder.add(fetchConfig.hashCode());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.FetchConfig;
|
||||
import io.ebeaninternal.server.lib.util.StringHelper;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
/**
|
||||
* Parses the path properties string.
|
||||
*/
|
||||
class OrmQueryPropertiesParser {
|
||||
|
||||
private static Response EMPTY = new Response();
|
||||
|
||||
/**
|
||||
* Immutable response of the parsed properties and options.
|
||||
*/
|
||||
static class Response {
|
||||
|
||||
final boolean readOnly;
|
||||
final boolean cache;
|
||||
final FetchConfig fetchConfig;
|
||||
final String properties;
|
||||
final LinkedHashSet<String> included;
|
||||
|
||||
Response(boolean readOnly, boolean cache, int queryFetchBatch, int lazyFetchBatch, String properties, LinkedHashSet<String> included) {
|
||||
this.readOnly = readOnly;
|
||||
this.cache = cache;
|
||||
this.properties = properties;
|
||||
this.included = included;
|
||||
if (lazyFetchBatch > -1 || queryFetchBatch > -1) {
|
||||
this.fetchConfig = new FetchConfig().lazy(lazyFetchBatch).query(queryFetchBatch);
|
||||
} else {
|
||||
this.fetchConfig = OrmQueryProperties.DEFAULT_FETCH;
|
||||
}
|
||||
}
|
||||
|
||||
Response() {
|
||||
this.readOnly = false;
|
||||
this.cache = false;
|
||||
this.fetchConfig = OrmQueryProperties.DEFAULT_FETCH;
|
||||
this.properties = "";
|
||||
this.included = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the path properties string returning the parsed properties and options.
|
||||
* In general it is comma delimited with some special strings like +lazy(20).
|
||||
*/
|
||||
public static Response parse(String rawProperties) {
|
||||
return new OrmQueryPropertiesParser(rawProperties).parse();
|
||||
}
|
||||
|
||||
private String inputProperties;
|
||||
|
||||
private String outputProperties = "";
|
||||
private boolean allProperties;
|
||||
private boolean readOnly;
|
||||
private boolean cache;
|
||||
private int queryFetchBatch = -1;
|
||||
private int lazyFetchBatch = -1;
|
||||
|
||||
private OrmQueryPropertiesParser(String inputProperties) {
|
||||
this.inputProperties = inputProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw string properties input.
|
||||
*/
|
||||
private Response parse() {
|
||||
|
||||
if (inputProperties == null || inputProperties.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
int pos = inputProperties.indexOf("+readonly");
|
||||
if (pos > -1) {
|
||||
inputProperties = StringHelper.replaceString(inputProperties, "+readonly", "");
|
||||
readOnly = true;
|
||||
}
|
||||
pos = inputProperties.indexOf("+cache");
|
||||
if (pos > -1) {
|
||||
inputProperties = StringHelper.replaceString(inputProperties, "+cache", "");
|
||||
cache = true;
|
||||
}
|
||||
pos = inputProperties.indexOf("+query");
|
||||
if (pos > -1) {
|
||||
queryFetchBatch = parseBatchHint(pos, "+query");
|
||||
}
|
||||
pos = inputProperties.indexOf("+lazy");
|
||||
if (pos > -1) {
|
||||
lazyFetchBatch = parseBatchHint(pos, "+lazy");
|
||||
}
|
||||
|
||||
LinkedHashSet<String> included = parseIncluded();
|
||||
String properties = (allProperties) ? "*" : outputProperties;
|
||||
return new Response(readOnly, cache, queryFetchBatch, lazyFetchBatch, properties, included);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the include separating by comma or semicolon.
|
||||
*/
|
||||
private LinkedHashSet<String> parseIncluded() {
|
||||
|
||||
inputProperties = inputProperties.trim();
|
||||
if (inputProperties.isEmpty()) {
|
||||
// default properties
|
||||
return null;
|
||||
}
|
||||
if (inputProperties.equals("*")) {
|
||||
// explicit all properties
|
||||
allProperties = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] res = inputProperties.split(",");
|
||||
|
||||
StringBuilder sb = new StringBuilder(70);
|
||||
LinkedHashSet<String> propertySet = new LinkedHashSet<>(res.length * 2);
|
||||
|
||||
int count = 0;
|
||||
String temp;
|
||||
for (String re : res) {
|
||||
temp = re.trim();
|
||||
if (!temp.isEmpty()) {
|
||||
if (count > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(temp);
|
||||
propertySet.add(temp);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (propertySet.isEmpty()) {
|
||||
// default properties
|
||||
return null;
|
||||
}
|
||||
|
||||
if (propertySet.contains("*")) {
|
||||
// explicit all properties
|
||||
allProperties = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
// partial properties
|
||||
outputProperties = sb.toString();
|
||||
return propertySet;
|
||||
}
|
||||
|
||||
private int parseBatchHint(int pos, String option) {
|
||||
|
||||
int startPos = pos + option.length();
|
||||
int endPos = findEndPos(startPos, inputProperties);
|
||||
if (endPos == -1) {
|
||||
inputProperties = StringHelper.replaceString(inputProperties, option, "");
|
||||
return 0;
|
||||
|
||||
} else {
|
||||
|
||||
String batchParam = inputProperties.substring(startPos + 1, endPos);
|
||||
|
||||
if (endPos + 1 >= inputProperties.length()) {
|
||||
inputProperties = inputProperties.substring(0, pos);
|
||||
} else {
|
||||
inputProperties = inputProperties.substring(0, pos) + inputProperties.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) {
|
||||
throw new RuntimeException("Error could not find ')' in " + props + " after position " + pos);
|
||||
}
|
||||
return endPara;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebeaninternal.api.SpiQuerySecondary;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The secondary query paths for 'query joins' and 'lazy loading'.
|
||||
*/
|
||||
class OrmQuerySecondary implements SpiQuerySecondary {
|
||||
|
||||
private final List<OrmQueryProperties> queryJoins;
|
||||
|
||||
private final List<OrmQueryProperties> lazyJoins;
|
||||
|
||||
/**
|
||||
* Construct with the 'query join' and 'lazy join' path properties.
|
||||
*/
|
||||
OrmQuerySecondary(List<OrmQueryProperties> queryJoins, List<OrmQueryProperties> lazyJoins) {
|
||||
this.queryJoins = queryJoins;
|
||||
this.lazyJoins = lazyJoins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of path/properties that are query join loaded.
|
||||
*/
|
||||
@Override
|
||||
public List<OrmQueryProperties> getQueryJoins() {
|
||||
return queryJoins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of path/properties that are lazy loaded.
|
||||
*/
|
||||
@Override
|
||||
public List<OrmQueryProperties> getLazyJoins() {
|
||||
return lazyJoins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebeaninternal.api.HashQueryPlanBuilder;
|
||||
import io.ebeaninternal.server.deploy.DeployParser;
|
||||
import io.ebeaninternal.server.persist.Binder;
|
||||
import io.ebeaninternal.server.type.DataBind;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Set properties for a UpdateQuery.
|
||||
*/
|
||||
public class OrmUpdateProperties {
|
||||
|
||||
private static final NullValue NULL_VALUE = new NullValue();
|
||||
|
||||
private static final NoneValue NONE_VALUE = new NoneValue();
|
||||
|
||||
/**
|
||||
* Bind value used in the set clause for update query.
|
||||
* It may/may not have bind values etc.
|
||||
*/
|
||||
public static abstract class Value {
|
||||
|
||||
public void bind(Binder binder, DataBind dataBind) throws SQLException {
|
||||
// default to no bind values
|
||||
}
|
||||
|
||||
public String bindClause() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public int getBindCount() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set property to null.
|
||||
*/
|
||||
private static class NullValue extends Value {
|
||||
@Override
|
||||
public String bindClause() {
|
||||
return "=null";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set property to a simple value.
|
||||
*/
|
||||
private static class SimpleValue extends Value {
|
||||
|
||||
final Object value;
|
||||
|
||||
SimpleValue(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBindCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String bindClause() {
|
||||
return "=?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(Binder binder, DataBind dataBind) throws SQLException {
|
||||
binder.bindObject(dataBind, value);
|
||||
dataBind.append(value).append(",");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set using an expression with no bind value.
|
||||
*/
|
||||
private static class NoneValue extends Value {
|
||||
@Override
|
||||
public String bindClause() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set using an expression with many bind values.
|
||||
*/
|
||||
private static class RawArrayValue extends Value {
|
||||
|
||||
final Object[] bindValues;
|
||||
|
||||
RawArrayValue(Object[] bindValues) {
|
||||
this.bindValues = bindValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBindCount() {
|
||||
return bindValues.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(Binder binder, DataBind dataBind) throws SQLException {
|
||||
for (Object val : bindValues) {
|
||||
binder.bindObject(dataBind, val);
|
||||
dataBind.append(val).append(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The set properties/expressions and their bind values.
|
||||
*/
|
||||
private LinkedHashMap<String, Value> values = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Normal set property.
|
||||
*/
|
||||
public void set(String propertyName, Object value) {
|
||||
if (value == null) {
|
||||
values.put(propertyName, NULL_VALUE);
|
||||
|
||||
} else {
|
||||
values.put(propertyName, new SimpleValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a raw expression with no bind values.
|
||||
*/
|
||||
public void setRaw(String propertyName) {
|
||||
values.put(propertyName, NONE_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a raw expression with many bind values.
|
||||
*/
|
||||
void setRaw(String propertyExpression, Object... vals) {
|
||||
if (vals.length == 0) {
|
||||
setRaw(propertyExpression);
|
||||
} else {
|
||||
values.put(propertyExpression, new RawArrayValue(vals));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this update has the same logical set clause.
|
||||
*/
|
||||
public boolean isSameByPlan(OrmUpdateProperties that) {
|
||||
return that.values.size() == values.size()
|
||||
&& logicalSetClause().equals(that.logicalSetClause());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the hash for the query plan caching.
|
||||
*/
|
||||
void buildQueryPlanHash(HashQueryPlanBuilder builder) {
|
||||
builder.add(OrmUpdateProperties.class);
|
||||
Set<Map.Entry<String, Value>> entries = values.entrySet();
|
||||
for (Map.Entry<String, Value> entry : entries) {
|
||||
builder.add(entry.getKey());
|
||||
builder.bind(entry.getValue().getBindCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind all the bind values for the update set clause.
|
||||
*/
|
||||
public void bind(Binder binder, DataBind dataBind) throws SQLException {
|
||||
for (Value bindValue : values.values()) {
|
||||
bindValue.bind(binder, dataBind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the actual set clause converting logical property names to db columns etc.
|
||||
*/
|
||||
public String buildSetClause(DeployParser deployParser) {
|
||||
|
||||
int setCount = 0;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (Map.Entry<String, Value> entry : values.entrySet()) {
|
||||
String property = entry.getKey();
|
||||
if (setCount++ > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
// translate to db columns and remove table alias placeholders
|
||||
sb.append(deployParser.parse(property).replace("${}", ""));
|
||||
sb.append(entry.getValue().bindClause());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a logical set clause to use for isSameByPlan() use.
|
||||
*/
|
||||
private String logicalSetClause() {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, Value> entry : values.entrySet()) {
|
||||
sb.append(", ");
|
||||
sb.append(entry.getKey()).append(entry.getValue().bindClause());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
public class SimpleTextParser {
|
||||
|
||||
private final String oql;
|
||||
private final char[] chars;
|
||||
private final int eof;
|
||||
|
||||
private int pos;
|
||||
private String word;
|
||||
private String lowerWord;
|
||||
|
||||
public SimpleTextParser(String oql) {
|
||||
this.oql = oql;
|
||||
this.chars = oql.toCharArray();
|
||||
this.eof = oql.length();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return oql.isEmpty();
|
||||
}
|
||||
|
||||
public int getPos() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
String getOql() {
|
||||
return oql;
|
||||
}
|
||||
|
||||
public String getWord() {
|
||||
return word;
|
||||
}
|
||||
|
||||
private String peekNextWord() {
|
||||
int origPos = pos;
|
||||
String nw = nextWordInternal();
|
||||
pos = origPos;
|
||||
return nw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the current and the next word.
|
||||
*/
|
||||
public boolean isMatch(String lowerMatch, String nextWordMatch) {
|
||||
|
||||
if (isMatch(lowerMatch)) {
|
||||
String nw = peekNextWord();
|
||||
if (nw != null) {
|
||||
nw = nw.toLowerCase();
|
||||
return nw.equals(nextWordMatch);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return word == null;
|
||||
}
|
||||
|
||||
public int findWordLower(String lowerMatch, int afterPos) {
|
||||
this.pos = afterPos;
|
||||
return findWordLower(lowerMatch);
|
||||
}
|
||||
|
||||
public int findWordLower(String lowerMatch) {
|
||||
do {
|
||||
if (nextWord() == null) {
|
||||
return -1;
|
||||
}
|
||||
if (lowerMatch.equals(lowerWord)) {
|
||||
return pos - lowerWord.length();
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the current word.
|
||||
*/
|
||||
public boolean isMatch(String lowerMatch) {
|
||||
return lowerMatch.equals(lowerWord);
|
||||
}
|
||||
|
||||
public String nextWord() {
|
||||
word = nextWordInternal();
|
||||
if (word != null) {
|
||||
lowerWord = word.toLowerCase();
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
private String nextWordInternal() {
|
||||
trimLeadingWhitespace();
|
||||
if (pos >= eof) {
|
||||
return null;
|
||||
}
|
||||
int start = pos;
|
||||
if (chars[pos] == '(') {
|
||||
moveToClose();
|
||||
} else {
|
||||
moveToEndOfWord();
|
||||
}
|
||||
return oql.substring(start, pos);
|
||||
}
|
||||
|
||||
private void moveToClose() {
|
||||
|
||||
pos++;
|
||||
int openParenthesisCount = 0;
|
||||
|
||||
for (; pos < eof; pos++) {
|
||||
char c = chars[pos];
|
||||
if (c == '(') {
|
||||
// count nested parenthesis
|
||||
openParenthesisCount++;
|
||||
|
||||
} else if (c == ')') {
|
||||
if (openParenthesisCount > 0) {
|
||||
// still in nested parenthesis
|
||||
--openParenthesisCount;
|
||||
} else {
|
||||
// we have found the end
|
||||
pos++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void moveToEndOfWord() {
|
||||
char c = chars[pos];
|
||||
boolean isOperator = isOperator(c);
|
||||
for (; pos < eof; pos++) {
|
||||
c = chars[pos];
|
||||
if (isWordTerminator(c, isOperator)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWordTerminator(char c, boolean isOperator) {
|
||||
return Character.isWhitespace(c) || (isOperator(c) ? !isOperator : c == '(' || isOperator);
|
||||
}
|
||||
|
||||
private boolean isOperator(char c) {
|
||||
switch (c) {
|
||||
case '<':
|
||||
return true;
|
||||
case '>':
|
||||
return true;
|
||||
case '=':
|
||||
return true;
|
||||
case '!':
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void trimLeadingWhitespace() {
|
||||
for (; pos < eof; pos++) {
|
||||
char c = chars[pos];
|
||||
if (!Character.isWhitespace(c)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
Reference in New Issue
Block a user