This is an automated email from the ASF dual-hosted git repository.

jungm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/main by this push:
     new c292cb662c TOMEE-4711 use the TransactionManager in Jakarta Data 
repositories so they work from @Transactional methods (#2950)
c292cb662c is described below

commit c292cb662ccfe54c25d680a4040dd8cf0ca33622
Author: Markus Jung <[email protected]>
AuthorDate: Fri Sep 25 20:14:59 2026 +0200

    TOMEE-4711 use the TransactionManager in Jakarta Data repositories so they 
work from @Transactional methods (#2950)
---
 .../tests/data/JakartaDataTransactionalTest.java   | 107 +++++++++++++++++++++
 .../tests/data/TransactionalItemService.java       |  58 +++++++++++
 .../data/handler/RepositoryInvocationHandler.java  |  30 ++----
 3 files changed, 174 insertions(+), 21 deletions(-)

diff --git 
a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataTransactionalTest.java
 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataTransactionalTest.java
new file mode 100644
index 0000000000..01d10d7620
--- /dev/null
+++ 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataTransactionalTest.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.openejb.arquillian.tests.data;
+
+import jakarta.inject.Inject;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verifies repository methods work when called from a CDI {@code 
@Transactional} method.
+ *
+ * The {@code @Transactional} interceptor makes every {@code UserTransaction} 
call throw
+ * {@code IllegalStateException} for the duration of the method, so the 
repository has to join
+ * the active transaction without going through {@code UserTransaction}.
+ */
+@RunWith(Arquillian.class)
+public class JakartaDataTransactionalTest {
+
+    private static final String PERSISTENCE_XML = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <persistence xmlns="https://jakarta.ee/xml/ns/persistence"; 
version="3.2">
+            <persistence-unit name="data-pu" transaction-type="JTA">
+                <jta-data-source>java:comp/DefaultDataSource</jta-data-source>
+                
<class>org.apache.openejb.arquillian.tests.data.SimpleItem</class>
+                <exclude-unlisted-classes>true</exclude-unlisted-classes>
+                <properties>
+                    <property 
name="jakarta.persistence.schema-generation.database.action"
+                              value="create"/>
+                    <property name="openjpa.jdbc.SynchronizeMappings"
+                              value="buildSchema(ForeignKeys=true)"/>
+                </properties>
+            </persistence-unit>
+        </persistence>
+        """;
+
+    private static final String BEANS_XML = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <beans xmlns="https://jakarta.ee/xml/ns/jakartaee"; version="4.0"
+               bean-discovery-mode="all">
+        </beans>
+        """;
+
+    @Deployment
+    public static WebArchive createDeployment() {
+        return ShrinkWrap.create(WebArchive.class, "data-transactional.war")
+            .addClass(SimpleItem.class)
+            .addClass(SimpleItemRepository.class)
+            .addClass(TransactionalItemService.class)
+            .addAsResource(new StringAsset(PERSISTENCE_XML), 
"META-INF/persistence.xml")
+            .addAsWebInfResource(new StringAsset(BEANS_XML), "beans.xml");
+    }
+
+    @Inject
+    private TransactionalItemService service;
+
+    @Inject
+    private SimpleItemRepository repository;
+
+    @Test
+    public void insertAndFindInsideTransactional() {
+        final Long id = service.insert("tx-insert");
+        assertNotNull("Item should have an ID after insert", id);
+
+        assertEquals("tx-insert", service.findLabel(id));
+    }
+
+    @Test
+    public void saveManagedEntityInsideTransactional() {
+        final Long id = service.insert("tx-before");
+
+        service.rename(id, "tx-after");
+
+        assertEquals("tx-after", 
repository.findById(id).orElseThrow().getLabel());
+    }
+
+    @Test
+    public void deleteByIdInsideTransactional() {
+        final Long id = service.insert("tx-delete");
+
+        service.delete(id);
+
+        assertTrue("Deleted item should not be found", 
repository.findById(id).isEmpty());
+    }
+}
diff --git 
a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/TransactionalItemService.java
 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/TransactionalItemService.java
new file mode 100644
index 0000000000..b63ba9e7a7
--- /dev/null
+++ 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/TransactionalItemService.java
@@ -0,0 +1,58 @@
+/*
+ * 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.openejb.arquillian.tests.data;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+
+/**
+ * Calls {@link SimpleItemRepository} from inside CDI {@code @Transactional} 
methods, where the
+ * JTA spec forbids any use of {@code UserTransaction}.
+ */
+@ApplicationScoped
+public class TransactionalItemService {
+
+    @Inject
+    private SimpleItemRepository repository;
+
+    @Transactional
+    public Long insert(final String label) {
+        return repository.insert(new SimpleItem(label)).getId();
+    }
+
+    @Transactional
+    public String findLabel(final Long id) {
+        return repository.findById(id).map(SimpleItem::getLabel).orElse(null);
+    }
+
+    /**
+     * Loads the entity and saves it again within one transaction, so the 
instance handed to
+     * {@code save} is the managed one {@code findById} returned.
+     */
+    @Transactional
+    public void rename(final Long id, final String label) {
+        final SimpleItem item = repository.findById(id).orElseThrow();
+        item.setLabel(label);
+        repository.save(item);
+    }
+
+    @Transactional
+    public void delete(final Long id) {
+        repository.deleteById(id);
+    }
+}
diff --git 
a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java
 
b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java
index 9d8b797e4c..46869b2625 100644
--- 
a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java
+++ 
b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java
@@ -30,7 +30,7 @@ import jakarta.data.repository.Update;
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.TypedQuery;
 import jakarta.transaction.Status;
-import jakarta.transaction.UserTransaction;
+import jakarta.transaction.TransactionManager;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.ConstraintViolationException;
 import jakarta.validation.executable.ExecutableValidator;
@@ -43,9 +43,7 @@ import org.apache.openejb.data.query.CriteriaQueryBuilder;
 import org.apache.openejb.data.query.FindAnnotationExecutor;
 import org.apache.openejb.data.query.MethodNameParser;
 import org.apache.openejb.loader.SystemInstance;
-import org.apache.openejb.spi.ContainerSystem;
 
-import javax.naming.NamingException;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -167,25 +165,27 @@ public class RepositoryInvocationHandler implements 
InvocationHandler {
     }
 
     private Object executeInTransaction(final EntityManager em, final Method 
method, final Object[] args) throws Exception {
-        final UserTransaction ut = lookupUserTransaction();
-        if (ut == null) {
+        // The container TransactionManager, not UserTransaction: a CDI 
@Transactional method forbids any UserTransaction use
+        // for its whole duration, and repositories are routinely called from 
exactly those methods.
+        final TransactionManager tm = 
SystemInstance.get().getComponent(TransactionManager.class);
+        if (tm == null) {
             return doInvoke(em, method, args);
         }
 
-        final boolean startedTx = ut.getStatus() != Status.STATUS_ACTIVE;
+        final boolean startedTx = tm.getStatus() == 
Status.STATUS_NO_TRANSACTION;
         if (startedTx) {
-            ut.begin();
+            tm.begin();
         }
         try {
             final Object result = doInvoke(em, method, args);
             if (startedTx) {
-                ut.commit();
+                tm.commit();
             }
             return result;
         } catch (final Exception e) {
             if (startedTx) {
                 try {
-                    ut.rollback();
+                    tm.rollback();
                 } catch (final Exception rollbackEx) {
                     LOGGER.log(Level.WARNING, "Rollback failed", rollbackEx);
                 }
@@ -194,18 +194,6 @@ public class RepositoryInvocationHandler implements 
InvocationHandler {
         }
     }
 
-    private UserTransaction lookupUserTransaction() {
-        try {
-            final ContainerSystem containerSystem = 
SystemInstance.get().getComponent(ContainerSystem.class);
-            if (containerSystem != null) {
-                return (UserTransaction) 
containerSystem.getJNDIContext().lookup("comp/UserTransaction");
-            }
-        } catch (final NamingException e) {
-            LOGGER.log(Level.FINE, "UserTransaction not available via JNDI", 
e);
-        }
-        return null;
-    }
-
     private Object doInvoke(final EntityManager em, final Method method, final 
Object[] args) {
         // Check built-in method names from BasicRepository/CrudRepository 
FIRST
         // These have well-known semantics and their inherited annotations 
(like @Find on findById)

Reply via email to