This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 7a48feff33a3 CAMEL-26145: camel-mybatis - Keep the body with InOut,
and fail an Insert, Update or Delete without a body when the statement has
parameters (#28916)
7a48feff33a3 is described below
commit 7a48feff33a363b02d257eaf20b40a8ed6e76c54
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:44:29 2026 +0100
CAMEL-26145: camel-mybatis - Keep the body with InOut, and fail an Insert,
Update or Delete without a body when the statement has parameters (#28916)
* CAMEL-26145: camel-mybatis - Keep the body with InOut, and fail an In…
…sert 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.
* CAMEL-26145: camel-mybatis - Fail an Update or Delete without a body …
…when the statement has parameters
Update and Delete with a null body (or a missing inputHeader) ran the
statement with NULL for every parameter, so an UPDATE without a WHERE set
columns to null in every row without an error. They now fail with the same
IllegalArgumentException as Insert; a statement without parameters still
runs.
* CAMEL-26145: camel-mybatis - escape attribute references in the docs
Asciidoctor read ${...} in inline code as a document attribute reference,
which failed the
PR doc validation. Use the +...+ passthrough so the text shows as written.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/catalog/docs/mybatis-component.adoc | 6 +
.../src/main/docs/mybatis-component.adoc | 6 +
.../camel/component/mybatis/MyBatisProducer.java | 23 +++-
.../mybatis/MyBatisInOutKeepsBodyTest.java | 127 +++++++++++++++++++++
.../mybatis/MyBatisInsertWithRollbackTest.java | 8 +-
.../component/mybatis/MyBatisNullBodyTest.java | 102 +++++++++++++++++
.../org/apache/camel/component/mybatis/Account.xml | 15 +++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++
8 files changed, 292 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..e854dab02fd2 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,12 @@ 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`, `Update` or `Delete`
+runs the statement without a parameter. If the statement has parameters, such
as `+#{id}+`, the producer fails with an
+`IllegalArgumentException` instead of running the statement with `NULL` for
every parameter, which would insert a row
+of nulls, or set columns to null in every row an `UPDATE` without a `WHERE`
matches.
== 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..e854dab02fd2 100644
--- a/components/camel-mybatis/src/main/docs/mybatis-component.adoc
+++ b/components/camel-mybatis/src/main/docs/mybatis-component.adoc
@@ -63,6 +63,12 @@ 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`, `Update` or `Delete`
+runs the statement without a parameter. If the statement has parameters, such
as `+#{id}+`, the producer fails with an
+`IllegalArgumentException` instead of running the statement with `NULL` for
every parameter, which would insert a row
+of nulls, or set columns to null in every row an `UPDATE` without a `WHERE`
matches.
== 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..7bd80827762f 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);
@@ -168,6 +169,7 @@ public class MyBatisProducer extends DefaultProducer {
doProcessResult(exchange, result, session);
}
} else {
+ checkNoParameters(session);
LOG.trace("Updating using statement: {}", statement);
result = session.update(statement);
doProcessResult(exchange, result, session);
@@ -201,6 +203,7 @@ public class MyBatisProducer extends DefaultProducer {
doProcessResult(exchange, result, session);
}
} else {
+ checkNoParameters(session);
LOG.trace("Deleting using statement: {}", statement);
result = session.delete(statement);
doProcessResult(exchange, result, session);
@@ -230,10 +233,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 +275,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 or updating
every row to 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/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/java/org/apache/camel/component/mybatis/MyBatisNullBodyTest.java
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisNullBodyTest.java
new file mode 100644
index 000000000000..d5781cceedca
--- /dev/null
+++
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisNullBodyTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 MyBatisNullBodyTest 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());
+ }
+
+ @Test
+ public void testUpdateNullBodyFailsWhenStatementHasParameters() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () ->
template.sendBody("mybatis:updateAccount?statementType=Update", null));
+
+ IllegalArgumentException cause =
assertInstanceOf(IllegalArgumentException.class, e.getCause());
+ assertEquals("The message body is null, but the MyBatis statement
updateAccount has parameters",
+ cause.getMessage());
+ // the accounts are not updated
+ Account account =
template.requestBody("mybatis:selectAccountById?statementType=SelectOne", 123,
Account.class);
+ assertEquals("James", account.getFirstName());
+ }
+
+ @Test
+ public void testUpdateNullBodyWithoutParameters() {
+ template.sendBody("mybatis:updateAllEmails?statementType=Update",
null);
+
+ Account account =
template.requestBody("mybatis:selectAccountById?statementType=SelectOne", 456,
Account.class);
+ assertEquals("[email protected]", account.getEmailAddress());
+ }
+
+ @Test
+ public void testDeleteMissingInputHeaderFailsWhenStatementHasParameters() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () ->
template.sendBody("mybatis:deleteAccountById?statementType=Delete&inputHeader=id",
123));
+
+ IllegalArgumentException cause =
assertInstanceOf(IllegalArgumentException.class, e.getCause());
+ assertEquals("The header id is null, but the MyBatis statement
deleteAccountById has parameters",
+ cause.getMessage());
+ assertEquals(2, countRows());
+ }
+
+ @Test
+ public void testDeleteNullBodyWithoutParameters() {
+ template.sendBody("mybatis:deleteAllAccounts?statementType=Delete",
null);
+
+ assertEquals(0, countRows());
+ }
+
+ private int countRows() {
+ return template.requestBody("mybatis:count?statementType=SelectOne",
null, Integer.class);
+ }
+}
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..8e8631098ae0 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,21 @@ 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>
+
+ <!-- An update and a delete without parameters -->
+ <update id="updateAllEmails">
+ update ACCOUNT set ACC_EMAIL = '[email protected]'
+ </update>
+
+ <delete id="deleteAllAccounts">
+ delete from ACCOUNT
+ </delete>
+
<!-- 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 96c851592919..5c38a93ddb1e 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
@@ -2617,6 +2617,17 @@ 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, Update or
Delete 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`, `Update` or `Delete` with a `null` body (or a missing
`inputHeader`) now fails with an
+`IllegalArgumentException` when the statement has parameters. Before, MyBatis
ran the statement with `NULL` for every
+parameter: an `Insert` stored a row of nulls, and an `Update` without a
`WHERE` set columns to null in every row. 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