diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java index 7bf1f5620d..1ed64cbaa9 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed 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 @@ -13,24 +13,35 @@ package org.springframework.integration.jdbc; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.sql.DataSource; -import org.springframework.integration.MessageRejectedException; import org.springframework.integration.handler.AbstractMessageHandler; +import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapperResultSetExtractor; +import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.KeyHolder; import org.springframework.messaging.Message; -import org.springframework.messaging.MessageDeliveryException; -import org.springframework.messaging.MessageHandlingException; +import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; /** @@ -46,18 +57,33 @@ import org.springframework.util.LinkedCaseInsensitiveMap; * headers with dotted names (e.g. business.id) * * @author Dave Syer + * @author Artem Bilan * @since 2.0 */ public class JdbcMessageHandler extends AbstractMessageHandler { + private final ResultSetExtractor>> generatedKeysResultSetExtractor = + new RowMapperResultSetExtractor>(new ColumnMapRowMapper(), 1); + private final NamedParameterJdbcOperations jdbcOperations; + private final PreparedStatementCreator generatedKeysStatementCreator = new PreparedStatementCreator() { + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + return con.prepareStatement(JdbcMessageHandler.this.updateSql, Statement.RETURN_GENERATED_KEYS); + } + + }; + private volatile String updateSql; - private volatile SqlParameterSourceFactory sqlParameterSourceFactory = new BeanPropertySqlParameterSourceFactory(); + private volatile SqlParameterSourceFactory sqlParameterSourceFactory; private volatile boolean keysGenerated; + private MessagePreparedStatementSetter preparedStatementSetter; + /** * Constructor taking {@link DataSource} from which the DB Connection can be obtained and the select query to * execute to retrieve new rows. @@ -83,7 +109,8 @@ public class JdbcMessageHandler extends AbstractMessageHandler { } /** - * Flag to indicate that the update query is an insert with autogenerated keys, which will be logged at debug level. + * Flag to indicate that the update query is an insert with auto-generated keys, + * which will be logged at debug level. * @param keysGenerated the flag value to set */ public void setKeysGenerated(boolean keysGenerated) { @@ -98,41 +125,101 @@ public class JdbcMessageHandler extends AbstractMessageHandler { this.sqlParameterSourceFactory = sqlParameterSourceFactory; } + /** + * Specify a {@link MessagePreparedStatementSetter} to populate parameters on the + * {@link PreparedStatement} with the {@link Message} context. + *

This is a low-level alternative to the {@link SqlParameterSourceFactory}. + * @param preparedStatementSetter the {@link MessagePreparedStatementSetter} to set. + * @since 4.2 + */ + public void setPreparedStatementSetter(MessagePreparedStatementSetter preparedStatementSetter) { + this.preparedStatementSetter = preparedStatementSetter; + } + @Override public String getComponentType() { return "jdbc:outbound-channel-adapter"; } + @Override + protected void onInit() throws Exception { + super.onInit(); + Assert.state(!(this.sqlParameterSourceFactory != null && this.preparedStatementSetter != null), + "'sqlParameterSourceFactory' and 'preparedStatementSetter' are mutually exclusive."); + if (this.sqlParameterSourceFactory == null && this.preparedStatementSetter == null) { + this.sqlParameterSourceFactory = new BeanPropertySqlParameterSourceFactory(); + } + } + /** * Executes the update, passing the message into the {@link SqlParameterSourceFactory}. */ @Override - protected void handleMessageInternal(Message message) throws MessageRejectedException, MessageHandlingException, - MessageDeliveryException { + protected void handleMessageInternal(Message message) throws Exception { List> keys = executeUpdateQuery(message, keysGenerated); - if (logger.isDebugEnabled() && !keys.isEmpty()) { - logger.debug("Generated keys: "+keys); + if (!keys.isEmpty() && logger.isDebugEnabled()) { + logger.debug("Generated keys: " + keys); } } - protected List> executeUpdateQuery(Object obj, boolean keysGenerated) { - SqlParameterSource updateParameterSource = new MapSqlParameterSource(); - if (this.sqlParameterSourceFactory != null) { - updateParameterSource = this.sqlParameterSourceFactory.createParameterSource(obj); + protected List> executeUpdateQuery(final Message message, boolean keysGenerated) { + SqlParameterSource updateParameterSource = EmptySqlParameterSource.INSTANCE; + if (this.preparedStatementSetter == null) { + if (this.sqlParameterSourceFactory != null) { + updateParameterSource = this.sqlParameterSourceFactory.createParameterSource(message); + } } if (keysGenerated) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - this.jdbcOperations.update(this.updateSql, updateParameterSource, - keyHolder); - return keyHolder.getKeyList(); + if (this.preparedStatementSetter != null) { + return this.jdbcOperations.getJdbcOperations().execute(this.generatedKeysStatementCreator, + new PreparedStatementCallback>>() { + + @Override + public List> doInPreparedStatement(PreparedStatement ps) + throws SQLException { + preparedStatementSetter.setValues(ps, message); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys != null) { + try { + + return generatedKeysResultSetExtractor.extractData(keys); + } + finally { + JdbcUtils.closeResultSet(keys); + } + } + return new LinkedList>(); + } + + }); + } + else { + KeyHolder keyHolder = new GeneratedKeyHolder(); + this.jdbcOperations.update(this.updateSql, updateParameterSource, keyHolder); + return keyHolder.getKeyList(); + } } else { - int updated = this.jdbcOperations.update(this.updateSql, updateParameterSource); + int updated; + if (this.preparedStatementSetter != null) { + updated = this.jdbcOperations.getJdbcOperations().update(this.updateSql, + new PreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps) throws SQLException { + JdbcMessageHandler.this.preparedStatementSetter.setValues(ps, message); + } + + }); + } + else { + updated = this.jdbcOperations.update(this.updateSql, updateParameterSource); + } LinkedCaseInsensitiveMap map = new LinkedCaseInsensitiveMap(); map.put("UPDATED", updated); return Collections.singletonList(map); } - } } diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcOutboundGateway.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcOutboundGateway.java index 3befbcc7e5..06609814d3 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcOutboundGateway.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcOutboundGateway.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.jdbc; import java.util.Collections; @@ -33,6 +34,7 @@ import org.springframework.util.StringUtils; /** * @author Dave Syer * @author Gunnar Hillert + * @author Artem Bilan * * @since 2.0 */ @@ -169,7 +171,7 @@ public class JdbcOutboundGateway extends AbstractReplyProducingMessageHandler im } /** - * Flag to indicate that the update query is an insert with autogenerated keys, which will be logged at debug level. + * Flag to indicate that the update query is an insert with auto-generated keys, which will be logged at debug level. * @param keysGenerated the flag value to set */ public void setKeysGenerated(boolean keysGenerated) { @@ -177,10 +179,15 @@ public class JdbcOutboundGateway extends AbstractReplyProducingMessageHandler im } public void setRequestSqlParameterSourceFactory(SqlParameterSourceFactory sqlParameterSourceFactory) { - Assert.notNull(this.handler); + Assert.notNull(this.handler, "'handler' cannot be null"); this.handler.setSqlParameterSourceFactory(sqlParameterSourceFactory); } + public void setRequestPreparedStatementSetter(MessagePreparedStatementSetter requestPreparedStatementSetter) { + Assert.notNull(this.handler, "'handler' cannot be null"); + this.handler.setPreparedStatementSetter(requestPreparedStatementSetter); + } + public void setReplySqlParameterSourceFactory(SqlParameterSourceFactory sqlParameterSourceFactory) { this.sqlParameterSourceFactory = sqlParameterSourceFactory; this.sqlParameterSourceFactorySet = true; diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/MessagePreparedStatementSetter.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/MessagePreparedStatementSetter.java new file mode 100644 index 0000000000..999d31c1fc --- /dev/null +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/MessagePreparedStatementSetter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed 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.springframework.integration.jdbc; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.messaging.Message; + +/** + * The callback to be used with the {@link JdbcMessageHandler} + * as an alternative to the {@link SqlParameterSourceFactory}. + *

+ * Plays the same role as standard {@link PreparedStatementSetter}, + * but with {@code Message requestMessage} context during {@code handleMessage} + * process in the {@link JdbcMessageHandler}. + * + * @author Artem Bilan + * @since 4.2 + * @see PreparedStatementSetter + */ +public interface MessagePreparedStatementSetter { + + void setValues(PreparedStatement ps, Message requestMessage) throws SQLException; + +} diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcMessageHandlerParser.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcMessageHandlerParser.java index 94f5c5371c..5034d60592 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcMessageHandlerParser.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcMessageHandlerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed 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 @@ -13,6 +13,8 @@ package org.springframework.integration.jdbc.config; +import org.w3c.dom.Element; + import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -21,10 +23,10 @@ import org.springframework.integration.config.xml.AbstractOutboundChannelAdapter import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.jdbc.JdbcMessageHandler; import org.springframework.util.StringUtils; -import org.w3c.dom.Element; /** * @author Dave Syer + * @author Artem Bilan * @since 2.0 * */ @@ -67,6 +69,7 @@ public class JdbcMessageHandlerParser extends AbstractOutboundChannelAdapterPars builder.addConstructorArgReference(jdbcOperationsRef); } IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "sql-parameter-source-factory"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "prepared-statement-setter"); builder.addConstructorArgValue(query); return builder.getBeanDefinition(); } diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParser.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParser.java index 378b57aed6..8cc5bcd990 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParser.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed 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 @@ -13,13 +13,14 @@ package org.springframework.integration.jdbc.config; +import org.w3c.dom.Element; + import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.xml.AbstractConsumerEndpointParser; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.jdbc.JdbcOutboundGateway; import org.springframework.util.StringUtils; -import org.w3c.dom.Element; /** * @author Dave Syer @@ -64,6 +65,8 @@ public class JdbcOutboundGatewayParser extends AbstractConsumerEndpointParser { .setReferenceIfAttributeDefined(builder, element, "reply-sql-parameter-source-factory"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-sql-parameter-source-factory"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, + "request-prepared-statement-setter"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "row-mapper"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "max-rows-per-poll"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "keys-generated"); diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/config/spring-integration-jdbc-4.2.xsd b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/config/spring-integration-jdbc-4.2.xsd index b40ae59fad..2d609cc15d 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/config/spring-integration-jdbc-4.2.xsd +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/config/spring-integration-jdbc-4.2.xsd @@ -243,6 +243,7 @@ default factory creates a bean property parameter source so the query can specify named parameters like :payload and :headers[foo]. + This attribute is mutually exclusive with the 'prepared-statement-setter'. @@ -250,6 +251,19 @@ + + + + + Reference to a MessagePreparedStatementSetter. + This attribute is mutually exclusive with the 'sql-parameter-source-factory'. + + + + + + + @@ -362,6 +376,7 @@ default factory creates a bean property parameter source so the query can specify named parameters like :payload and :headers[foo]. + This attribute is mutually exclusive with the 'request-prepared-statement-setter'. @@ -369,6 +384,19 @@ + + + + + Reference to a MessagePreparedStatementSetter. + This attribute is mutually exclusive with the 'request-sql-parameter-source-factory'. + + + + + + + diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageHandlerIntegrationTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageHandlerIntegrationTests.java index c3a6d55111..4363c8035c 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageHandlerIntegrationTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,12 @@ package org.springframework.integration.jdbc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; import org.junit.Before; @@ -34,6 +38,7 @@ import org.springframework.messaging.support.GenericMessage; /** * @author Dave Syer + * @author Artem Bilan */ public class JdbcMessageHandlerIntegrationTests { @@ -58,6 +63,7 @@ public class JdbcMessageHandlerIntegrationTests { @Test public void testSimpleStaticInsert() { JdbcMessageHandler handler = new JdbcMessageHandler(jdbcTemplate, "insert into foos (id, status, name) values (1, 0, 'foo')"); + handler.afterPropertiesSet(); Message message = new GenericMessage("foo"); handler.handleMessage(message); Map map = jdbcTemplate.queryForMap("SELECT * FROM FOOS WHERE ID=?", 1); @@ -69,15 +75,38 @@ public class JdbcMessageHandlerIntegrationTests { @Test public void testSimpleDynamicInsert() { JdbcMessageHandler handler = new JdbcMessageHandler(jdbcTemplate, "insert into foos (id, status, name) values (1, 0, :payload)"); + handler.afterPropertiesSet(); Message message = new GenericMessage("foo"); handler.handleMessage(message); Map map = jdbcTemplate.queryForMap("SELECT * FROM FOOS WHERE ID=?", 1); assertEquals("Wrong name", "foo", map.get("NAME")); } + @Test + public void testInsertWithMessagePreparedStatementSetter() { + JdbcMessageHandler handler = new JdbcMessageHandler(jdbcTemplate, "insert into foos (id, status, name) values (1, 0, ?)"); + final AtomicBoolean setterInvoked = new AtomicBoolean(); + handler.setPreparedStatementSetter(new MessagePreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, Message requestMessage) throws SQLException { + ps.setObject(1, requestMessage.getPayload()); + setterInvoked.set(true); + } + + }); + handler.afterPropertiesSet(); + Message message = new GenericMessage("foo"); + handler.handleMessage(message); + Map map = jdbcTemplate.queryForMap("SELECT * FROM FOOS WHERE ID=?", 1); + assertEquals("Wrong name", "foo", map.get("NAME")); + assertTrue(setterInvoked.get()); + } + @Test public void testIdHeaderDynamicInsert() { JdbcMessageHandler handler = new JdbcMessageHandler(jdbcTemplate, "insert into foos (id, status, name) values (:headers[idAsString], 0, :payload)"); + handler.afterPropertiesSet(); Message message = new GenericMessage("foo"); String id = message.getHeaders().getId().toString(); message = MessageBuilder.fromMessage(message) @@ -92,6 +121,7 @@ public class JdbcMessageHandlerIntegrationTests { @Test public void testDottedHeaderDynamicInsert() { JdbcMessageHandler handler = new JdbcMessageHandler(jdbcTemplate, "insert into foos (id, status, name) values (:headers[business.id], 0, :payload)"); + handler.afterPropertiesSet(); Message message = MessageBuilder.withPayload("foo").setHeader("business.id", "FOO").build(); handler.handleMessage(message); String id = message.getHeaders().get("business.id").toString(); diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParserTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParserTests.java index 3cf86da09c..d50e5c4652 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParserTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/JdbcOutboundGatewayParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed 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 @@ -10,6 +10,7 @@ * 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.springframework.integration.jdbc.config; import static org.junit.Assert.assertEquals; @@ -17,6 +18,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.Collections; import java.util.Map; @@ -34,12 +37,14 @@ import org.springframework.integration.core.MessagingTemplate; import org.springframework.integration.endpoint.PollingConsumer; import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice; import org.springframework.integration.jdbc.JdbcOutboundGateway; +import org.springframework.integration.jdbc.MessagePreparedStatementSetter; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.support.GenericMessage; /** * @author Dave Syer @@ -84,19 +89,33 @@ public class JdbcOutboundGatewayParserTests { } @Test + @SuppressWarnings("unchecked") public void testKeyGeneration() { setUp("handlingKeyGenerationJdbcOutboundGatewayTest.xml", getClass()); Message message = MessageBuilder.withPayload(Collections.singletonMap("foo", "bar")).build(); channel.send(message); Message reply = messagingTemplate.receive(); assertNotNull(reply); - @SuppressWarnings("unchecked") Map payload = (Map) reply.getPayload(); Object id = payload.get("SCOPE_IDENTITY()"); assertNotNull(id); Map map = this.jdbcTemplate.queryForMap("SELECT * from BARS"); assertEquals("Wrong id", id, map.get("ID")); assertEquals("Wrong name", "bar", map.get("name")); + + this.jdbcTemplate.execute("DELETE FROM BARS"); + + MessageChannel setterRequest = this.context.getBean("setterRequest", MessageChannel.class); + setterRequest.send(new GenericMessage("bar2")); + reply = messagingTemplate.receive(); + assertNotNull(reply); + + payload = (Map) reply.getPayload(); + id = payload.get("SCOPE_IDENTITY()"); + assertNotNull(id); + map = this.jdbcTemplate.queryForMap("SELECT * from BARS"); + assertEquals("Wrong id", id, map.get("ID")); + assertEquals("Wrong name", "bar2", map.get("name")); } @Test @@ -259,4 +278,14 @@ public class JdbcOutboundGatewayParserTests { } } + + public static class TestMessagePreparedStatementSetter implements MessagePreparedStatementSetter { + + @Override + public void setValues(PreparedStatement ps, Message requestMessage) throws SQLException { + ps.setObject(1, requestMessage.getPayload()); + } + + } + } diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingKeyGenerationJdbcOutboundGatewayTest.xml b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingKeyGenerationJdbcOutboundGatewayTest.xml index 8e185f5ce5..b3f60c98f9 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingKeyGenerationJdbcOutboundGatewayTest.xml +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingKeyGenerationJdbcOutboundGatewayTest.xml @@ -17,6 +17,16 @@ + + + + diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingParameterSourceJdbcOutboundChannelAdapterTest.xml b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingParameterSourceJdbcOutboundChannelAdapterTest.xml index 7a4b7cdcd7..cc3233c05b 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingParameterSourceJdbcOutboundChannelAdapterTest.xml +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/config/handlingParameterSourceJdbcOutboundChannelAdapterTest.xml @@ -1,15 +1,23 @@ + + + + + + diff --git a/spring-integration-jdbc/src/test/resources/log4j.properties b/spring-integration-jdbc/src/test/resources/log4j.properties index 5c0370d306..badf30e5cb 100644 --- a/spring-integration-jdbc/src/test/resources/log4j.properties +++ b/spring-integration-jdbc/src/test/resources/log4j.properties @@ -1,4 +1,4 @@ -log4j.rootCategory=INFO, stdout +log4j.rootCategory=WARN, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout diff --git a/src/reference/asciidoc/jdbc.adoc b/src/reference/asciidoc/jdbc.adoc index 7e8479a835..381b096744 100644 --- a/src/reference/asciidoc/jdbc.adoc +++ b/src/reference/asciidoc/jdbc.adoc @@ -181,6 +181,49 @@ The following example uses a `ExpressionEvaluatingSqlParameterSourceFactory` to For further information, please also see <> +_PreparedStatement Callback_ + +There are some cases when the flexibility and loose-coupling of `SqlParameterSourceFactory` isn't enough for the target +`PreparedStatement` or we need to do some low-level JDBC work. +The Spring JDBC module provides APIs to configure the execution environment (e.g. `ConnectionCallback` +or `PreparedStatementCreator`) and manipulation of parameter values (e.g. `SqlParameterSource`). +Or even APIs for low level operations, for example `StatementCallback`. + +Starting with _Spring Integration 4.2_, the `MessagePreparedStatementSetter` is available to allow +the specification of parameters on the `PreparedStatement` manually, in the `requestMessage` context. +This class plays exactly the same role as `PreparedStatementSetter` in the standard Spring JDBC API. +Actually it is invoked directly from an inline `PreparedStatementSetter` implementation, when the `JdbcMessageHandler` +performs invokes `execute` on the `JdbcTemplate`. + +This functional interface option is mutually exclusive with `sqlParameterSourceFactory` and can be used as a more +powerful alternative to populate parameters of the `PreparedStatement` from the `requestMessage`. +For example it is useful when we need to store `File` data to the DataBase `BLOB` column in a stream manner: + +[source,java] +---- +@Bean +@ServiceActivator(inputChannel = "storeFileChannel") +public MessageHandler jdbcMessageHandler(DataSource dataSource) { + JdbcMessageHandler jdbcMessageHandler = new JdbcMessageHandler(dataSource, + "INSERT INTO imagedb (image_name, content, description) VALUES (?, ?, ?)"); + jdbcMessageHandler.setPreparedStatementSetter((ps, m) -> { + ps.setString(1, m.getHeaders().get(FileHeaders.FILENAME)); + try (FileInputStream inputStream = new FileInputStream((File) m.getPayload())) { + ps.setBlob(2, inputStream); + } + catch (Exception e) { + throw new MessageHandlingException(m, e); + } + ps.setClob(3, new StringReader(m.getHeaders().get("description", String.class))); + }); + return jdbcMessageHandler; +} +---- + +From the XML configuration perspective, the `prepared-statement-setter` attribute is available on the +`` component, to specify a `MessagePreparedStatementSetter` +bean reference. + [[jdbc-outbound-gateway]] === Outbound Gateway @@ -239,11 +282,19 @@ If keys-generated="true" then the root of the expression is the generated keys ( The outbound gateway requires a reference to either a DataSource or a JdbcTemplate. It can also have a `SqlParameterSourceFactory` injected to control the binding of the incoming message to the query. +Starting with the _version 4.2_ the `request-prepared-statement-setter` attribute is available on the +`` as an alternative to the `request-sql-parameter-source-factory`. +It allows you to specify a `MessagePreparedStatementSetter` bean reference, which implements more sophisticated +`PreparedStatement` preparation before its execution. + +See <> for more information about `MessagePreparedStatementSetter`. + [[jdbc-message-store]] === JDBC Message Store -Spring Integration provides 2 JDBC specifc Message Store implementations. -The first one, is the `JdbcMessageStore` which is suitable to be used in conjunction with _Aggregators_ and the _Claimcheck_ pattern. +Spring Integration provides 2 JDBC specific Message Store implementations. +The first one, is the `JdbcMessageStore` which is suitable to be used in conjunction with _Aggregators_ and the +_Claim-Check_ pattern. While it can be used for backing _Message Channels_ as well, you may want to consider using the `JdbcChannelMessageStore` implementation instead, as it provides a more targeted and scalable implementation. [[jdbc-message-store-generic]] diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index a916e4ec58..ee40e08a85 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -76,6 +76,14 @@ Codec-based transformers and message converters are also provided. See <> for more information. +[[x4.2-prepared-statement-setter]] +==== Message PreparedStatement Setter + +A new `MessagePreparedStatementSetter` functional interface callback is available for the `JdbcMessageHandler` +(`` and ``) as an alternative to the +`SqlParameterSourceFactory` to populate parameters on the `PreparedStatement` with the `requestMessage` context. + +See <> for more information. [[x4.2-general]] === General Changes