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"}));
+    }
+}