blob: 118cdd60e87e511d8ab2e6896001628a51f75923 [file] [log] [blame]
/*****************************************************************
* 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
*
* https://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.access.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ResultIterator;
import org.apache.cayenne.access.DataNode;
import org.apache.cayenne.access.OperationObserver;
import org.apache.cayenne.access.jdbc.reader.RowReader;
import org.apache.cayenne.access.translator.ParameterBinding;
import org.apache.cayenne.access.types.ExtendedType;
import org.apache.cayenne.access.types.ExtendedTypeMap;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DefaultScalarResultSegment;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.query.QueryMetadata;
import org.apache.cayenne.query.SQLAction;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.util.Util;
/**
* Implements a strategy for execution of SQLTemplates.
*
* @since 1.2 replaces SQLTemplateExecutionPlan
*/
public class SQLTemplateAction implements SQLAction {
protected SQLTemplate query;
protected QueryMetadata queryMetadata;
protected DbEntity dbEntity;
protected DataNode dataNode;
protected DbAdapter dbAdapter;
/**
* @since 4.0
*/
public SQLTemplateAction(SQLTemplate query, DataNode dataNode) {
this.query = query;
this.dataNode = dataNode;
this.queryMetadata = query.getMetaData(dataNode.getEntityResolver());
this.dbEntity = queryMetadata.getDbEntity();
// using unwrapped adapter to check for the right SQL flavor...
this.dbAdapter = dataNode.getAdapter().unwrap();
}
/**
* Returns unwrapped DbAdapter used to find correct SQL for a given DB.
*/
public DbAdapter getAdapter() {
return dbAdapter;
}
/**
* Runs a SQLTemplate query, collecting all results. If a callback expects
* an iterated result, result processing is stopped after the first
* ResultSet is encountered.
*/
@Override
public void performAction(Connection connection, OperationObserver callback) throws SQLException, Exception {
String template = extractTemplateString();
// sanity check - misconfigured templates
if (template == null) {
throw new CayenneRuntimeException("No template string configured for adapter " + dbAdapter.getClass().getName());
}
boolean loggable = dataNode.getJdbcEventLogger().isLoggable();
List<Number> counts = new ArrayList<>();
// bind either positional or named parameters;
// for legacy reasons named parameters are processed as a batch.. this
// should go away after 4.0; newer positional parameter only support a
// single set of values.
if (query.getPositionalParams().isEmpty()) {
runWithNamedParametersBatch(connection, callback, template, counts, loggable);
} else {
runWithPositionalParameters(connection, callback, template, counts, loggable);
}
// notify of combined counts of all queries inside SQLTemplate
// multiplied by the number of parameter sets...
int[] ints = new int[counts.size()];
for (int i = 0; i < ints.length; i++) {
ints[i] = counts.get(i).intValue();
}
callback.nextBatchCount(query, ints);
}
private void bindExtendedTypes(ParameterBinding[] bindings) {
int i = 1;
for (ParameterBinding binding : bindings) {
Object value = binding.getValue();
ExtendedType extendedType = value != null
? getAdapter().getExtendedTypes().getRegisteredType(value.getClass())
: getAdapter().getExtendedTypes().getDefaultType();
binding.setExtendedType(extendedType);
binding.setStatementPosition(i++);
}
}
private void runWithPositionalParameters(Connection connection, OperationObserver callback, String template,
Collection<Number> counts, boolean loggable) throws Exception {
SQLStatement compiled = dataNode.getSqlTemplateProcessor().processTemplate(template,
query.getPositionalParams());
bindExtendedTypes(compiled.getBindings());
if (loggable) {
dataNode.getJdbcEventLogger().logQuery(compiled.getSql(), compiled.getBindings());
}
execute(connection, callback, compiled, counts);
}
@SuppressWarnings("unchecked")
private void runWithNamedParametersBatch(Connection connection, OperationObserver callback, String template,
Collection<Number> counts, boolean loggable) throws Exception {
int size = query.parametersSize();
// zero size indicates a one-shot query with no parameters
// so fake a single entry batch...
int batchSize = (size > 0) ? size : 1;
// for now supporting deprecated batch parameters...
Iterator<Map<String, ?>> it;
if(size == 0) {
Iterator empty = Collections.singleton(Collections.emptyMap()).iterator();
it = empty;
} else {
it = query.parametersIterator();
}
for (int i = 0; i < batchSize; i++) {
Map<String, ?> nextParameters = it.next();
SQLStatement compiled = dataNode.getSqlTemplateProcessor().processTemplate(template, nextParameters);
bindExtendedTypes(compiled.getBindings());
if (loggable) {
dataNode.getJdbcEventLogger().logQuery(compiled.getSql(), compiled.getBindings());
}
execute(connection, callback, compiled, counts);
}
}
protected void execute(Connection connection, OperationObserver callback, SQLStatement compiled,
Collection<Number> updateCounts) throws SQLException, Exception {
long t1 = System.currentTimeMillis();
boolean iteratedResult = callback.isIteratedResult();
int generatedKeys = query.isReturnGeneratedKeys() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS;
PreparedStatement statement = connection.prepareStatement(compiled.getSql(), generatedKeys);
try {
bind(statement, compiled.getBindings());
// process a mix of results
boolean isResultSet = statement.execute();
if(query.isReturnGeneratedKeys()) {
ResultSet generatedKeysResultSet = statement.getGeneratedKeys();
if (generatedKeysResultSet != null) {
processSelectResult(compiled, connection, statement, generatedKeysResultSet, callback, t1);
}
}
boolean firstIteration = true;
while (true) {
if (firstIteration) {
firstIteration = false;
} else {
isResultSet = statement.getMoreResults();
}
if (isResultSet) {
ResultSet resultSet = statement.getResultSet();
if (resultSet != null) {
try {
processSelectResult(compiled, connection, statement, resultSet, callback, t1);
} finally {
if (!iteratedResult) {
resultSet.close();
}
}
// ignore possible following update counts and bail early on iterated results
if (iteratedResult) {
break;
}
}
} else {
int updateCount = statement.getUpdateCount();
if (updateCount == -1) {
break;
}
updateCounts.add(updateCount);
dataNode.getJdbcEventLogger().logUpdateCount(updateCount);
}
}
} finally {
if (!iteratedResult) {
statement.close();
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected void processSelectResult(SQLStatement compiled, Connection connection, Statement statement,
ResultSet resultSet, OperationObserver callback, final long startTime) throws Exception {
boolean iteratedResult = callback.isIteratedResult();
ExtendedTypeMap types = dataNode.getAdapter().getExtendedTypes();
RowDescriptorBuilder builder = configureRowDescriptorBuilder(compiled, resultSet);
recreateQueryMetadata(resultSet);
RowReader<?> rowReader = dataNode.rowReader(builder.getDescriptor(types), queryMetadata);
ResultIterator<?> it = new JDBCResultIterator<>(statement, resultSet, rowReader);
if (iteratedResult) {
it = new ConnectionAwareResultIterator(it, connection) {
@Override
protected void doClose() {
dataNode.getJdbcEventLogger().logSelectCount(rowCounter, System.currentTimeMillis() - startTime);
super.doClose();
}
};
}
it = new LimitResultIterator<>(it, getFetchOffset(), query.getFetchLimit());
if (iteratedResult) {
try {
callback.nextRows(query, it);
} catch (Exception ex) {
it.close();
throw ex;
}
} else {
// note that we are not closing the iterator here, relying on caller
// to close the underlying ResultSet on its own... this is a hack,
// maybe a cleaner flow is due here.
List<?> resultRows = it.allRows();
dataNode.getJdbcEventLogger().logSelectCount(resultRows.size(), System.currentTimeMillis() - startTime);
callback.nextRows(query, resultRows);
}
}
private void recreateQueryMetadata(ResultSet resultSet) throws SQLException {
if(query.isUseScalar() && queryMetadata.getResultSetMapping() != null && queryMetadata.getResultSetMapping().isEmpty()){
for(int i = 0; i < resultSet.getMetaData().getColumnCount(); i++) {
queryMetadata.getResultSetMapping().add(new DefaultScalarResultSegment(String.valueOf(i), i));
}
}
}
/**
* Creates column descriptors based on compiled statement and query metadata
*/
private ColumnDescriptor[] createColumnDescriptors(SQLStatement compiled) {
// SQLTemplate #result columns take precedence over other ways to determine the type
if (compiled.getResultColumns().length > 0) {
if(query.getResultColumnsTypes() != null) {
throw new CayenneRuntimeException("Caused by setting return types by directives and by parameters in query.");
} else {
return compiled.getResultColumns();
}
}
// check explicitly set column types
if(query.getResultColumnsTypes() == null) {
return null;
}
int size = query.getResultColumnsTypes().size();
ColumnDescriptor[] columnDescriptors = new ColumnDescriptor[size];
for(int i = 0; i < size; i++) {
ColumnDescriptor columnDescriptor = new ColumnDescriptor();
columnDescriptor.setJavaClass(query.getResultColumnsTypes().get(i).getCanonicalName());
columnDescriptors[i] = columnDescriptor;
}
return columnDescriptors;
}
/**
* @since 3.0
*/
protected RowDescriptorBuilder configureRowDescriptorBuilder(SQLStatement compiled, ResultSet resultSet)
throws SQLException {
RowDescriptorBuilder builder = new RowDescriptorBuilder()
.setResultSet(resultSet)
.setColumns(createColumnDescriptors(compiled))
.validateDuplicateColumnNames();
if(query.getResultColumnsTypes() != null) {
builder.mergeColumnsWithRsMetadata();
}
ObjEntity entity = queryMetadata.getObjEntity();
if (entity != null && isResultColumnTypesEmpty()) {
// TODO: andrus 2008/03/28 support flattened attributes with aliases...
for (ObjAttribute attribute : entity.getAttributes()) {
String column = attribute.getDbAttributePath();
if (column == null || column.indexOf('.') > 0) {
continue;
}
builder.overrideColumnType(column, attribute.getType());
}
}
// override numeric Java types based on JDBC defaults for DbAttributes, as Oracle
// ResultSetMetadata is not very precise about NUMERIC distinctions...
// (BigDecimal vs Long vs. Integer)
if (dbEntity != null && isResultColumnTypesEmpty()) {
for (DbAttribute attribute : dbEntity.getAttributes()) {
if (!builder.isOverriden(attribute.getName()) && TypesMapping.isNumeric(attribute.getType())) {
builder.overrideColumnType(attribute.getName(), TypesMapping.getJavaBySqlType(attribute.getType()));
}
}
}
switch (query.getColumnNamesCapitalization()) {
case LOWER:
builder.useLowercaseColumnNames();
break;
case UPPER:
builder.useUppercaseColumnNames();
break;
}
return builder;
}
private boolean isResultColumnTypesEmpty(){
return query.getResultColumnsTypes() == null || query.getResultColumnsTypes().isEmpty();
}
/**
* Extracts a template string from a SQLTemplate query. Exists mainly for
* the benefit of subclasses that can customize returned template.
*
* @since 1.2
*/
protected String extractTemplateString() {
String sql = query.getTemplate(dbAdapter.getClass().getName());
// note that we MUST convert line breaks to spaces. On some databases (DB2)
// queries with breaks simply won't run; the rest are affected by CAY-726.
return Util.stripLineBreaks(sql, ' ');
}
/**
* Binds parameters to the PreparedStatement.
*/
protected void bind(PreparedStatement preparedStatement, ParameterBinding[] bindings)
throws SQLException, Exception {
// bind parameters
for (ParameterBinding binding : bindings) {
dataNode.getAdapter().bindParameter(preparedStatement, binding);
}
if (queryMetadata.getStatementFetchSize() != 0) {
preparedStatement.setFetchSize(queryMetadata.getStatementFetchSize());
}
int queryTimeout = queryMetadata.getQueryTimeout();
if(queryTimeout != QueryMetadata.QUERY_TIMEOUT_DEFAULT) {
preparedStatement.setQueryTimeout(queryTimeout);
}
}
/**
* Returns a SQLTemplate for this action.
*/
public SQLTemplate getQuery() {
return query;
}
/**
* @since 3.0
*/
protected int getFetchOffset() {
return query.getFetchOffset();
}
}