
com.avaje.ebeaninternal.server.query.SqlTreeBuilder Maven / Gradle / Ivy
/**
* Copyright (C) 2006 Robin Bygrave
*
* This file is part of Ebean.
*
* Ebean is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Ebean is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Ebean; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.avaje.ebeaninternal.server.query;
import com.avaje.ebean.Query.Type;
import com.avaje.ebeaninternal.api.ManyWhereJoins;
import com.avaje.ebeaninternal.api.SpiQuery;
import com.avaje.ebeaninternal.server.core.OrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.InheritInfo;
import com.avaje.ebeaninternal.server.deploy.TableJoin;
import com.avaje.ebeaninternal.server.el.ElPropertyValue;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryProperties;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Factory for SqlTree.
*/
public class SqlTreeBuilder {
private static final Logger logger = Logger.getLogger(SqlTreeBuilder.class.getName());
private final SpiQuery> query;
private final BeanDescriptor> desc;
private final OrmQueryDetail queryDetail;
private final StringBuilder summary = new StringBuilder();
private final CQueryPredicates predicates;
private final boolean subQuery;
/**
* Property if resultSet contains master and detail rows.
*/
private BeanPropertyAssocMany> manyProperty;
private String manyPropertyName;
private final SqlTreeAlias alias;
private final DefaultDbSqlContext ctx;
private final HashSet selectIncludes = new HashSet();
private final ManyWhereJoins manyWhereJoins;
private final TableJoin includeJoin;
private final boolean rawSql;
/**
* Construct for RawSql query.
*/
public SqlTreeBuilder(OrmQueryRequest> request, CQueryPredicates predicates, OrmQueryDetail queryDetail) {
this.rawSql = true;
this.desc = request.getBeanDescriptor();
this.query = null;
this.subQuery = false;
this.queryDetail = queryDetail;
this.predicates = predicates;
this.includeJoin = null;
this.manyWhereJoins = null;
this.alias = null;//new SqlTreeAlias(request.getBeanDescriptor().getBaseTableAlias());
this.ctx = null;//new DefaultDbSqlContext(alias, tableAliasPlaceHolder, columnAliasPrefix, !subQuery);
}
/**
* The predicates are used to determine if 'extra' joins are required to
* support the where and/or order by clause. If so these extra joins are
* added to the root node.
*/
public SqlTreeBuilder(String tableAliasPlaceHolder, String columnAliasPrefix, OrmQueryRequest> request,
CQueryPredicates predicates) {
this.rawSql = false;
this.desc = request.getBeanDescriptor();
this.query = request.getQuery();
this.subQuery = Type.SUBQUERY.equals(query.getType());
this.includeJoin = query.getIncludeTableJoin();
this.manyWhereJoins = query.getManyWhereJoins();
this.queryDetail = query.getDetail();
this.predicates = predicates;
this.alias = new SqlTreeAlias(request.getBeanDescriptor().getBaseTableAlias());
this.ctx = new DefaultDbSqlContext(alias, tableAliasPlaceHolder, columnAliasPrefix, !subQuery);
}
/**
* Build based on the includes and using the BeanJoinTree.
*/
public SqlTree build() {
//BeanDescriptor> desc = request.getBeanDescriptor();
SqlTree sqlTree = new SqlTree();
summary.append(desc.getName());
// build the appropriate chain of SelectAdapter's
buildRoot(desc, sqlTree);
// build the actual String
SqlTreeNode rootNode = sqlTree.getRootNode();
if (!rawSql){
sqlTree.setSelectSql(buildSelectClause(rootNode));
sqlTree.setFromSql(buildFromClause(rootNode));
sqlTree.setInheritanceWhereSql(buildWhereClause(rootNode));
sqlTree.setEncryptedProps(ctx.getEncryptedProps());
}
sqlTree.setIncludes(queryDetail.getIncludes());
sqlTree.setSummary(summary.toString());
if (manyPropertyName != null){
ElPropertyValue manyPropEl = desc.getElGetValue(manyPropertyName);
sqlTree.setManyProperty(manyProperty, manyPropertyName, manyPropEl);
}
return sqlTree;
}
private String buildSelectClause(SqlTreeNode rootNode) {
if (rawSql){
return "Not Used";
}
rootNode.appendSelect(ctx, subQuery);
String selectSql = ctx.getContent();
// trim off the first comma
if (selectSql.length() >= SqlTreeNode.COMMA.length()) {
selectSql = selectSql.substring(SqlTreeNode.COMMA.length());
}
return selectSql;
}
private String buildWhereClause(SqlTreeNode rootNode) {
if (rawSql){
return "Not Used";
}
rootNode.appendWhere(ctx);
return ctx.getContent();
}
private String buildFromClause(SqlTreeNode rootNode) {
if (rawSql){
return "Not Used";
}
rootNode.appendFrom(ctx, false);
return ctx.getContent();
}
private void buildRoot(BeanDescriptor> desc, SqlTree sqlTree) {
SqlTreeNode selectRoot = buildSelectChain(null, null, desc, null);
sqlTree.setRootNode(selectRoot);
if (!rawSql){
alias.addJoin(queryDetail.getIncludes());
alias.addJoin(predicates.getPredicateIncludes());
alias.addManyWhereJoins(manyWhereJoins.getJoins());
// build set of table alias
alias.buildAlias();
predicates.parseTableAlias(alias);
}
}
/**
* Recursively build the query tree depending on what leaves in the tree
* should be included.
*/
private SqlTreeNode buildSelectChain(String prefix, BeanPropertyAssoc> prop, BeanDescriptor> desc,
List joinList) {
List myJoinList = new ArrayList();
BeanPropertyAssocOne>[] ones = desc.propertiesOne();
for (int i = 0; i < ones.length; i++) {
String propPrefix = SplitName.add(prefix, ones[i].getName());
if (isIncludeBean(propPrefix, ones[i])) {
selectIncludes.add(propPrefix);
buildSelectChain(propPrefix, ones[i], ones[i].getTargetDescriptor(), myJoinList);
}
}
BeanPropertyAssocMany>[] manys = desc.propertiesMany();
for (int i = 0; i < manys.length; i++) {
String propPrefix = SplitName.add(prefix, manys[i].getName());
if (isIncludeMany(prefix, propPrefix, manys[i])) {
selectIncludes.add(propPrefix);
buildSelectChain(propPrefix, manys[i], manys[i].getTargetDescriptor(), myJoinList);
}
}
if (prefix == null && !rawSql) {
addManyWhereJoins(myJoinList);
}
SqlTreeNode selectNode = buildNode(prefix, prop, desc, myJoinList);
if (joinList != null) {
joinList.add(selectNode);
}
return selectNode;
}
/**
* Add joins used to support where clause predicates on 'many' properties.
*
* These joins are effectively independent of any fetch joins on 'many' properties.
*
*/
private void addManyWhereJoins(List myJoinList) {
Set includes = manyWhereJoins.getJoins();
for (String joinProp : includes) {
BeanPropertyAssoc> beanProperty = (BeanPropertyAssoc>) desc.getBeanPropertyFromPath(joinProp);
SqlTreeNodeManyWhereJoin nodeJoin = new SqlTreeNodeManyWhereJoin(joinProp, beanProperty);
myJoinList.add(nodeJoin);
}
}
private SqlTreeNode buildNode(String prefix, BeanPropertyAssoc> prop, BeanDescriptor> desc,
List myList) {
OrmQueryProperties queryProps = queryDetail.getChunk(prefix, false);
SqlTreeProperties props = getBaseSelect(desc, queryProps);
if (prefix == null) {
buildExtraJoins(desc, myList);
return new SqlTreeNodeRoot(desc, props, myList, !subQuery, includeJoin);
} else if (prop instanceof BeanPropertyAssocMany>) {
return new SqlTreeNodeManyRoot(prefix, (BeanPropertyAssocMany>) prop, props, myList);
} else {
return new SqlTreeNodeBean(prefix, prop, props, myList, true);
}
}
/**
* Build extra joins to support properties used in where clause but not
* already in select clause.
*/
private void buildExtraJoins(BeanDescriptor> desc, List myList) {
if (rawSql){
return;
}
Set predicateIncludes = predicates.getPredicateIncludes();
if (predicateIncludes == null) {
return;
}
// Note includes - basically means joins.
// The selectIncludes is the set of joins that are required to support
// the 'select' part of the query. We may need to add other joins to
// support the predicates or order by clauses.
// remove ManyWhereJoins from the predicateIncludes
predicateIncludes.removeAll(manyWhereJoins.getJoins());
// look for predicateIncludes that are not in selectIncludes and add
// them as extra joins to the query
IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, predicateIncludes);
Collection extraJoins = extraJoinDistill.getExtraJoinRootNodes();
if (extraJoins.isEmpty()) {
return;
} else {
// add extra joins required to support predicates
// and/or order by clause
Iterator it = extraJoins.iterator();
while (it.hasNext()) {
SqlTreeNodeExtraJoin extraJoin = it.next();
myList.add(extraJoin);
if (extraJoin.isManyJoin()) {
// as we are now going to join to the many then we need
// to add the distinct to the sql query to stop duplicate
// rows...
query.setDistinct(true);
}
}
}
}
/**
* A subQuery has slightly different rules in that it just generates SQL
* (into the where clause) and its properties are not required to read the
* resultSet etc.
*
* This means it can included individual properties of an embedded bean.
*
*/
private void addPropertyToSubQuery(SqlTreeProperties selectProps, BeanDescriptor> desc,
OrmQueryProperties queryProps, String propName) {
BeanProperty p = desc.findBeanProperty(propName);
if (p == null) {
logger.log(Level.SEVERE, "property [" + propName + "]not found on " + desc + " for query - excluding it.");
}
else if (p instanceof BeanPropertyAssoc> && p.isEmbedded()) {
// if the property is embedded we need to lookup the real column name
int pos = propName.indexOf(".");
if (pos > -1) {
String name = propName.substring(pos + 1);
p = ((BeanPropertyAssoc>) p).getTargetDescriptor().findBeanProperty(name);
}
}
selectProps.add(p);
}
private void addProperty(SqlTreeProperties selectProps, BeanDescriptor> desc, OrmQueryProperties queryProps,
String propName) {
if (subQuery) {
addPropertyToSubQuery(selectProps, desc, queryProps, propName);
return;
}
int basePos = propName.indexOf('.');
if (basePos > -1) {
// property on an embedded bean. Embedded beans do not yet
// support being partially populated so we include the
// 'base' property and make sure we only do that once
String baseName = propName.substring(0, basePos);
// make sure we only included the base/embedded bean once
if (!selectProps.containsProperty(baseName)) {
BeanProperty p = desc.findBeanProperty(baseName);
if (p == null) {
String m = "property [" + propName + "] not found on " + desc + " for query - excluding it.";
logger.log(Level.SEVERE, m);
} else if (p.isEmbedded()) {
// add the embedded bean (and effectively
// all its properties)
selectProps.add(p);
// also make sure it is added to included properties
// to avoid unnecessary lazy loading
selectProps.getIncludedProperties().add(baseName);
} else {
String m = "property [" + p.getFullBeanName()
+ "] expected to be an embedded bean for query - excluding it.";
logger.log(Level.SEVERE, m);
}
}
} else {
// find the property including searching the
// sub class hierarchy if required
BeanProperty p = desc.findBeanProperty(propName);
if (p == null) {
logger.log(Level.SEVERE, "property [" + propName + "] not found on " + desc
+ " for query - excluding it.");
} else if (p.isId()) {
// do not bother to include id for normal queries as the
// id is always added (except for subQueries)
} else if (p instanceof BeanPropertyAssoc>) {
// need to check if this property should be
// excluded. This occurs when this property is
// included as a bean join. With a bean join
// the property should be excluded as the bean
// join has its own node in the SqlTree.
if (!queryProps.isIncludedBeanJoin(p.getName())) {
// include the property... which basically
// means include the foreign key column(s)
selectProps.add(p);
}
} else {
selectProps.add(p);
}
}
}
private SqlTreeProperties getBaseSelectPartial(BeanDescriptor> desc, OrmQueryProperties queryProps) {
SqlTreeProperties selectProps = new SqlTreeProperties();
selectProps.setReadOnly(queryProps.isReadOnly());
selectProps.setIncludedProperties(queryProps.getAllIncludedProperties());
// add properties in the order in which they appear
// in the query. Gives predictable sql/properties for
// use with SqlSelect type queries.
// Also note that this can include transient properties.
// This makes sense for transient properties used to
// hold sum() count() type values (with SqlSelect)
Iterator it = queryProps.getSelectProperties();
while (it.hasNext()) {
String propName = it.next();
if (propName.length() > 0) {
addProperty(selectProps, desc, queryProps, propName);
}
}
return selectProps;
}
private SqlTreeProperties getBaseSelect(BeanDescriptor> desc, OrmQueryProperties queryProps) {
boolean partial = queryProps != null && !queryProps.allProperties();
if (partial) {
return getBaseSelectPartial(desc, queryProps);
}
SqlTreeProperties selectProps = new SqlTreeProperties();
// normal simple properties of the bean
selectProps.add(desc.propertiesBaseScalar());
selectProps.add(desc.propertiesBaseCompound());
selectProps.add(desc.propertiesEmbedded());
BeanPropertyAssocOne>[] propertiesOne = desc.propertiesOne();
for (int i = 0; i < propertiesOne.length; i++) {
if (queryProps != null && queryProps.isIncludedBeanJoin(propertiesOne[i].getName())) {
// if it is a joined bean... then don't add the property
// as it will have its own entire Node in the SqlTree
} else {
selectProps.add(propertiesOne[i]);
}
}
selectProps.setTableJoins(desc.tableJoins());
InheritInfo inheritInfo = desc.getInheritInfo();
if (inheritInfo != null) {
// add sub type properties
inheritInfo.addChildrenProperties(selectProps);
}
return selectProps;
}
/**
* Return true if this many node should be included in the query.
*/
private boolean isIncludeMany(String prefix, String propName, BeanPropertyAssocMany> manyProp) {
if (queryDetail.isJoinsEmpty()) {
return false;
}
if (queryDetail.includes(propName)) {
if (manyProperty != null) {
// only one many associated allowed to be included in fetch
if (logger.isLoggable(Level.FINE)) {
String msg = "Not joining [" + propName + "] as already joined to a Many[" + manyProperty + "].";
logger.fine(msg);
}
return false;
}
manyProperty = manyProp;
manyPropertyName = propName;
summary.append(" +many:").append(propName);
return true;
}
return false;
}
/**
* Test to see if we are including this node into the query.
*
* Return true if this node is FULLY included resulting in table join. If
* the node is not included but its parent has been included then a "bean
* proxy" is added and false is returned.
*
*/
private boolean isIncludeBean(String prefix, BeanPropertyAssocOne> prop) {
if (queryDetail.includes(prefix)) {
// explicitly included
summary.append(", ").append(prefix);
String[] splitNames = SplitName.split(prefix);
queryDetail.includeBeanJoin(splitNames[0], splitNames[1]);
return true;
}
return false;
}
/**
* Takes the select includes and the predicates includes and determines the
* extra joins required to support the predicates (that are not already
* supported by the select includes).
*
* This returns ONLY the leaves. The joins for the leaves
*
*/
private static class IncludesDistiller {
private final Set selectIncludes;
private final Set predicateIncludes;
/**
* Contains the 'root' extra joins. We only return the roots back.
*/
private final Map joinRegister = new HashMap();
/**
* Register of all the extra join nodes.
*/
private final Map rootRegister = new HashMap();
private final BeanDescriptor> desc;
private IncludesDistiller(BeanDescriptor> desc, Set selectIncludes, Set predicateIncludes) {
this.desc = desc;
this.selectIncludes = selectIncludes;
this.predicateIncludes = predicateIncludes;
}
/**
* Build the collection of extra joins returning just the roots.
*
* each root returned here could contain a little tree of joins. This
* follows the more natural pattern and allows for forcing outer joins
* from a join to a 'many' down through the rest of its tree.
*
*/
private Collection getExtraJoinRootNodes() {
String[] extras = findExtras();
if (extras.length == 0) {
return rootRegister.values();
}
// sort so we process only getting the leaves
// excluding nodes between root and the leaf
Arrays.sort(extras);
// reverse order so get the leaves first...
for (int i = 0; i < extras.length; i++) {
createExtraJoin(extras[i]);
}
return rootRegister.values();
}
private void createExtraJoin(String includeProp) {
SqlTreeNodeExtraJoin extraJoin = createJoinLeaf(includeProp);
if (extraJoin != null) {
// add the extra join...
// find root of this extra join... linking back to the
// parents (creating the tree) as it goes.
SqlTreeNodeExtraJoin root = findExtraJoinRoot(includeProp, extraJoin);
// register the root because these are the only ones we
// return back.
rootRegister.put(root.getName(), root);
}
}
/**
* Create a SqlTreeNodeExtraJoin, register and return it.
*/
private SqlTreeNodeExtraJoin createJoinLeaf(String propertyName) {
ElPropertyValue elGetValue = desc.getElGetValue(propertyName);
if (elGetValue == null) {
// this can occur for master detail queries
// with concatenated keys (so not an error now)
return null;
}
BeanProperty beanProperty = elGetValue.getBeanProperty();
if (beanProperty instanceof BeanPropertyAssoc>) {
BeanPropertyAssoc> assocProp = (BeanPropertyAssoc>) beanProperty;
if (assocProp.isEmbedded()) {
// no extra join required for embedded beans
return null;
}
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, assocProp);
joinRegister.put(propertyName, extraJoin);
return extraJoin;
}
return null;
}
/**
* Find the root the this extra join tree.
*
* This may need to create a parent join implicitly if a predicate join
* 'skips' a level. e.g. where details.user.id = 1 (maybe join to
* details is not specified and is implicitly created.
*
*/
private SqlTreeNodeExtraJoin findExtraJoinRoot(String includeProp, SqlTreeNodeExtraJoin childJoin) {
int dotPos = includeProp.lastIndexOf('.');
if (dotPos == -1) {
// no parent possible(parent is root)
return childJoin;
} else {
// look in register ...
String parentPropertyName = includeProp.substring(0, dotPos);
if (selectIncludes.contains(parentPropertyName)) {
// parent already handled by select
return childJoin;
}
SqlTreeNodeExtraJoin parentJoin = joinRegister.get(parentPropertyName);
if (parentJoin == null) {
// we need to create this the parent implicitly...
parentJoin = createJoinLeaf(parentPropertyName);
}
parentJoin.addChild(childJoin);
return findExtraJoinRoot(parentPropertyName, parentJoin);
}
}
/**
* Find the extra joins required by predicates and not already taken
* care of by the select.
*/
private String[] findExtras() {
List extras = new ArrayList();
for (String predProp : predicateIncludes) {
if (!selectIncludes.contains(predProp)) {
extras.add(predProp);
}
}
return extras.toArray(new String[extras.size()]);
}
}
}