This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26145-mybatis-inout-body in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8a844e3a1334db0d891d363a5af480ef6a7b2d93 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 17:19:27 2026 +0100 CAMEL-26145: camel-mybatis - Keep the body with InOut, and fail an Insert without a body when the statement has parameters With an InOut exchange the producer copied the in body to the out message only when outputHeader was set, so Insert, Update and Delete (and their List variants), which put the result in a header, left the body null. The body is now always preserved, and a SELECT still replaces it with the result. An Insert with a null body ran the statement without a parameter, so a statement with parameters inserted a row of NULLs without an error. It now fails with an IllegalArgumentException; a statement without parameters still runs. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/docs/mybatis-component.adoc | 5 + .../src/main/docs/mybatis-component.adoc | 5 + .../camel/component/mybatis/MyBatisProducer.java | 21 +++- .../mybatis/MyBatisInOutKeepsBodyTest.java | 127 +++++++++++++++++++++ .../mybatis/MyBatisInsertNullBodyTest.java | 63 ++++++++++ .../mybatis/MyBatisInsertWithRollbackTest.java | 8 +- .../org/apache/camel/component/mybatis/Account.xml | 6 + .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 10 ++ 8 files changed, 239 insertions(+), 6 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc index d84d40a70f92..434feb36377a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc @@ -63,6 +63,11 @@ Camel will not replace the body. This allows you to continue routing and keep the original body. The response from MyBatis is stored in the header with the key `CamelMyBatisResult`, or in the header that the `outputHeader` option names, which leaves the body unchanged for a `SELECT` as well. +This is the same for an `InOnly` and an `InOut` exchange. + +When the body (or the header that the `inputHeader` option names) is `null`, an `Insert` runs the statement +without a parameter. If the statement has parameters, such as `#{id}`, the producer fails with an +`IllegalArgumentException` instead of inserting a row where every parameter is `NULL`. == Examples diff --git a/components/camel-mybatis/src/main/docs/mybatis-component.adoc b/components/camel-mybatis/src/main/docs/mybatis-component.adoc index d84d40a70f92..434feb36377a 100644 --- a/components/camel-mybatis/src/main/docs/mybatis-component.adoc +++ b/components/camel-mybatis/src/main/docs/mybatis-component.adoc @@ -63,6 +63,11 @@ Camel will not replace the body. This allows you to continue routing and keep the original body. The response from MyBatis is stored in the header with the key `CamelMyBatisResult`, or in the header that the `outputHeader` option names, which leaves the body unchanged for a `SELECT` as well. +This is the same for an `InOnly` and an `InOut` exchange. + +When the body (or the header that the `inputHeader` option names) is `null`, an `Insert` runs the statement +without a parameter. If the statement has parameters, such as `#{id}`, the producer fails with an +`IllegalArgumentException` instead of inserting a row where every parameter is `NULL`. == Examples diff --git a/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisProducer.java b/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisProducer.java index be3320002846..6b39e71c0206 100644 --- a/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisProducer.java +++ b/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisProducer.java @@ -135,6 +135,7 @@ public class MyBatisProducer extends DefaultProducer { doProcessResult(exchange, result, session); } } else { + checkNoParameters(session); LOG.trace("Inserting using statement: {}", statement); result = session.insert(statement); doProcessResult(exchange, result, session); @@ -230,10 +231,8 @@ public class MyBatisProducer extends DefaultProducer { answer = exchange.getOut(); // preserve headers answer.getHeaders().putAll(exchange.getIn().getHeaders()); - if (outputHeader != null) { - //if we put the MyBatis result into a header we should preserve the body as well - answer.setBody(exchange.getIn().getBody()); - } + // preserve the body, which a SELECT replaces with the result below, unless the result goes into a header + answer.setBody(exchange.getIn().getBody()); } if (endpoint.getStatementType() == StatementType.SelectList || endpoint.getStatementType() == StatementType.SelectOne) { // we should not set the body if its a stored procedure as the result is already in its OUT parameter @@ -274,6 +273,20 @@ public class MyBatisProducer extends DefaultProducer { answer.setHeader(MyBatisConstants.MYBATIS_STATEMENT_NAME, statement); } + /** + * Fails when there is no input, but the statement has parameters, as MyBatis would otherwise run the statement with + * NULL for every parameter, such as inserting a row of nulls. + */ + private void checkNoParameters(SqlSession session) { + MappedStatement ms = session.getConfiguration().getMappedStatement(statement); + if (!ms.getBoundSql(null).getParameterMappings().isEmpty()) { + final String inputHeader = getEndpoint().getInputHeader(); + String input = inputHeader != null ? "header " + inputHeader : "message body"; + throw new IllegalArgumentException( + "The " + input + " is null, but the MyBatis statement " + statement + " has parameters"); + } + } + @Override public MyBatisEndpoint getEndpoint() { return (MyBatisEndpoint) super.getEndpoint(); diff --git a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInOutKeepsBodyTest.java b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInOutKeepsBodyTest.java new file mode 100644 index 000000000000..be8e388777c9 --- /dev/null +++ b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInOutKeepsBodyTest.java @@ -0,0 +1,127 @@ +/* + * 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.mybatis; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.camel.Exchange; +import org.apache.camel.ExchangePattern; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Insert, Update and Delete store the result in a header, so the body is kept, with InOut as well as with InOnly. + */ +public class MyBatisInOutKeepsBodyTest extends MyBatisTestSupport { + + @Test + public void testInsertKeepsBody() { + Account account = createAccount(444, "Willem"); + + Exchange out = request("mybatis:insertAccount?statementType=Insert", account); + + assertSame(account, out.getMessage().getBody()); + assertEquals(1, out.getMessage().getHeader(MyBatisConstants.MYBATIS_RESULT)); + assertEquals(3, countRows()); + } + + @Test + public void testInsertListKeepsBody() { + List<Account> accounts = Arrays.asList(createAccount(444, "Willem"), createAccount(555, "Donald")); + + Exchange out = request("mybatis:batchInsertAccount?statementType=InsertList", accounts); + + assertSame(accounts, out.getMessage().getBody()); + assertEquals(2, out.getMessage().getHeader(MyBatisConstants.MYBATIS_RESULT)); + assertEquals(4, countRows()); + } + + @Test + public void testUpdateKeepsBody() { + Account account = createAccount(123, "Jim"); + + Exchange out = request("mybatis:updateAccount?statementType=Update", account); + + assertSame(account, out.getMessage().getBody()); + assertEquals(1, out.getMessage().getHeader(MyBatisConstants.MYBATIS_RESULT)); + } + + @Test + public void testUpdateListKeepsBody() { + Map<String, Object> params = new HashMap<>(); + params.put("list", Arrays.asList(createAccount(123, "James"), createAccount(456, "Claus"))); + params.put("emailAddress", "[email protected]"); + + Exchange out = request("mybatis:batchUpdateAccount?statementType=UpdateList", params); + + assertSame(params, out.getMessage().getBody()); + assertEquals(2, out.getMessage().getHeader(MyBatisConstants.MYBATIS_RESULT)); + } + + @Test + public void testDeleteKeepsBody() { + Exchange out = request("mybatis:deleteAccountById?statementType=Delete", 456); + + assertEquals(456, out.getMessage().getBody()); + assertEquals(1, out.getMessage().getHeader(MyBatisConstants.MYBATIS_RESULT)); + assertEquals(1, countRows()); + } + + @Test + public void testDeleteListKeepsBody() { + List<Integer> ids = Arrays.asList(123, 456); + + Exchange out = request("mybatis:batchDeleteAccountById?statementType=DeleteList", ids); + + assertSame(ids, out.getMessage().getBody()); + assertEquals(2, out.getMessage().getHeader(MyBatisConstants.MYBATIS_RESULT)); + assertEquals(0, countRows()); + } + + @Test + public void testSelectOneSetsResultAsBody() { + Exchange out = request("mybatis:selectAccountById?statementType=SelectOne", 456); + + Account account = out.getMessage().getBody(Account.class); + assertEquals("Claus", account.getFirstName()); + } + + private Exchange request(String uri, Object body) { + Exchange out = template.send(uri, ExchangePattern.InOut, e -> e.getIn().setBody(body)); + assertNull(out.getException()); + return out; + } + + private int countRows() { + return template.requestBody("mybatis:count?statementType=SelectOne", null, Integer.class); + } + + private static Account createAccount(int id, String firstName) { + Account account = new Account(); + account.setId(id); + account.setFirstName(firstName); + account.setLastName("Last"); + account.setEmailAddress(firstName + "@example.com"); + return account; + } +} diff --git a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertNullBodyTest.java b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertNullBodyTest.java new file mode 100644 index 000000000000..2c5e327cbc3c --- /dev/null +++ b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertNullBodyTest.java @@ -0,0 +1,63 @@ +/* + * 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.mybatis; + +import org.apache.camel.CamelExecutionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MyBatisInsertNullBodyTest extends MyBatisTestSupport { + + @Test + public void testInsertNullBodyFailsWhenStatementHasParameters() { + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.sendBody("mybatis:insertAccount?statementType=Insert", null)); + + IllegalArgumentException cause = assertInstanceOf(IllegalArgumentException.class, e.getCause()); + assertEquals("The message body is null, but the MyBatis statement insertAccount has parameters", + cause.getMessage()); + // no row of nulls is inserted + assertEquals(2, countRows()); + } + + @Test + public void testInsertMissingInputHeaderFailsWhenStatementHasParameters() { + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.sendBody("mybatis:insertAccount?statementType=Insert&inputHeader=account", "ignored")); + + IllegalArgumentException cause = assertInstanceOf(IllegalArgumentException.class, e.getCause()); + assertEquals("The header account is null, but the MyBatis statement insertAccount has parameters", + cause.getMessage()); + assertEquals(2, countRows()); + } + + @Test + public void testInsertNullBodyWithoutParameters() { + template.sendBody("mybatis:insertDefaultAccount?statementType=Insert", null); + + assertEquals(3, countRows()); + Account account = template.requestBody("mybatis:selectAccountById?statementType=SelectOne", 999, Account.class); + assertEquals("Default", account.getFirstName()); + } + + private int countRows() { + return template.requestBody("mybatis:count?statementType=SelectOne", null, Integer.class); + } +} diff --git a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertWithRollbackTest.java b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertWithRollbackTest.java index 498ea8f8908e..120d2b57478e 100644 --- a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertWithRollbackTest.java +++ b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisInsertWithRollbackTest.java @@ -41,12 +41,16 @@ public class MyBatisInsertWithRollbackTest extends MyBatisTestSupport { @Test public void testInsert() throws Exception { + // an account without names violates the NOT NULL constraints + Account account = new Account(); + account.setId(444); + getMockEndpoint("mock:commit").expectedMessageCount(0); getMockEndpoint("mock:rollback").expectedMessageCount(1); - getMockEndpoint("mock:rollback").message(0).body().isEqualTo(null); + getMockEndpoint("mock:rollback").message(0).body().isEqualTo(account); getMockEndpoint("mock:rollback").message(0).header(Exchange.EXCEPTION_CAUGHT).isInstanceOf(PersistenceException.class); - template.sendBody("direct:start", null); + template.sendBody("direct:start", account); MockEndpoint.assertIsSatisfied(context); diff --git a/components/camel-mybatis/src/test/resources/org/apache/camel/component/mybatis/Account.xml b/components/camel-mybatis/src/test/resources/org/apache/camel/component/mybatis/Account.xml index 789547a47fe3..36743a0c18d0 100644 --- a/components/camel-mybatis/src/test/resources/org/apache/camel/component/mybatis/Account.xml +++ b/components/camel-mybatis/src/test/resources/org/apache/camel/component/mybatis/Account.xml @@ -139,6 +139,12 @@ aliases to match the properties of the target result class. --> delete from ACCOUNT where ACC_ID = #{id} </delete> + <!-- An insert without parameters --> + <insert id="insertDefaultAccount"> + insert into ACCOUNT (ACC_ID, ACC_FIRST_NAME, ACC_LAST_NAME, ACC_EMAIL) + values (999, 'Default', 'Account', '[email protected]') + </insert> + <!-- START SNIPPET: e1 --> <select id="selectUnprocessedAccounts" resultMap="AccountResult"> select * from ACCOUNT where PROCESSED = false diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 1bc188419d61..eee7a3bbdd9a 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -2487,6 +2487,16 @@ handshake; an IP address must be listed as a subject alternative name of the cer host name (such as `mllp://8888`) cannot pass the check. Use a certificate that matches the host of the endpoint, or set the new `hostnameVerification` option to `false` to keep the previous behaviour. +=== camel-mybatis - the body is kept with InOut, and an Insert without a body fails when the statement has parameters + +With an `InOut` exchange, the `Insert`, `InsertList`, `Update`, `UpdateList`, `Delete` and `DeleteList` statement +types now keep the message body, as they already did with `InOnly`. Before, the body was `null` after the call unless +the `outputHeader` option was set. The result is still in the `CamelMyBatisResult` header (or the `outputHeader`). + +An `Insert` with a `null` body (or a missing `inputHeader`) now fails with an `IllegalArgumentException` when the +statement has parameters. Before, MyBatis inserted a row where every parameter was `NULL`. A statement without +parameters still runs with a `null` body. + === camel-aws2-s3 - streaming upload with the random or timestamp naming strategy names the first file too With `streamingUploadMode=true` and `namingStrategy=random` (or `timestamp`), the first file uploaded by a producer was
