/***************************************************************** * 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 org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.QueryResponse; import org.apache.cayenne.QueryResult; import org.apache.cayenne.map.DataMap; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.util.QueryResultBuilder; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A generic query based on raw SQL and featuring fluent API. While * {@link SQLExec} can be used to select data (see {@link #execute(ObjectContext)} * ), it is normally used for updates, DDL operations, etc. * * @since 4.0 */ public class SQLExec extends IndirectQuery { private static final long serialVersionUID = -6533566707148045615L; /** * Creates a query executing provided SQL run against default database. */ public static SQLExec query(String sql) { return new SQLExec(sql); } /** * Creates a query executing provided SQL that performs routing based on the * provided DataMap name. */ public static SQLExec query(String dataMapName, String sql) { SQLExec query = new SQLExec(sql); query.dataMapName = dataMapName; return query; } protected String dataMapName; protected StringBuilder sqlBuffer; protected Map<String, Object> params; protected List<Object> positionalParams; public SQLExec(String sql) { this.sqlBuffer = sql != null ? new StringBuilder(sql) : new StringBuilder(); } 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 SQLExec append(String sqlChunk) { sqlBuffer.append(sqlChunk); this.replacementQuery = null; return this; } public SQLExec params(String name, Object value) { params(Collections.singletonMap(name, value)); return this; } @SuppressWarnings({"rawtypes", "unchecked"}) public SQLExec params(Map<String, ?> parameters) { if (this.params == null) { this.params = new HashMap<>(parameters); } else { Map bareMap = parameters; this.params.putAll(bareMap); } 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 SQLExec 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 SQLExec paramsList(List<Object> params) { // since named parameters are specified, resetting positional parameters this.params = null; this.positionalParams = params; return this; } /** * Returns a potentially immutable 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 immutable list of positional parameters that will * be bound to SQL. */ public List<Object> getPositionalParams() { return positionalParams != null ? positionalParams : Collections.emptyList(); } public QueryResult execute(ObjectContext context) { // TODO: switch ObjectContext to QueryResult instead of QueryResponse // and create its own 'exec' method QueryResponse response = context.performGenericQuery(this); QueryResultBuilder builder = QueryResultBuilder.builder(response.size()); for (response.reset(); response.next(); ) { if (response.isList()) { builder.addSelectResult(response.currentList()); } else { builder.addBatchUpdateResult(response.currentUpdateCount()); } } return builder.build(); } public int update(ObjectContext context) { // TODO: create a corresponding method in ObjectContext QueryResult results = execute(context); if (results.size() != 1) { throw new CayenneRuntimeException("Expected a single update result. Got a total of %d", results.size()); } return results.firstUpdateCount(); } public int[] updateBatch(ObjectContext context) { // TODO: create a corresponding method in ObjectContext QueryResult results = execute(context); if (results.size() != 1) { throw new CayenneRuntimeException("Expected a single update result. Got a total of %d", results.size()); } return results.firstBatchUpdateCount(); } @Override protected Query createReplacementQuery(EntityResolver resolver) { Object root; 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.setRoot(root); template.setDefaultTemplate(getSql()); if (positionalParams != null) { template.setParamsList(positionalParams); } else { template.setParams(params); } return template; } }