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)