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 28000bbe7c86 CAMEL-26851: camel-main - transacted with a local 
transaction manager when the application has one DataSource (#29629)
28000bbe7c86 is described below

commit 28000bbe7c867360560a446e88e0a406ef2b1725
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 16:13:23 2026 +0100

    CAMEL-26851: camel-main - transacted with a local transaction manager when 
the application has one DataSource (#29629)
    
    A route that uses transacted needs a transaction manager. Spring Boot and 
Quarkus set one up; Camel Main and camel run do not, and with spring-tx on the 
classpath the route failed to start: no PlatformTransactionManager in the 
registry. When there is none, but the registry has exactly one DataSource and 
spring-jdbc is on the classpath, Camel now creates a JdbcTransactionManager for 
that DataSource, binds it as transactionManager for the other transacted 
routes, and makes the SpringTra [...]
    
    camel run bound a placeholder policy for transacted, which runs the steps 
without a transaction (CAMEL-26850). In the same case it now downloads 
camel-spring instead, and Camel makes the local transaction manager for the 
DataSource. The placeholder and its WARN stay for the other cases; the WARN now 
says what gives transactions.
    
    The transactional client page explains the convention, and that the SQL and 
Spring JDBC components take part in it while the JDBC component does not.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/catalog/docs/transactional-client.adoc   |   7 ++
 .../SqlTransactedLocalTransactionManagerTest.java  | 107 +++++++++++++++++++++
 .../modules/eips/pages/transactional-client.adoc   |   7 ++
 .../camel/reifier/AbstractPolicyReifier.java       |  46 ++++++++-
 dsl/camel-kamelet-main/pom.xml                     |   6 ++
 .../camel/main/download/TransactedDownloader.java  |  29 +++++-
 .../main/download/TransactedDownloaderTest.java    |  32 ++++++
 7 files changed, 230 insertions(+), 4 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/transactional-client.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/transactional-client.adoc
index 8ec329fb2c66..527f615754f1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/transactional-client.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/transactional-client.adoc
@@ -188,6 +188,13 @@ manager configured. In more complex scenarios, where you 
either use multiple
 transaction managers or transaction propagation policies, you have to do 
additional
 configuration.
 
+When there is no transaction manager at all, as with Camel Main and the Camel 
CLI (`camel run`), and the
+application has exactly one `DataSource`, Camel creates a local 
`JdbcTransactionManager` for that `DataSource`
+and binds it as `transactionManager`. This needs `camel-spring` and 
`spring-jdbc` on the classpath. The
+xref:ROOT:sql-component.adoc[SQL] and 
xref:ROOT:spring-jdbc-component.adoc[Spring JDBC] components take part in
+the transaction, as they get their connections through Spring. The 
xref:ROOT:jdbc-component.adoc[JDBC] component
+takes its connections from the `DataSource` directly, so it does not.
+
 === Using different transaction propagations
 
 In some rare situations, you may need to use multiple transactions with the 
same exchange.
diff --git 
a/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlTransactedLocalTransactionManagerTest.java
 
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlTransactedLocalTransactionManagerTest.java
new file mode 100644
index 000000000000..67dbfb0cd201
--- /dev/null
+++ 
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlTransactedLocalTransactionManagerTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+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.JdbcTransactionManager;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-26851: transacted without a transaction manager or a policy in the 
registry, as with Camel Main and camel run,
+ * uses a local transaction manager for the one DataSource of the application.
+ */
+public class SqlTransactedLocalTransactionManagerTest extends CamelTestSupport 
{
+
+    private static final String SQL = 
"sql:overriddenByTheHeader?dataSource=#testdb&allowQueryFromHeader=true";
+
+    private EmbeddedDatabase db;
+    private JdbcTemplate jdbc;
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        // only the DataSource: no transaction manager and no transacted policy
+        db = new EmbeddedDatabaseBuilder()
+                .setName(getClass().getSimpleName())
+                .setType(EmbeddedDatabaseType.H2).build();
+        registry.bind("testdb", db);
+    }
+
+    @Override
+    public void doPostSetup() {
+        jdbc = new JdbcTemplate(db);
+        jdbc.execute("CREATE TABLE CUSTOMER (ID VARCHAR(15) NOT NULL PRIMARY 
KEY, NAME VARCHAR(100))");
+    }
+
+    @Override
+    public void doPostTearDown() {
+        if (db != null) {
+            db.shutdown();
+        }
+    }
+
+    private void insertTwice(String routeId, String second) throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:" + routeId).routeId(routeId)
+                        .transacted()
+                        .to(SQL)
+                        .setHeader(SqlConstants.SQL_QUERY, constant(second))
+                        .to(SQL);
+            }
+        });
+    }
+
+    private Exchange send(String routeId) {
+        return template.send("direct:" + routeId,
+                e -> e.getIn().setHeader(SqlConstants.SQL_QUERY, "insert into 
customer values('cust1','cmueller')"));
+    }
+
+    @Test
+    public void bothInsertsCommit() throws Exception {
+        insertTwice("commit", "insert into customer 
values('cust2','muellerc')");
+
+        assertFalse(send("commit").isFailed());
+        assertEquals(2, jdbc.queryForObject("select count(*) from customer", 
Long.class));
+        // the local transaction manager is bound, so other transacted routes 
use the same one
+        Object tm = context.getRegistry().lookupByName("transactionManager");
+        JdbcTransactionManager local = 
assertInstanceOf(JdbcTransactionManager.class, tm);
+        assertSame(db, local.getDataSource());
+    }
+
+    @Test
+    public void aFailingSecondInsertRollsBackTheFirst() throws Exception {
+        // primary key violation
+        insertTwice("rollback", "insert into customer 
values('cust1','muellerc')");
+
+        assertTrue(send("rollback").isFailed());
+        assertEquals(0, jdbc.queryForObject("select count(*) from customer", 
Long.class));
+    }
+}
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/transactional-client.adoc
 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/transactional-client.adoc
index 8ec329fb2c66..527f615754f1 100644
--- 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/transactional-client.adoc
+++ 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/transactional-client.adoc
@@ -188,6 +188,13 @@ manager configured. In more complex scenarios, where you 
either use multiple
 transaction managers or transaction propagation policies, you have to do 
additional
 configuration.
 
+When there is no transaction manager at all, as with Camel Main and the Camel 
CLI (`camel run`), and the
+application has exactly one `DataSource`, Camel creates a local 
`JdbcTransactionManager` for that `DataSource`
+and binds it as `transactionManager`. This needs `camel-spring` and 
`spring-jdbc` on the classpath. The
+xref:ROOT:sql-component.adoc[SQL] and 
xref:ROOT:spring-jdbc-component.adoc[Spring JDBC] components take part in
+the transaction, as they get their connections through Spring. The 
xref:ROOT:jdbc-component.adoc[JDBC] component
+takes its connections from the `DataSource` directly, so it does not.
+
 === Using different transaction propagations
 
 In some rare situations, you may need to use multiple transactions with the 
same exchange.
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractPolicyReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractPolicyReifier.java
index 960d3299e4d6..6680be8a5011 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractPolicyReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractPolicyReifier.java
@@ -17,6 +17,7 @@
 package org.apache.camel.reifier;
 
 import java.lang.reflect.Method;
+import java.util.Set;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Route;
@@ -79,7 +80,16 @@ public abstract class AbstractPolicyReifier<T extends 
ProcessorDefinition<?>> ex
             if (tmClazz != null) {
                 // see if we can find the platform transaction manager in the
                 // registry
-                Object transactionManager = mandatoryFindSingleByType(tmClazz);
+                Object transactionManager = findSingleByType(tmClazz);
+                if (transactionManager == null) {
+                    // none, but the application has one DataSource (Camel 
Main, camel run): a local transaction
+                    // manager for it
+                    transactionManager = localTransactionManager(tmClazz);
+                }
+                if (transactionManager == null) {
+                    // fails telling that there is no 
PlatformTransactionManager
+                    transactionManager = mandatoryFindSingleByType(tmClazz);
+                }
                 // only one platform manager then use it as default and
                 // create a transacted
                 // policy with it and default to required
@@ -119,4 +129,38 @@ public abstract class AbstractPolicyReifier<T extends 
ProcessorDefinition<?>> ex
 
         return answer;
     }
+
+    /**
+     * A local transaction manager for the one DataSource of the registry, 
when the registry has no transaction manager,
+     * as with Camel Main and camel run (CAMEL-26851). It is bound as 
transactionManager, so the other transacted routes
+     * use it too. Null without exactly one DataSource, or without spring-jdbc 
on the classpath.
+     */
+    private Object localTransactionManager(Class<?> tmClazz) {
+        Class<?> dsClazz = 
camelContext.getClassResolver().resolveClass("javax.sql.DataSource");
+        Class<?> jtmClazz = camelContext.getClassResolver()
+                
.resolveClass("org.springframework.jdbc.support.JdbcTransactionManager");
+        if (dsClazz == null || jtmClazz == null || 
!tmClazz.isAssignableFrom(jtmClazz)) {
+            return null;
+        }
+        Set<?> dataSources = camelContext.getRegistry().findByType(dsClazz);
+        if (dataSources.size() != 1) {
+            return null;
+        }
+        Object dataSource = dataSources.iterator().next();
+        Object transactionManager;
+        try {
+            transactionManager = 
jtmClazz.getConstructor(dsClazz).newInstance(dataSource);
+        } catch (Exception e) {
+            throw new RuntimeCamelException("Cannot create a 
JdbcTransactionManager for the DataSource: " + dataSource, e);
+        }
+        if (camelContext.getRegistry().lookupByName("transactionManager") == 
null) {
+            camelContext.getRegistry().bind("transactionManager", 
transactionManager);
+            LOG.info("Using a local JdbcTransactionManager for transacted 
routes, with the DataSource: {}", dataSource);
+        } else {
+            LOG.debug("Using a local JdbcTransactionManager for this 
transacted route only, with the DataSource: {},"
+                      + " as another bean is bound as transactionManager",
+                    dataSource);
+        }
+        return transactionManager;
+    }
 }
diff --git a/dsl/camel-kamelet-main/pom.xml b/dsl/camel-kamelet-main/pom.xml
index f09d2aa98791..c981972f75d2 100644
--- a/dsl/camel-kamelet-main/pom.xml
+++ b/dsl/camel-kamelet-main/pom.xml
@@ -183,6 +183,12 @@
         </dependency>
 
         <!-- test -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <version>${spring-version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
index a455a36c1359..c4934de8f436 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.main.download;
 
+import javax.sql.DataSource;
+
 import org.apache.camel.CamelContext;
 import org.apache.camel.NamedNode;
 import org.apache.camel.Processor;
@@ -59,10 +61,16 @@ public class TransactedDownloader {
                         if (downloader != null) {
                             downloader.downloadDependency("org.apache.camel", 
"camel-jta",
                                     route.getCamelContext().getVersion());
-                            if 
(bindPassThroughPolicies(route.getCamelContext(), transacted)) {
+                            if 
(usesLocalTransactionManager(route.getCamelContext(), transacted)) {
+                                // CAMEL-26851: Camel makes a local 
transaction manager for the one DataSource, and the
+                                // policy of it, which is in camel-spring
+                                
downloader.downloadDependency("org.apache.camel", "camel-spring",
+                                        route.getCamelContext().getVersion());
+                            } else if 
(bindPassThroughPolicies(route.getCamelContext(), transacted)) {
                                 LOG.warn("Route {} uses transacted, but there 
is no transaction manager: the route runs"
-                                         + " without a transaction. Declare a 
transaction manager bean, or export to"
-                                         + " Spring Boot or Quarkus, for 
transactions.",
+                                         + " without a transaction. Declare 
one DataSource (spring.datasource.*) with"
+                                         + " spring-jdbc on the classpath, as 
the sql component has, or a transaction"
+                                         + " manager bean, or export to Spring 
Boot or Quarkus, for transactions.",
                                         route.getRouteId());
                             }
                         }
@@ -92,6 +100,21 @@ public class TransactedDownloader {
         return true;
     }
 
+    /**
+     * Whether Camel makes a local transaction manager for the transacted 
(CAMEL-26851): it has no policy or reference
+     * of its own, the registry has no transaction manager but exactly one 
DataSource, and spring-jdbc is on the
+     * classpath.
+     */
+    static boolean usesLocalTransactionManager(CamelContext context, 
TransactedDefinition transacted) {
+        if (transacted.getPolicy() != null || transacted.getRef() != null || 
hasTransactionManager(context)) {
+            return false;
+        }
+        // with the application class loader, as for the 
PlatformTransactionManager below
+        Class<?> jtm = 
ObjectHelper.loadClass("org.springframework.jdbc.support.JdbcTransactionManager",
+                context.getApplicationContextClassLoader());
+        return jtm != null && 
context.getRegistry().findByType(DataSource.class).size() == 1;
+    }
+
     /**
      * Whether the registry has a transacted policy of the user's own, or a 
Spring PlatformTransactionManager, of which
      * Camel makes the policy.
diff --git 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/TransactedDownloaderTest.java
 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/TransactedDownloaderTest.java
index bd4e3e007516..ca7da114c7b2 100644
--- 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/TransactedDownloaderTest.java
+++ 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/TransactedDownloaderTest.java
@@ -26,6 +26,7 @@ import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.model.TransactedDefinition;
 import org.apache.camel.spi.TransactedPolicy;
 import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -82,6 +83,37 @@ public class TransactedDownloaderTest {
         }
     }
 
+    @Test
+    public void oneDataSourceWithSpringJdbcGetsALocalTransactionManager() 
throws Exception {
+        // CAMEL-26851: Camel makes a local transaction manager for it, so no 
placeholder is bound
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getRegistry().bind("orders", new 
DriverManagerDataSource("jdbc:h2:mem:orders"));
+            
assertTrue(TransactedDownloader.usesLocalTransactionManager(context, new 
TransactedDefinition()));
+        }
+    }
+
+    @Test
+    public void noLocalTransactionManagerWithoutExactlyOneDataSource() throws 
Exception {
+        try (CamelContext context = new DefaultCamelContext()) {
+            
assertFalse(TransactedDownloader.usesLocalTransactionManager(context, new 
TransactedDefinition()));
+            context.getRegistry().bind("orders", new 
DriverManagerDataSource("jdbc:h2:mem:orders"));
+            context.getRegistry().bind("audit", new 
DriverManagerDataSource("jdbc:h2:mem:audit"));
+            
assertFalse(TransactedDownloader.usesLocalTransactionManager(context, new 
TransactedDefinition()));
+        }
+    }
+
+    @Test
+    public void noLocalTransactionManagerForAPolicyOfItsOwn() throws Exception 
{
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getRegistry().bind("orders", new 
DriverManagerDataSource("jdbc:h2:mem:orders"));
+            TransactedDefinition transacted = new TransactedDefinition();
+            transacted.setRef("myPolicy");
+            
assertFalse(TransactedDownloader.usesLocalTransactionManager(context, 
transacted));
+            context.getRegistry().bind("PROPAGATION_REQUIRED", new 
CountingPolicy());
+            
assertFalse(TransactedDownloader.usesLocalTransactionManager(context, new 
TransactedDefinition()));
+        }
+    }
+
     private static RouteBuilder orders() {
         return new RouteBuilder() {
             @Override

Reply via email to