/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.query;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.Property;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.ObjEntity;
/**
* <p>A helper builder for queries selecting individual properties based on the root object.</p>
* <p>
* It can be used to select properties of the object itself, properties of related entities
* or some function calls (including aggregate functions).
* </p>
* <p>
* Usage examples: <pre>
* {@code
* // select list of names:
* List<String> names = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME).select(context);
*
* // select count:
* long count = ObjectSelect.columnQuery(Artist.class, Property.COUNT).selectOne();
*
* // select only required properties of an entity:
* List<Object[]> data = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
* .where(Artist.ARTIST_NAME.like("Picasso%))
* .select(context);
* }
* </pre>
* </p>
* <p><b>Note: this class can't be instantiated directly. Use {@link ObjectSelect}.</b></p>
* @see ObjectSelect#columnQuery(Class, Property)
*
* @since 4.0
*/
public class ColumnSelect<T> extends FluentSelect<T> {
private Collection<Property<?>> columns;
private boolean havingExpressionIsActive = false;
// package private for tests
boolean singleColumn = true;
private Expression having;
boolean distinct;
boolean suppressDistinct;
protected ColumnSelect() {
super();
}
/**
* Copy constructor to convert ObjectSelect to ColumnSelect
*/
protected ColumnSelect(ObjectSelect<T> select) {
super();
this.name = select.name;
this.entityType = select.entityType;
this.entityName = select.entityName;
this.dbEntityName = select.dbEntityName;
this.where = select.where;
this.orderings = select.orderings;
this.prefetches = select.prefetches;
this.limit = select.limit;
this.offset = select.offset;
this.pageSize = select.pageSize;
this.statementFetchSize = select.statementFetchSize;
this.cacheStrategy = select.cacheStrategy;
this.cacheGroup = select.cacheGroup;
}
@Override
protected Query createReplacementQuery(EntityResolver resolver) {
SelectQuery<?> replacement = (SelectQuery)super.createReplacementQuery(resolver);
replacement.setColumns(columns);
replacement.setHavingQualifier(having);
replacement.setCanReturnScalarValue(singleColumn);
replacement.setDistinct(distinct);
replacement.setSuppressDistinct(suppressDistinct);
return replacement;
}
/**
* Sets the type of the entity to fetch without changing the return type of
* the query.
*
* @return this object
*/
public ColumnSelect<T> entityType(Class<?> entityType) {
return resetEntity(entityType, null, null);
}
/**
* Sets the {@link ObjEntity} name to fetch without changing the return type
* of the query. This form is most often used for generic entities that
* don't map to a distinct class.
*
* @return this object
*/
public ColumnSelect<T> entityName(String entityName) {
return resetEntity(null, entityName, null);
}
/**
* Sets the {@link DbEntity} name to fetch without changing the return type
* of the query. This form is most often used for generic entities that
* don't map to a distinct class.
*
* @return this object
*/
public ColumnSelect<T> dbEntityName(String dbEntityName) {
return resetEntity(null, null, dbEntityName);
}
@SuppressWarnings("unchecked")
private ColumnSelect<T> resetEntity(Class<?> entityType, String entityName, String dbEntityName) {
this.entityType = entityType;
this.entityName = entityName;
this.dbEntityName = dbEntityName;
return (ColumnSelect<T>)this;
}
/**
* Appends a qualifier expression of this query. An equivalent to
* {@link #and(Expression...)} that can be used a syntactic sugar.
*
* @return this object
*/
public ColumnSelect<T> where(Expression expression) {
return and(expression);
}
/**
* Appends a qualifier expression of this query, using provided expression
* String and an array of position parameters. This is an equivalent to
* calling "and".
*
* @return this object
*/
public ColumnSelect<T> where(String expressionString, Object... parameters) {
return and(ExpressionFactory.exp(expressionString, parameters));
}
/**
* AND's provided expressions to the existing WHERE clause expression.
*
* @return this object
*/
public ColumnSelect<T> and(Expression... expressions) {
if (expressions == null || expressions.length == 0) {
return this;
}
return and(Arrays.asList(expressions));
}
/**
* OR's provided expressions to the existing WHERE clause expression.
*
* @return this object
*/
@SuppressWarnings("unchecked")
public ColumnSelect<T> or(Expression... expressions) {
if (expressions == null || expressions.length == 0) {
return this;
}
return or(Arrays.asList(expressions));
}
/**
* Add an ascending ordering on the given property. If there is already an ordering
* on this query then add this ordering with a lower priority.
*
* @param property the property to sort on
* @return this object
*/
public ColumnSelect<T> orderBy(String property) {
return orderBy(new Ordering(property));
}
/**
* Add an ordering on the given property. If there is already an ordering
* on this query then add this ordering with a lower priority.
*
* @param property the property to sort on
* @param sortOrder the direction of the ordering
* @return this object
*/
public ColumnSelect<T> orderBy(String property, SortOrder sortOrder) {
return orderBy(new Ordering(property, sortOrder));
}
/**
* Add one or more orderings to this query.
*
* @return this object
*/
public ColumnSelect<T> orderBy(Ordering... orderings) {
if (orderings == null) {
return this;
}
if (this.orderings == null) {
this.orderings = new ArrayList<>(orderings.length);
}
Collections.addAll(this.orderings, orderings);
return this;
}
/**
* Adds a list of orderings to this query.
*
* @return this object
*/
public ColumnSelect<T> orderBy(Collection<Ordering> orderings) {
if (orderings == null) {
return this;
}
if (this.orderings == null) {
this.orderings = new ArrayList<>(orderings.size());
}
this.orderings.addAll(orderings);
return this;
}
/**
* Merges prefetch into the query prefetch tree.
*
* @return this object
*/
public ColumnSelect<T> prefetch(PrefetchTreeNode prefetch) {
if (prefetch == null) {
return this;
}
if (prefetches == null) {
prefetches = new PrefetchTreeNode();
}
prefetches.merge(prefetch);
return this;
}
/**
* Merges a prefetch path with specified semantics into the query prefetch
* tree.
*
* @return this object
*/
public ColumnSelect<T> prefetch(String path, int semantics) {
if (path == null) {
return this;
}
if (prefetches == null) {
prefetches = new PrefetchTreeNode();
}
prefetches.addPath(path).setSemantics(semantics);
return this;
}
/**
* Resets query fetch limit - a parameter that defines max number of objects
* that should be ever be fetched from the database.
*/
public ColumnSelect<T> limit(int fetchLimit) {
if (this.limit != fetchLimit) {
this.limit = fetchLimit;
this.replacementQuery = null;
}
return this;
}
/**
* Resets query fetch offset - a parameter that defines how many objects
* should be skipped when reading data from the database.
*/
public ColumnSelect<T> offset(int fetchOffset) {
if (this.offset != fetchOffset) {
this.offset = fetchOffset;
this.replacementQuery = null;
}
return this;
}
/**
* Resets query page size. A non-negative page size enables query result
* pagination that saves memory and processing time for large lists if only
* parts of the result are ever going to be accessed.
*/
public ColumnSelect<T> pageSize(int pageSize) {
if (this.pageSize != pageSize) {
this.pageSize = pageSize;
this.replacementQuery = null;
}
return this;
}
/**
* Sets fetch size of the PreparedStatement generated for this query. Only
* non-negative values would change the default size.
*
* @see Statement#setFetchSize(int)
*/
public ColumnSelect<T> statementFetchSize(int size) {
if (this.statementFetchSize != size) {
this.statementFetchSize = size;
this.replacementQuery = null;
}
return this;
}
public ColumnSelect<T> cacheStrategy(QueryCacheStrategy strategy) {
if (this.cacheStrategy != strategy) {
this.cacheStrategy = strategy;
this.replacementQuery = null;
}
if(this.cacheGroup != null) {
this.cacheGroup = null;
this.replacementQuery = null;
}
return this;
}
public ColumnSelect<T> cacheStrategy(QueryCacheStrategy strategy, String cacheGroup) {
return cacheStrategy(strategy).cacheGroup(cacheGroup);
}
public ColumnSelect<T> cacheGroup(String cacheGroup) {
this.cacheGroup = cacheGroup;
this.replacementQuery = null;
return this;
}
/**
* Instructs Cayenne to look for query results in the "local" cache when
* running the query. This is a short-hand notation for:
* <p>
* <pre>
* query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroup);
* </pre>
*/
public ColumnSelect<T> localCache(String cacheGroup) {
return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroup);
}
/**
* Instructs Cayenne to look for query results in the "local" cache when
* running the query. This is a short-hand notation for:
* <p>
* <pre>
* query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE);
* </pre>
*/
public ColumnSelect<T> localCache() {
return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE);
}
/**
* Instructs Cayenne to look for query results in the "shared" cache when
* running the query. This is a short-hand notation for:
* <p>
* <pre>
* query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroup);
* </pre>
*/
public ColumnSelect<T> sharedCache(String cacheGroup) {
return cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroup);
}
/**
* Instructs Cayenne to look for query results in the "shared" cache when
* running the query. This is a short-hand notation for:
* <p>
* <pre>
* query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE);
* </pre>
*/
public ColumnSelect<T> sharedCache() {
return cacheStrategy(QueryCacheStrategy.SHARED_CACHE);
}
/**
* <p>Add properties to select.</p>
* <p>Can be any properties that can be resolved against root entity type
* (root entity properties, function call expressions, properties of relationships, etc).</p>
* <p>
* <pre>
* {@code
* List<Object[]> columns = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME)
* .columns(Artist.ARTIST_SALARY, Artist.DATE_OF_BIRTH)
* .select(context);
* }
* </pre>
*
* @param firstProperty first property
* @param otherProperties array of properties to select
* @see ColumnSelect#column(Property)
* @see ColumnSelect#columns(Collection)
*/
@SuppressWarnings("unchecked")
public ColumnSelect<Object[]> columns(Property<?> firstProperty, Property<?>... otherProperties) {
if (columns == null) {
columns = new ArrayList<>(otherProperties.length + 1);
}
columns.add(firstProperty);
Collections.addAll(columns, otherProperties);
singleColumn = false;
return (ColumnSelect<Object[]>)this;
}
/**
* <p>Add properties to select.</p>
* <p>Can be any properties that can be resolved against root entity type
* (root entity properties, function call expressions, properties of relationships, etc).</p>
* <p>
* @param properties collection of properties, <b>must</b> contain at least one element
* @see ColumnSelect#columns(Property, Property[])
*/
@SuppressWarnings("unchecked")
public ColumnSelect<Object[]> columns(Collection<Property<?>> properties) {
if (properties == null){
throw new NullPointerException("properties is null");
}
if (properties.isEmpty()) {
throw new IllegalArgumentException("properties must contain at least one element");
}
if (this.columns == null) {
this.columns = new ArrayList<>(properties.size());
}
columns.addAll(properties);
singleColumn = false;
return (ColumnSelect<Object[]>)this;
}
@SuppressWarnings("unchecked")
protected <E> ColumnSelect<E> column(Property<E> property) {
if (this.columns == null) {
this.columns = new ArrayList<>(1);
} else {
this.columns.clear(); // if we don't clear then return type will be incorrect
}
this.columns.add(property);
return (ColumnSelect<E>) this;
}
/**
* <p>Shortcut for {@link #columns(Property, Property[])} columns}(Property.COUNT)</p>
*/
public ColumnSelect<Object[]> count() {
return columns(Property.COUNT);
}
/**
* <p>Select COUNT(property)</p>
* <p>Can return different result than COUNT(*) as it will count only non null values</p>
* @see ColumnSelect#count()
*/
public ColumnSelect<Object[]> count(Property<?> property) {
return columns(property.count());
}
/**
* <p>Select minimum value of property</p>
* @see ColumnSelect#columns(Property, Property[])
*/
public ColumnSelect<Object[]> min(Property<?> property) {
return columns(property.min());
}
/**
* <p>Select maximum value of property</p>
* @see ColumnSelect#columns(Property, Property[])
*/
public ColumnSelect<Object[]> max(Property<?> property) {
return columns(property.max());
}
/**
* <p>Select average value of property</p>
* @see ColumnSelect#columns(Property, Property[])
*/
public ColumnSelect<Object[]> avg(Property<?> property) {
return columns(property.avg());
}
/**
* <p>Select sum of values</p>
* @see ColumnSelect#columns(Property, Property[])
*/
public <E extends Number> ColumnSelect<Object[]> sum(Property<E> property) {
return columns(property.sum());
}
/**
* Appends a having qualifier expression of this query. An equivalent to
* {@link #and(Expression...)} that can be used a syntactic sugar.
*
* @return this object
*/
public ColumnSelect<T> having(Expression expression) {
havingExpressionIsActive = true;
return and(expression);
}
/**
* Appends a having qualifier expression of this query, using provided expression
* String and an array of position parameters. This is an equivalent to
* calling "and".
*
* @return this object
*/
public ColumnSelect<T> having(String expressionString, Object... parameters) {
havingExpressionIsActive = true;
return and(ExpressionFactory.exp(expressionString, parameters));
}
/**
* AND's provided expressions to the existing WHERE or HAVING clause expression.
*
* @return this object
*/
public ColumnSelect<T> and(Collection<Expression> expressions) {
if (expressions == null || expressions.isEmpty()) {
return this;
}
Collection<Expression> all;
Expression activeExpression = getActiveExpression();
if (activeExpression != null) {
all = new ArrayList<>(expressions.size() + 1);
all.add(activeExpression);
all.addAll(expressions);
} else {
all = expressions;
}
setActiveExpression(ExpressionFactory.and(all));
return this;
}
/**
* OR's provided expressions to the existing WHERE or HAVING clause expression.
*
* @return this object
*/
public ColumnSelect<T> or(Collection<Expression> expressions) {
if (expressions == null || expressions.isEmpty()) {
return this;
}
Collection<Expression> all;
Expression activeExpression = getActiveExpression();
if (activeExpression != null) {
all = new ArrayList<>(expressions.size() + 1);
all.add(activeExpression);
all.addAll(expressions);
} else {
all = expressions;
}
setActiveExpression(ExpressionFactory.or(all));
return this;
}
/**
* Explicitly request distinct in query.
*/
public ColumnSelect<T> distinct() {
this.suppressDistinct = false;
this.distinct = true;
return this;
}
/**
* Explicitly suppress distinct in query.
*/
public ColumnSelect<T> suppressDistinct() {
this.suppressDistinct = true;
this.distinct = false;
return this;
}
private void setActiveExpression(Expression exp) {
if(havingExpressionIsActive) {
having = exp;
} else {
where = exp;
}
}
private Expression getActiveExpression() {
if(havingExpressionIsActive) {
return having;
} else {
return where;
}
}
public Collection<Property<?>> getColumns() {
return columns;
}
/**
* Returns a HAVING clause Expression of this query.
*/
public Expression getHaving() {
return having;
}
@Override
public T selectFirst(ObjectContext context) {
return context.selectFirst(limit(1));
}
}