/*****************************************************************
* 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.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.ResultBatchIterator;
import org.apache.cayenne.ResultIterator;
import org.apache.cayenne.ResultIteratorCallback;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.SQLResult;
/**
* A selecting query based on raw SQL and featuring fluent API.
*
* @since 4.0
*/
public class SQLSelect<T> extends IndirectQuery implements Select<T> {
private static final long serialVersionUID = -7074293371883740872L;
/**
* Creates a query that selects DataRows and uses default routing.
*/
public static SQLSelect<DataRow> dataRowQuery(String sql) {
return new SQLSelect<>(sql);
}
/**
* Creates a query that selects DataRows and uses routing based on the
* provided DataMap name.
*/
public static SQLSelect<DataRow> dataRowQuery(String dataMapName, String sql) {
SQLSelect<DataRow> query = new SQLSelect<>(sql);
query.dataMapName = dataMapName;
return query;
}
/**
* Creates a query that selects DataObjects.
*/
public static <T> SQLSelect<T> query(Class<T> type, String sql) {
return new SQLSelect<>(type, sql);
}
/**
* Creates a query that selects scalar values and uses default routing.
*/
public static <T> SQLSelect<T> scalarQuery(Class<T> type, String sql) {
SQLSelect<T> query = new SQLSelect<>(sql);
query.scalarType = type;
return query;
}
/**
* Creates a query that selects scalar values and uses routing based on the
* provided DataMap name.
*/
public static <T> SQLSelect<T> scalarQuery(Class<T> type, String dataMapName, String sql) {
SQLSelect<T> query = new SQLSelect<>(sql);
query.dataMapName = dataMapName;
query.scalarType = type;
return query;
}
protected Class<T> persistentType;
protected Class<T> scalarType;
protected String dataMapName;
protected StringBuilder sqlBuffer;
protected QueryCacheStrategy cacheStrategy;
protected String cacheGroup;
protected Map<String, Object> params;
protected List<Object> positionalParams;
protected CapsStrategy columnNameCaps;
protected int limit;
protected int offset;
protected int pageSize;
protected int statementFetchSize;
public SQLSelect(String sql) {
this(null, sql);
}
public SQLSelect(Class<T> persistentType, String sql) {
this.persistentType = persistentType;
this.sqlBuffer = sql != null ? new StringBuilder(sql) : new StringBuilder();
this.limit = QueryMetadata.FETCH_LIMIT_DEFAULT;
this.offset = QueryMetadata.FETCH_OFFSET_DEFAULT;
this.pageSize = QueryMetadata.PAGE_SIZE_DEFAULT;
}
@Override
public List<T> select(ObjectContext context) {
return context.select(this);
}
@Override
public T selectOne(ObjectContext context) {
return context.selectOne(this);
}
@Override
public T selectFirst(ObjectContext context) {
return context.selectFirst(limit(1));
}
@Override
public void iterate(ObjectContext context, ResultIteratorCallback<T> callback) {
context.iterate(this, callback);
}
@Override
public ResultIterator<T> iterator(ObjectContext context) {
return context.iterator(this);
}
@Override
public ResultBatchIterator<T> batchIterator(ObjectContext context, int size) {
return context.batchIterator(this, size);
}
public boolean isFetchingDataRows() {
return persistentType == null;
}
public boolean isFetchingScalars() {
return scalarType != null;
}
public String getSql() {
String sql = sqlBuffer.toString();
return sql.length() > 0 ? sql : null;
}
/**
* Appends a piece of SQL to the previously stored SQL template.
*/
public SQLSelect<T> append(String sqlChunk) {
sqlBuffer.append(sqlChunk);
this.replacementQuery = null;
return this;
}
public SQLSelect<T> params(String name, Object value) {
params(Collections.singletonMap(name, value));
return this;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public SQLSelect<T> params(Map<String, ?> parameters) {
if (this.params == null) {
this.params = new HashMap<>(parameters);
} else {
this.params.putAll(parameters);
}
this.replacementQuery = null;
// since named parameters are specified, resetting positional
// parameters
this.positionalParams = null;
return this;
}
/**
* Initializes positional parameters of the query. Parameters are bound in
* the order they are found in the SQL template. If a given parameter name
* is used more than once, only the first occurrence is treated as
* "position", subsequent occurrences are bound with the same value as the
* first one. If template parameters count is different from the array
* parameter count, an exception will be thrown.
* <p>
* Note that calling this method will reset any previously set *named*
* parameters.
*/
public SQLSelect<T> paramsArray(Object... params) {
return paramsList(params != null ? Arrays.asList(params) : null);
}
/**
* Initializes positional parameters of the query. Parameters are bound in
* the order they are found in the SQL template. If a given parameter name
* is used more than once, only the first occurrence is treated as
* "position", subsequent occurrences are bound with the same value as the
* first one. If template parameters count is different from the list
* parameter count, an exception will be thrown.
* <p>
* Note that calling this method will reset any previously set *named*
* parameters.
*/
public SQLSelect<T> paramsList(List<Object> params) {
// since named parameters are specified, resetting positional
// parameters
this.params = null;
this.positionalParams = params;
return this;
}
/**
* Returns a potentially immmutable map of named parameters that will be
* bound to SQL.
*/
public Map<String, Object> getParams() {
return params != null ? params : Collections.<String, Object> emptyMap();
}
/**
* Returns a potentially immmutable list of positional parameters that will
* be bound to SQL.
*/
public List<Object> getPositionalParams() {
return positionalParams != null ? positionalParams : Collections.emptyList();
}
@Override
protected Query createReplacementQuery(EntityResolver resolver) {
Object root;
if (persistentType != null) {
root = persistentType;
} else if (dataMapName != null) {
DataMap map = resolver.getDataMap(dataMapName);
if (map == null) {
throw new CayenneRuntimeException("Invalid dataMapName '%s'", dataMapName);
}
root = map;
} else {
// will route via default node. TODO: allow explicit node name?
root = null;
}
SQLTemplate template = new SQLTemplate();
template.setFetchingDataRows(isFetchingDataRows());
template.setRoot(root);
template.setDefaultTemplate(getSql());
template.setCacheGroup(cacheGroup);
template.setCacheStrategy(cacheStrategy);
if (positionalParams != null) {
template.setParamsList(positionalParams);
} else {
template.setParams(params);
}
template.setColumnNamesCapitalization(columnNameCaps);
template.setFetchLimit(limit);
template.setFetchOffset(offset);
template.setPageSize(pageSize);
template.setStatementFetchSize(statementFetchSize);
if (isFetchingScalars()) {
SQLResult resultMap = new SQLResult();
resultMap.addColumnResult("x");
template.setResult(resultMap);
}
return template;
}
/**
* Instructs Cayenne to look for query results in the "local" cache when
* running the query. This is a short-hand notation for:
*
* <pre>
* query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE);
* </pre>
*/
public SQLSelect<T> localCache() {
return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE);
}
/**
* Instructs Cayenne to look for query results in the "local" cache when
* running the query. This is a short-hand notation for:
*
* <pre>
* query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroup);
* </pre>
*/
public SQLSelect<T> localCache(String cacheGroup) {
return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroup);
}
/**
* Instructs Cayenne to look for query results in the "shared" cache when
* running the query. This is a short-hand notation for:
*
* <pre>
* query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE);
* </pre>
*/
public SQLSelect<T> sharedCache() {
return cacheStrategy(QueryCacheStrategy.SHARED_CACHE);
}
/**
* Instructs Cayenne to look for query results in the "shared" cache when
* running the query. This is a short-hand notation for:
*
* <pre>
* query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroup);
* </pre>
*/
public SQLSelect<T> sharedCache(String cacheGroup) {
return cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroup);
}
public QueryCacheStrategy getCacheStrategy() {
return cacheStrategy;
}
public SQLSelect<T> cacheStrategy(QueryCacheStrategy strategy) {
if(cacheStrategy != strategy) {
cacheStrategy = strategy;
replacementQuery = null;
}
if(cacheGroup != null) {
cacheGroup = null;
replacementQuery = null;
}
return this;
}
public SQLSelect<T> cacheStrategy(QueryCacheStrategy strategy, String cacheGroup) {
return cacheStrategy(strategy).cacheGroup(cacheGroup);
}
public String getCacheGroup() {
return cacheGroup;
}
public SQLSelect<T> cacheGroup(String cacheGroup) {
this.cacheGroup = cacheGroup;
this.replacementQuery = null;
return this;
}
/**
* Returns a column name capitalization policy applied to selecting queries.
* This is used to simplify mapping of the queries like "SELECT * FROM ...",
* ensuring that a chosen Cayenne column mapping strategy (e.g. all column
* names in uppercase) is portable across database engines that can have
* varying default capitalization. Default (null) value indicates that
* column names provided in result set are used unchanged.
*/
public CapsStrategy getColumnNameCaps() {
return columnNameCaps;
}
/**
* Sets a column name capitalization policy applied to selecting queries.
* This is used to simplify mapping of the queries like "SELECT * FROM ...",
* ensuring that a chosen Cayenne column mapping strategy (e.g. all column
* names in uppercase) is portable across database engines that can have
* varying default capitalization. Default (null) value indicates that
* column names provided in result set are used unchanged.
* <p>
* Note that while a non-default setting is useful for queries that do not
* rely on a #result directive to describe columns, it works for all
* SQLTemplates the same way.
*/
public SQLSelect<T> columnNameCaps(CapsStrategy columnNameCaps) {
if (this.columnNameCaps != columnNameCaps) {
this.columnNameCaps = columnNameCaps;
this.replacementQuery = null;
}
return this;
}
/**
* Equivalent of setting {@link CapsStrategy#UPPER}
*/
public SQLSelect<T> upperColumnNames() {
return columnNameCaps(CapsStrategy.UPPER);
}
/**
* Equivalent of setting {@link CapsStrategy#LOWER}
*/
public SQLSelect<T> lowerColumnNames() {
return columnNameCaps(CapsStrategy.LOWER);
}
public int getLimit() {
return limit;
}
public SQLSelect<T> limit(int fetchLimit) {
if (this.limit != fetchLimit) {
this.limit = fetchLimit;
this.replacementQuery = null;
}
return this;
}
public int getOffset() {
return offset;
}
public SQLSelect<T> offset(int fetchOffset) {
if (this.offset != fetchOffset) {
this.offset = fetchOffset;
this.replacementQuery = null;
}
return this;
}
public int getPageSize() {
return pageSize;
}
public SQLSelect<T> pageSize(int pageSize) {
if (this.pageSize != pageSize) {
this.pageSize = pageSize;
this.replacementQuery = null;
}
return this;
}
/**
* Sets JDBC statement's fetch size (0 for no default size)
*/
public SQLSelect<T> statementFetchSize(int size) {
if (this.statementFetchSize != size) {
this.statementFetchSize = size;
this.replacementQuery = null;
}
return this;
}
/**
* @return JBDC statement's fetch size
*/
public int getStatementFetchSize() {
return statementFetchSize;
}
}