You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by dk...@apache.org on 2016/01/22 17:08:25 UTC
cayenne git commit: CAY-2053 SQLExec fluent query API
Repository: cayenne
Updated Branches:
refs/heads/master aeb0a91a3 -> 763a42f66
CAY-2053 SQLExec fluent query API
Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/763a42f6
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/763a42f6
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/763a42f6
Branch: refs/heads/master
Commit: 763a42f6631c2a09aafc81337e259249176c319c
Parents: aeb0a91
Author: aadamchik <aa...@apache.org>
Authored: Sun Jan 25 05:57:55 2015 -0500
Committer: Dzmitry Kazimirchyk <dk...@gmail.com>
Committed: Fri Jan 22 19:01:35 2016 +0300
----------------------------------------------------------------------
.../java/org/apache/cayenne/QueryResult.java | 56 +++++
.../java/org/apache/cayenne/query/SQLExec.java | 241 +++++++++++++++++++
.../apache/cayenne/util/QueryResultBuilder.java | 150 ++++++++++++
.../org/apache/cayenne/query/SQLExecIT.java | 74 ++++++
4 files changed, 521 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cayenne/blob/763a42f6/cayenne-server/src/main/java/org/apache/cayenne/QueryResult.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/QueryResult.java b/cayenne-server/src/main/java/org/apache/cayenne/QueryResult.java
new file mode 100644
index 0000000..0e112a9
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/QueryResult.java
@@ -0,0 +1,56 @@
+/*****************************************************************
+ * 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;
+
+import java.util.List;
+
+/**
+ * Represents a single item in a multipart query execution. Can be either an
+ * update count or a list of objects.
+ *
+ * @since 4.0
+ */
+public interface QueryResult {
+
+ /**
+ * Returns true if encapsulated result is a select result.
+ */
+ boolean isSelectResult();
+
+ /**
+ * Returns true if encapsulated result is a batch update result.
+ */
+ boolean isBatchUpdate();
+
+ /**
+ * Returns a list of selected objects. Throws unless
+ * {@link #isSelectResult()} returns true.
+ */
+ List<?> getSelectResult();
+
+ /**
+ * Returns an update count.
+ */
+ int getUpdateResult();
+
+ /**
+ * Returns batch update result in a form of array of individual update counts.
+ */
+ int[] getBatchUpdateResult();
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/763a42f6/cayenne-server/src/main/java/org/apache/cayenne/query/SQLExec.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLExec.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLExec.java
new file mode 100644
index 0000000..4fada6c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLExec.java
@@ -0,0 +1,241 @@
+/*****************************************************************
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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;
+
+/**
+ * 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<String, Object>(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 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();
+ }
+
+ public List<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
+ List<QueryResult> results = execute(context);
+
+ if (results.size() != 1) {
+ throw new CayenneRuntimeException("Expected a single update result. Got a total of " + results.size());
+ }
+
+ QueryResult result = results.get(0);
+ if (result.isSelectResult()) {
+ throw new CayenneRuntimeException("Expected a single update result. Got a select instead.");
+ }
+
+ return result.getUpdateResult();
+ }
+
+ public int[] updateBatch(ObjectContext context) {
+ // TODO: create a corresponding method in ObjectContext
+ List<QueryResult> results = execute(context);
+
+ if (results.size() != 1) {
+ throw new CayenneRuntimeException("Expected a single update result. Got a total of " + results.size());
+ }
+
+ QueryResult result = results.get(0);
+ if (result.isSelectResult()) {
+ throw new CayenneRuntimeException("Expected a single update result. Got a select instead.");
+ }
+
+ return result.getBatchUpdateResult();
+ }
+
+ @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;
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/763a42f6/cayenne-server/src/main/java/org/apache/cayenne/util/QueryResultBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/QueryResultBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/util/QueryResultBuilder.java
new file mode 100644
index 0000000..814f5fc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/QueryResultBuilder.java
@@ -0,0 +1,150 @@
+/*****************************************************************
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.QueryResult;
+
+/**
+ * @since 4.0
+ */
+public class QueryResultBuilder {
+
+ private List<QueryResult> queryResult;
+
+ public static QueryResultBuilder builder() {
+ return new QueryResultBuilder(3);
+ }
+
+ public static QueryResultBuilder builder(int expectedSize) {
+ return new QueryResultBuilder(expectedSize);
+ }
+
+ public static List<QueryResult> empty() {
+ return Collections.emptyList();
+ }
+
+ public static List<QueryResult> singleSelect(List<?> selectResult) {
+ return Collections.<QueryResult>singletonList(new SelectResult(selectResult));
+ }
+
+ public static List<QueryResult> singleObjectSelect(Object selectObject) {
+ List<Object> result = Collections.singletonList(selectObject);
+ return Collections.<QueryResult>singletonList(new SelectResult(result));
+ }
+
+ QueryResultBuilder(int expectedSize) {
+ this.queryResult = new ArrayList<QueryResult>(expectedSize);
+ }
+
+ public QueryResultBuilder addSelectResult(List<?> result) {
+ queryResult.add(new SelectResult(result));
+ return this;
+ }
+
+ public QueryResultBuilder addBatchUpdateResult(int[] result) {
+ queryResult.add(new BatchUpdateResult(result));
+ return this;
+ }
+
+ public List<QueryResult> build() {
+ return queryResult;
+ }
+
+ private static class SelectResult implements QueryResult {
+
+ private List<?> result;
+
+ SelectResult(List<?> result) {
+ this.result = result;
+ }
+
+ @Override
+ public boolean isSelectResult() {
+ return true;
+ }
+
+ @Override
+ public boolean isBatchUpdate() {
+ return false;
+ }
+
+ @Override
+ public List<?> getSelectResult() {
+ return result;
+ }
+
+ @Override
+ public int getUpdateResult() {
+ throw new CayenneRuntimeException("Can't access update result. This result is a select");
+ }
+
+ @Override
+ public int[] getBatchUpdateResult() {
+ throw new CayenneRuntimeException("Can't access update result. This result is a select");
+ }
+ }
+
+ private static class BatchUpdateResult implements QueryResult {
+
+ private int[] result;
+
+ BatchUpdateResult(int[] result) {
+ this.result = result;
+ }
+
+ @Override
+ public boolean isSelectResult() {
+ return false;
+ }
+
+ @Override
+ public boolean isBatchUpdate() {
+ return result.length > 1;
+ }
+
+ @Override
+ public List<?> getSelectResult() {
+ throw new CayenneRuntimeException("Can't access select result. This result is an update");
+ }
+
+ @Override
+ public int getUpdateResult() {
+ if (result.length == 0) {
+ throw new CayenneRuntimeException("No update results");
+ }
+
+ if (result.length == 1) {
+ return result[0];
+ }
+
+ throw new CayenneRuntimeException("This result is a batch update");
+ }
+
+ @Override
+ public int[] getBatchUpdateResult() {
+ return result;
+ }
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/763a42f6/cayenne-server/src/test/java/org/apache/cayenne/query/SQLExecIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLExecIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLExecIT.java
new file mode 100644
index 0000000..8e0eeb8
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLExecIT.java
@@ -0,0 +1,74 @@
+/*****************************************************************
+ * 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class SQLExecIT extends ServerCase {
+
+ @Inject
+ private DataContext context;
+
+ @Inject
+ private DBHelper dbHelper;
+
+ @Test
+ public void test_DataMapNameRoot() throws Exception {
+ int inserted = SQLExec.query("testmap", "INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (1, 'a')").update(
+ context);
+ assertEquals(1, inserted);
+ }
+
+ @Test
+ public void test_DefaultRoot() throws Exception {
+ int inserted = SQLExec.query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (1, 'a')").update(context);
+ assertEquals(1, inserted);
+ }
+
+ @Test
+ public void test_ParamsArray_Single() throws Exception {
+
+ int inserted = SQLExec.query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (1, #bind($name))")
+ .paramsArray("a3").update(context);
+
+ assertEquals(1, inserted);
+ assertEquals("a3", dbHelper.getString("ARTIST", "ARTIST_NAME"));
+ }
+
+ @Test
+ public void test_ParamsArray_Multiple() throws Exception {
+
+ int inserted = SQLExec.query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (#bind($id), #bind($name))")
+ .paramsArray(55, "a3").update(context);
+
+ assertEquals(1, inserted);
+ assertArrayEquals(new Object[]{55l, "a3"},
+ dbHelper.select("ARTIST", new String[]{"ARTIST_ID", "ARTIST_NAME"}));
+ }
+}