Repository: camel Updated Branches: refs/heads/camel-2.12.x 18ac8cd60 -> 88b77a363 refs/heads/camel-2.13.x 39744db54 -> f0fbc8430
[CAMEL-7313] Adding support to retrieve generated keys for camel-sql Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/6919c57c Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/6919c57c Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/6919c57c Branch: refs/heads/camel-2.13.x Commit: 6919c57c7cdfeb8def9b429a64668183544b7d31 Parents: 39744db Author: Grzegorz Grzybek <gr.grzy...@gmail.com> Authored: Fri Mar 28 21:44:35 2014 +0100 Committer: Willem Jiang <willem.ji...@gmail.com> Committed: Mon Mar 31 14:16:43 2014 +0800 ---------------------------------------------------------------------- components/camel-sql/pom.xml | 5 + .../camel/component/sql/SqlConstants.java | 25 ++ .../apache/camel/component/sql/SqlProducer.java | 48 +++- .../component/sql/SqlGeneratedKeysTest.java | 261 +++++++++++++++++++ .../sql/createAndPopulateDatabase3.sql | 27 ++ parent/pom.xml | 7 + 6 files changed, 371 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/6919c57c/components/camel-sql/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-sql/pom.xml b/components/camel-sql/pom.xml index 3d12c4c..8b48e23 100644 --- a/components/camel-sql/pom.xml +++ b/components/camel-sql/pom.xml @@ -81,6 +81,11 @@ <artifactId>derby</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.hsqldb</groupId> + <artifactId>hsqldb</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> http://git-wip-us.apache.org/repos/asf/camel/blob/6919c57c/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java ---------------------------------------------------------------------- diff --git a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java index aef93a2..e2fef78 100644 --- a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java +++ b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java @@ -27,6 +27,31 @@ public final class SqlConstants { public static final String SQL_ROW_COUNT = "CamelSqlRowCount"; + /** + * Boolean input header. + * Set its value to true to retrieve generated keys, default is false + */ + public static final String SQL_RETRIEVE_GENERATED_KEYS = "CamelSqlRetrieveGeneratedKeys"; + + /** + * <tt>String[]</tt> or <tt>int[]</tt> input header - optional + * Set it to specify the expected generated columns, see: + * + * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/sql/Statement.html#execute(java.lang.String, int[])"> + * java.sql.Statement.execute(java.lang.String, int[])</a> + * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/sql/Statement.html#execute(java.lang.String, java.lang.String[])"> + * java.sql.Statement.execute(java.lang.String, java.lang.String[])</a> + */ + public static final String SQL_GENERATED_COLUMNS = "CamelGeneratedColumns"; + + /** + * <tt>org.springframework.jdbc.support.KeyHolder</tt> output header containing the generated keys retrieved + * + * @see <a href="http://docs.spring.io/spring/docs/3.2.x/javadoc-api/org/springframework/jdbc/support/KeyHolder.html"> + * org.springframework.jdbc.support.KeyHolder</a> + */ + public static final String SQL_GENERATED_KEY_HOLDER = "CamelSqlGeneratedKeyHolder"; + private SqlConstants() { // Utility class } http://git-wip-us.apache.org/repos/asf/camel/blob/6919c57c/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java ---------------------------------------------------------------------- diff --git a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java index 870e7b4..2df7941 100644 --- a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java +++ b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java @@ -16,9 +16,11 @@ */ package org.apache.camel.component.sql; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -27,6 +29,9 @@ import org.apache.camel.Exchange; import org.apache.camel.impl.DefaultProducer; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; public class SqlProducer extends DefaultProducer { private String query; @@ -57,7 +62,33 @@ public class SqlProducer extends DefaultProducer { final String sql = queryHeader != null ? queryHeader : query; final String preparedQuery = sqlPrepareStatementStrategy.prepareQuery(sql, getEndpoint().isAllowNamedParameters()); - jdbcTemplate.execute(preparedQuery, new PreparedStatementCallback<Map<?, ?>>() { + // CAMEL-7313 - check whether to return generated keys + final Boolean shouldRetrieveGeneratedKeys = + exchange.getIn().getHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, false, Boolean.class); + + PreparedStatementCreator statementCreator = new PreparedStatementCreator() { + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + if (!shouldRetrieveGeneratedKeys) { + return con.prepareStatement(preparedQuery); + } else { + Object expectedGeneratedColumns = exchange.getIn().getHeader(SqlConstants.SQL_GENERATED_COLUMNS); + if (expectedGeneratedColumns == null) { + return con.prepareStatement(preparedQuery, Statement.RETURN_GENERATED_KEYS); + } else if (expectedGeneratedColumns instanceof String[]) { + return con.prepareStatement(preparedQuery, (String[]) expectedGeneratedColumns); + } else if (expectedGeneratedColumns instanceof int[]) { + return con.prepareStatement(preparedQuery, (int[]) expectedGeneratedColumns); + } else { + throw new IllegalArgumentException( + "Header specifying expected returning columns isn't an instance of String[] or int[] but " + + expectedGeneratedColumns.getClass()); + } + } + } + }; + + jdbcTemplate.execute(statementCreator, new PreparedStatementCallback<Map<?, ?>>() { public Map<?, ?> doInPreparedStatement(PreparedStatement ps) throws SQLException { int expected = parametersCount > 0 ? parametersCount : ps.getParameterMetaData().getParameterCount(); @@ -78,6 +109,8 @@ public class SqlProducer extends DefaultProducer { } } + boolean isResultSet = false; + // execute the prepared statement and populate the outgoing message if (batch) { int[] updateCounts = ps.executeBatch(); @@ -87,7 +120,7 @@ public class SqlProducer extends DefaultProducer { } exchange.getIn().setHeader(SqlConstants.SQL_UPDATE_COUNT, total); } else { - boolean isResultSet = ps.execute(); + isResultSet = ps.execute(); if (isResultSet) { // preserve headers first, so we can override the SQL_ROW_COUNT header exchange.getOut().getHeaders().putAll(exchange.getIn().getHeaders()); @@ -123,6 +156,17 @@ public class SqlProducer extends DefaultProducer { } } + if (shouldRetrieveGeneratedKeys) { + if (isResultSet) { + // we won't return generated keys for SELECT statements + exchange.getOut().setHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, new GeneratedKeyHolder()); + } else { + List<Map<String, Object>> generatedKeys = getEndpoint().queryForList(ps.getGeneratedKeys()); + KeyHolder holder = new GeneratedKeyHolder(generatedKeys); + exchange.getOut().setHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, holder); + } + } + // data is set on exchange so return null return null; } http://git-wip-us.apache.org/repos/asf/camel/blob/6919c57c/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlGeneratedKeysTest.java ---------------------------------------------------------------------- diff --git a/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlGeneratedKeysTest.java b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlGeneratedKeysTest.java new file mode 100644 index 0000000..ce3326e --- /dev/null +++ b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlGeneratedKeysTest.java @@ -0,0 +1,261 @@ +/** + * 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.camel.component.sql; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.jdbc.support.KeyHolder; + + +public class SqlGeneratedKeysTest extends CamelTestSupport { + + private EmbeddedDatabase db; + + @Before + public void setUp() throws Exception { + // Only HSQLDB seem to handle: + // - more than one generated column in row + // - return all keys generated in batch insert + db = new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL).addScript("sql/createAndPopulateDatabase3.sql").build(); + + super.setUp(); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + + db.shutdown(); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + getContext().getComponent("sql", SqlComponent.class).setDataSource(db); + from("direct:insert").to("sql:insert into projects (project, license, description) values (#, #, #)").to("mock:result"); + from("direct:batch").to("sql:insert into projects (project, license, description) values (#, #, #)?batch=true").to("mock:result"); + from("direct:select").to("sql:select * from projects order by id asc").to("mock:result"); + from("direct:insert2").to("sql:insert into developers (name, position) values (#, #)").to("mock:result"); + } + }; + } + + @Test + public void testRetrieveGeneratedKey() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:insert"); + + Exchange exchange = endpoint.createExchange(); + exchange.getIn().setBody(new Object[] {"project x", "ASF", "new project"}); + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + + // now we send the exchange to the endpoint, and receives the response from Camel + Exchange out = template.send(endpoint, exchange); + + // assertions of the response + assertNotNull(out); + assertNotNull(out.getOut()); + assertNotNull(out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER)); + + KeyHolder generatedKeys = out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, KeyHolder.class); + assertNotNull("out body could not be converted to a KeyHolder - was: " + + out.getIn().getBody(), generatedKeys); + assertEquals(1, generatedKeys.getKeyList().get(0).size()); + + Map<String, Object> row = generatedKeys.getKeys(); + assertEquals("auto increment value should be 3", Integer.valueOf(3), row.get("ID")); + + assertEquals("generated keys row count should be one", 1, generatedKeys.getKeyList().size()); + } + + @Test + public void testRetrieveGeneratedKeys() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:insert2"); + + Exchange exchange = endpoint.createExchange(); + exchange.getIn().setBody(new Object[] {"Star Swirl", "Wizard"}); + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + exchange.getIn().setHeader(SqlConstants.SQL_GENERATED_COLUMNS, new String[]{"ID1", "ID2"}); + + // now we send the exchange to the endpoint, and receives the response from Camel + Exchange out = template.send(endpoint, exchange); + + // assertions of the response + assertNotNull(out); + assertNotNull(out.getOut()); + assertNotNull(out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER)); + + KeyHolder generatedKeys = out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, KeyHolder.class); + assertNotNull("out body could not be converted to a KeyHolder - was: " + + out.getIn().getBody(), generatedKeys); + assertEquals(2, generatedKeys.getKeyList().get(0).size()); + + Map<String, Object> row = generatedKeys.getKeys(); + assertEquals("auto increment value of ID1 should be 5", Integer.valueOf(5), row.get("ID1")); + assertEquals("auto increment value of ID2 should be 6", Integer.valueOf(6), row.get("ID2")); + + assertEquals("generated keys row count should be one", 1, generatedKeys.getKeyList().size()); + } + + @Test + public void testRetrieveGeneratedKeysForBatch() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:batch"); + + Exchange exchange = endpoint.createExchange(); + List<Object[]> payload = new ArrayList<Object[]>(4); + payload.add(new Object[] {"project x", "ASF", "new project x"}); + payload.add(new Object[] {"project y", "ASF", "new project y"}); + payload.add(new Object[] {"project z", "ASF", "new project z"}); + payload.add(new Object[] {"project q", "ASF", "new project q"}); + exchange.getIn().setBody(payload); + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + + // now we send the exchange to the endpoint, and receives the response from Camel + Exchange out = template.send(endpoint, exchange); + + // assertions of the response + assertNotNull(out); + assertNotNull(out.getOut()); + assertNotNull(out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER)); + + KeyHolder generatedKeys = out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, KeyHolder.class); + assertNotNull("out body could not be converted to a KeyHolder - was: " + + out.getIn().getBody(), generatedKeys); + + // it seems not to work with Derby... + assertEquals(4, generatedKeys.getKeyList().size()); + + int id = 3; + for (Map<String, Object> row: generatedKeys.getKeyList()) { + assertEquals("auto increment value should be " + id, Integer.valueOf(id++), row.get("ID")); + } + + } + + @Test + public void testRetrieveGeneratedKeyWithStringGeneratedColumns() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:insert"); + + Exchange exchange = endpoint.createExchange(); + exchange.getIn().setBody(new Object[] {"project x", "ASF", "new project"}); + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + exchange.getIn().setHeader(SqlConstants.SQL_GENERATED_COLUMNS, new String[]{"ID"}); + + // now we send the exchange to the endpoint, and receives the response from Camel + Exchange out = template.send(endpoint, exchange); + + // assertions of the response + assertNotNull(out); + assertNotNull(out.getOut()); + assertNotNull(out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER)); + + KeyHolder generatedKeys = out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, KeyHolder.class); + assertNotNull("out body could not be converted to a KeyHolder - was: " + + out.getIn().getBody(), generatedKeys); + assertEquals(1, generatedKeys.getKeyList().get(0).size()); + + Map<String, Object> row = generatedKeys.getKeys(); + assertEquals("auto increment value should be 3", Integer.valueOf(3), row.get("ID")); + + assertEquals("generated keys row count should be one", 1, generatedKeys.getKeyList().size()); + } + + @Test + public void testRetrieveGeneratedKeyWithIntGeneratedColumns() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:insert"); + + Exchange exchange = endpoint.createExchange(); + exchange.getIn().setBody(new Object[] {"project x", "ASF", "new project"}); + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + exchange.getIn().setHeader(SqlConstants.SQL_GENERATED_COLUMNS, new int[]{1}); + + // now we send the exchange to the endpoint, and receives the response from Camel + Exchange out = template.send(endpoint, exchange); + + // assertions of the response + assertNotNull(out); + assertNotNull(out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER)); + + KeyHolder generatedKeys = out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, KeyHolder.class); + assertNotNull("out body could not be converted to a KeyHolder - was: " + + out.getIn().getBody(), generatedKeys); + assertEquals(1, generatedKeys.getKeyList().get(0).size()); + + Map<String, Object> row = generatedKeys.getKeys(); + assertEquals("auto increment value should be 3", Integer.valueOf(3), row.get("ID")); + + assertEquals("generated keys row count should be one", 1, generatedKeys.getKeyList().size()); + } + + @Test + public void testGivenAnInvalidGeneratedColumnsHeaderThenAnExceptionIsThrown() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:insert"); + + Exchange exchange = endpoint.createExchange(); + // then we set the SQL on the in body + exchange.getIn().setBody(new Object[] {"project x", "ASF", "new project"}); + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + + // set wrong data type for generated columns + exchange.getIn().setHeader(SqlConstants.SQL_GENERATED_COLUMNS, new Object[]{}); + + // now we send the exchange to the endpoint, and receives the response from Camel + template.send(endpoint, exchange); + + assertTrue(exchange.isFailed()); + } + + @Test + public void testNoKeysForSelect() throws Exception { + // first we create our exchange using the endpoint + Endpoint endpoint = context.getEndpoint("direct:select"); + + Exchange exchange = endpoint.createExchange(); + // then we set the SQL on the in body + exchange.getIn().setHeader(SqlConstants.SQL_RETRIEVE_GENERATED_KEYS, true); + + // now we send the exchange to the endpoint, and receives the response from Camel + Exchange out = template.send(endpoint, exchange); + + @SuppressWarnings("unchecked") + List<Map<String, Object>> result = out.getIn().getBody(List.class); + assertEquals("We should get 3 projects", 3, result.size()); + + KeyHolder holder = out.getIn().getHeader(SqlConstants.SQL_GENERATED_KEY_HOLDER, KeyHolder.class); + assertEquals("We should not get any keys", 0, holder.getKeyList().size()); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/6919c57c/components/camel-sql/src/test/resources/sql/createAndPopulateDatabase3.sql ---------------------------------------------------------------------- diff --git a/components/camel-sql/src/test/resources/sql/createAndPopulateDatabase3.sql b/components/camel-sql/src/test/resources/sql/createAndPopulateDatabase3.sql new file mode 100644 index 0000000..d26fd27 --- /dev/null +++ b/components/camel-sql/src/test/resources/sql/createAndPopulateDatabase3.sql @@ -0,0 +1,27 @@ +-- ------------------------------------------------------------------------ +-- 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. +-- ------------------------------------------------------------------------ + +-- START SNIPPET: e1 +create table projects (id integer primary key GENERATED ALWAYS AS IDENTITY, project varchar(10), license varchar(5), description varchar(1000) default null); +insert into projects (project, license, description) values ('Camel', 'ASF', ''); +insert into projects (project, license, description) values ('AMQ', 'ASF', ''); +insert into projects (project, license, description) values ('Linux', 'XXX', ''); +-- END SNIPPET: e1 + +-- START SNIPPET: e2 +create table developers (id1 integer primary key GENERATED ALWAYS AS IDENTITY (START WITH 5), name varchar(20), position varchar(20), id2 integer GENERATED ALWAYS AS (id1+1)); +-- END SNIPPET: e2 \ No newline at end of file http://git-wip-us.apache.org/repos/asf/camel/blob/6919c57c/parent/pom.xml ---------------------------------------------------------------------- diff --git a/parent/pom.xml b/parent/pom.xml index eacf016..b7fa009 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -179,6 +179,7 @@ <hbase-bundle-version>0.94.6_1</hbase-bundle-version> <hibernate-validator-version>5.0.3.Final</hibernate-validator-version> <hibernate-version>4.2.9.Final</hibernate-version> + <hsqldb-version>2.3.2</hsqldb-version> <httpunit-version>1.7</httpunit-version> <httpcore4-version>4.3.2</httpcore4-version> <httpclient4-version>4.3.3</httpclient4-version> @@ -1930,6 +1931,12 @@ <artifactId>derby</artifactId> <version>${derby-version}</version> </dependency> + <dependency> + <groupId>org.hsqldb</groupId> + <artifactId>hsqldb</artifactId> + <version>${hsqldb-version}</version> + <scope>test</scope> + </dependency> <!-- optional SNMP4J --> <dependency>