24

我正在开发一个需要访问两个不同数据库服务器(H2 和 Oracle)的 webapp。该容器是一个Apache Tomee 1.5.1,我正在使用 Java EE 堆栈以及其中提供的库(JSF、JPA、CDI、EJB 等)。

我试图在 XA 事务中使用两个实体管理器从 Oracle 数据库中提取数据并在转换后将其保存在 H2 中,但是无论我使用什么实体管理器,所有查询都是针对 H2 数据库执行的。有什么帮助吗?

编辑:我发现如果我尝试以相反的顺序访问实体管理器,它们的行为是相同的,但访问的是 Oracle。即:实体管理器与访问的第一个数据库保持一致。

发生这种情况的 EJB(service.getFoo()从 JSF 调用):

@Named
@Stateless
public class Service {
    @Inject
    @OracleDatabase
    private EntityManager emOracle;

    @Inject
    @H2Database
    private EntityManager emH2;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Foo> getFoo() {
        TypedQuery<Foo> q = emH2.createQuery(
                "SELECT x FROM Foo f", Foo.class);
        List<Foo> l = q.getResultList();
        if (l == null || l.isEmpty()) {
            update();
        }

        return q.getResultList();
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void update() {
        // FAIL: This query executes against H2 with Oracle entity manager!
        List<Object[]> l = emOracle.createNativeQuery("SELECT * FROM bar ").getResultList(); 

        //more stuff...
    }
}

实体管理器的资源生产者 (CDI)(其中 @H2Database 和 @OracleDatabase 是限定符):

public class Resources {
    @Produces
    @PersistenceContext(unitName = "OraclePU")
    @OracleDatabase
    private EntityManager emOracle;

    @Produces
    @PersistenceContext(unitName = "H2PU")
    @H2Database
    private EntityManager emH2;
}

我的peristence.xml看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="H2PU"
        transaction-type="JTA">
        <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
        <jta-data-source>H2DS</jta-data-source>
        <class>my.app.h2.Foo</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
    </persistence-unit>

    <persistence-unit name="OraclePU" transaction-type="JTA">
        <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
        <jta-data-source>OracleDS</jta-data-source>
        <class>my.app.oracle.Bar</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
    </persistence-unit>
</persistence>

最后,tomee.xml中的数据源(该文件中没有配置任何其他数据源):

<Resource id="OracleDS" type="javax.sql.DataSource">
    jdbcDriver = oracle.jdbc.xa.client.OracleXADataSource
    jdbcUrl = jdbc:oracle:thin:@server:port:instance
    jtaManaged = true
    password = abcde
    userName = user
</Resource>

<Resource id="H2DS" type="javax.sql.DataSource">
    jdbcDriver=org.h2.jdbcx.JdbcDataSource
    jdbcUrl=jdbc:h2:h2/db;AUTO_SERVER=TRUE
    jtaManaged = true
    password = edcba
    userName = user
</Resource>
4

2 回答 2

45

容器管理的持久性上下文

当使用容器管理的持久性上下文时(就像您通过 @PersistenceContext 注释一样),JPA 规范指定只有一个持久性上下文可以与 JTA 事务相关联。

持久性上下文由 Java EE 容器创建。尽管出现了代码(@PersistenceContext 注释似乎表明 PC 被直接注入到您的 EntityManager 实例变量中),但持久性上下文实际上存储为 JTA 事务中的引用。每次 EntityManager 操作发生时,它都不会引用它自己的内部持久性上下文。相反,它执行一个特殊操作,因为它是容器管理的——它总是在 JTA 事务中查找持久性上下文并使用它。这称为 JTA 持久性上下文传播。

JPA 规范中的一些引用:

当使用容器管理的实体管理器时,持久性上下文的生命周期总是自动管理,对应用程序透明,并且持久性上下文通过 JTA 事务传播。

容器管理的事务范围持久性上下文

... 当容器管理的实体管理器在活动 JTA 事务的范围内被调用 [76] 时,一个新的持久性上下文开始,并且没有与 JTA 事务相关联的当前持久性上下文。创建持久性上下文,然后将其与 JTA 事务相关联。

容器管理的扩展持久性上下文

...容器管理的扩展持久性上下文只能在有状态会话 bean 的范围内启动。它从创建声明对 PersistenceContextType.EXTENDED 类型的实体管理器的依赖关系的有状态会话 bean 开始存在,并且被称为绑定到有状态会话 bean。通过 PersistenceContext 注释或 persistence-context-ref 部署描述符元素声明对扩展持久性上下文的依赖。当有状态会话 bean 的 @Remove 方法完成时(或者有状态会话 bean 实例被销毁),容器将关闭持久性上下文。

持久化上下文传播的要求

...如果调用了一个组件并且没有 JTA 事务...,则不会传播持久性上下文。• 调用使用 PersistenceContextType.TRANSACTION 定义的实体管理器将导致使用新的持久性上下文。• 调用使用 PersistenceContextType.EXTENDED 定义的实体管理器将导致使用绑定到该组件的现有扩展持久性上下文。

... 如果调用了一个组件并将 JTA 事务传播到该组件中: • 如果该组件是一个有状态会话 bean,扩展的持久性上下文已绑定到该 bean,并且有一个不同的持久性上下文绑定到 JTA 事务,则EJBException 由容器引发。• 否则,如果存在绑定到 JTA 事务的持久性上下文,则传播并使用该持久性上下文。

所以这是你的问题。显而易见的 64 美元问题:为什么规范要求这个???

嗯,这是因为它是一种经过深思熟虑的权衡,它为 EJB 带来了强大的 EntityManager 魔法。

使用 JTA 事务传播单个持久性上下文有一个限制:事务不能跨越多个持久性上下文,因此不能跨越多个数据库。

但是,它也有一个巨大的优势:在 EJB 中声明的任何 entityManager 都可以自动共享相同的持久性上下文,因此可以对同一组 JPA 实体进行操作并参与同一事务。您可以有一个 EJB 链调用任何复杂性的其他 EJB,并且它们都针对 JPA 实体数据进行明智且一致的行为。而且它们也不需要跨方法调用一致地初始化/共享实体管理器引用的复杂性——可以在每个方法中私有地声明 EntityManagers。实现逻辑可以非常简单。

问题的答案:使用应用程序管理的持久性上下文(通过应用程序管理的 EntityManagers

通过以下方法之一声明您的 entityManager:

// "Java EE style" declaration of EM
@PersistenceUnit(unitName="H2PU")
EntityManagerFactory emfH2;
EntityManager emH2 = emfH2.createEntityManager();

或者

// "JSE style" declaration of EM
EntityManagerFactory emfH2 = javax.persistence.Persistence.createEntityManagerFactory("H2PU");
EntityManager emH2 = emfH2.createEntityManager();

and the same for emfOracle & emOracle.    

完成每个 EM 后,您必须调用 em.close() - 最好通过 final { } 子句或通过 Java 7 try-with-resources 语句。

应用程序管理的 EM 仍然参与(即同步)JTA 事务。任何数量的应用程序管理的 EM 都可以参与单个 JTA 事务——但这些都不会将它们的持久性上下文与任何容器管理的 EM 关联或传播到任何容器管理的 EM中。

如果 EntityManager 是在 JTA 事务的上下文之外创建的(在事务开始之前),那么您必须明确要求它加入 JTA 事务:

// must be run from within Java EE code scope that already has a JTA 
// transaction active:
em.joinTransaction();  

或者更简单,如果 EntityManager 是在 JTA 事务的上下文中创建的,那么应用程序管理的 EntityManager 会自动隐式加入 JTA 事务 - 不需要 joinTransaction()。

因此,应用程序管理的 EM 可以有一个跨越多个数据库的 JTA 事务。当然,您总是可以运行独立于 JTA 的本地资源 JDBC 事务:

EntityTransaction tx = em.getTransaction();  
tx.begin();

// ....

tx.commit();

编辑:使用应用程序管理的实体管理器进行事务管理的额外细节

警告:下面的代码示例用于教育用途 - 我已经将它们从头顶输入以帮助解释我的观点并且没有时间编译/调试/测试。

EJB 的默认@TransactionManagement 参数是TransactionManagement.CONTAINER,EJB 方法的默认@TransactionAttribute 参数是TransactionAttribute.REQUIRED。

事务管理有四种排列方式:

  • A) 带有 CONTAINER 管理的 JTA 事务的 EJB

    这是首选的 Java EE 方法。
    EJB 类 @TransactionManagement 注释:
    必须显式设置为 TransactionManagement.CONTAINER 或省略它以隐式使用默认值。
    EJB 方法@TransactionAttribute 注释:必须显式设置为 TransactionAttribute.REQUIRED 或省略它以隐式使用默认值。(注意:如果您有不同的业务场景,您可以使用 TransactionAttribute.MANDATORY 或 TransactionAttribute.REQUIRES_NEW,前提是它们的语义符合您的需求。)
    应用程序管理的实体管理器:
    它们必须通过 Persistence.createEntityManagerFactory("unitName") 和 emf 创建.createEntityManager(),如上所述。
    通过 JTA 事务加入 EntityManager:
    在事务性 EJB 方法中创建 EntityManager,它们将自动加入 JTA 事务。或者,如果事先创建了 EntityManager,则在事务 EJB 方法中调用 em.joinTransaction()。
    使用完毕后调用 EntityManager.close()。这应该是所有需要的。

    基本示例 - 只需使用更多 EntityManager 进行跨多个 DB 的事务:

    @Stateless  
    public class EmployeeServiceBean implements EmployeeService {
    
        // Transactional method
        public void createEmployee() {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("EmployeeService");
            EntityManager em = emf.createEntityManager();
            Employee emp = ...; // set some data
            // No need for manual join - em created in active tx context, automatic join:
            // em.joinTransaction();         
            em.persist(emp);
            // other data & em operations ...
            // call other EJBs to partake in same transaction ...
            em.close();    // Note: em can be closed before JTA tx committed. 
                       // Persistence Context will still exist & be propagated 
                       // within JTA tx.  Another EM instance could be declared and it 
                       // would propagate & associate the persistence context to it.
                       // Some time later when tx is committed [at end of this 
                       // method], Data will still be flushed and committed and 
                       // Persistence Context removed .
        emf.close();
        }
    
    }
    
    
    
    @Stateful  
    public class EmployeeServiceBean implements EmployeeService {  
    
        // Because bean is stateful, can store as instance vars and use in multiple methods  
        private EntityManagerFactory emf;
        private EntityManager em;
    
        @PostConstruct      // automatically called when EJB constructed and session starts
        public void init() {
            emf = Persistence.createEntityManagerFactory("EmployeeService");
            em = emf.createEntityManager();
        }
    
        // Transactional method
        public void createEmployee() {
            Employee emp = ...; // set some data
            em.joinTransaction();         // em created before JTA tx - manual join
            em.persist(emp);
        }
    
        // Transactional method
        public void updateEmployee() {
            Employee emp = em.find(...);  // load the employee
            // don't do join if both methods called in same session - can only call once: 
            // em.joinTransaction();         // em created before JTA tx - manual join
            emp.set(...);                 // change some data
                                 // no persist call - automatically flushed with commit
        }
    
        @Remove                           // automatically called when EJB session ends
        public void cleanup() {
            em.close();
            emf.close();
        }
    // ...
    }
    
  • B) 带有 BEAN 管理的 JTA 事务的 EJB

    使用@TransactionManagement.BEAN。
    注入JTA UserTransaction 接口,使bean 可以直接标记JTA 事务。
    通过 UserTransaction.begin()/commit()/rollback() 手动标记/同步事务。
    确保 EntityManager 加入 JTA 事务 - 在活动的 JTA 事务上下文中创建 EM 或调用 em.joinTransaction()。

    例子:

    @TransactionManagement(TransactionManagement.BEAN)  
    @Stateless  
    public class EmployeeServiceBean implements EmployeeService {
    
        // inject the JTA transaction interface
        @Resource UserTransaction jtaTx;
    
        public void createEmployee() {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("EmployeeService");
            EntityManager em = emf.createEntityManager();
            try {
                jtaTx.begin();
                try {
                   em.joinTransaction();         
                   Employee emp = ...; // set some data
                   em.persist(emp);
                   // other data & em operations ...
                   // call other EJBs to partake in same transaction ...
                } finally {
                    jtaTx.commit();
                }
            } catch (Exception e) {
               // handle exceptions from UserTransaction methods
               // ...
            }
    
            Employee emp = ...; // set some data
            // No need for manual join - em created in active tx context, automatic join:
            // em.joinTransaction();         
            em.persist(emp);
            em.close();    // Note: em can be closed before JTA tx committed. 
                       // Persistence Context will still exist inside JTA tx.
                       // Data will still be flushed and committed and Persistence 
                       // Context removed some time later when tx is committed.
            emf.close();
        }
    
    }
    
  • C) POJO/Non-EJB 与手动编码(bean 管理)资源本地事务(不是 JTA)

    只需使用 JPA EntityTransaction 接口进行 tx 分界(通过 em.getTransaction() 获得)。

    例子:

    public class ProjectServlet extends HttpServlet {
    
        @EJB ProjectService bean;
    
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
            // ...
            try {
                EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPU");
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                tx.begin();
                try {
                    bean.assignEmployeeToProject(projectId, empId);
                    bean.updateProjectStatistics();
                } finally {
                    tx.commit();
                }
            } catch (Exception e) {
               // handle exceptions from EntityTransaction methods
               // ...
            }
        // ...
        }
    }
    
  • D) 带有手动编码(POJO 管理)JTA 事务的 POJO/非 EJB

    这假设 POJO/组件在一些支持 JTA 的容器中运行。
    如果在 Java EE 容器中,可以使用 JTA UserTransaction 接口的 Java EE 资源注入。
    (或者,可以显式查找 JTA 接口的句柄并对其进行划分,然后调用 em.getTransaction().joinTransaction() - 请参阅 JTA 规范。)

    例子:

    public class ProjectServlet extends HttpServlet {
    
        @Resource UserTransaction tx;
        @EJB ProjectService bean;
    
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
            // ...
            try {
                tx.begin();
                try {
                    bean.assignEmployeeToProject(projectId, empId);
                    bean.updateProjectStatistics();
                    EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPU");
                    EntityManager em = emf.createEntityManager();
                    // Should be able to avoid explicit call to join transaction.
                    // Should automatically join because EM created in active tx context.
                    // em.joinTransaction();
                    // em operations on data here
                    em.close();
                    emf.close();
                } finally {
                    tx.commit();
                }
            } catch (Exception e) {
               // handle exceptions from UserTransaction methods
               // ...
            }
        // ...
        }
    }
    
于 2013-03-12T07:24:49.117 回答
-2

尝试首先创建一个查询而不是本机查询,返回一个 Bars 列表。还尝试在您的 EJB 中注释 H2 注入。如果它有效,那么您就知道这是一个 CDI 冲突问题。

于 2013-03-11T14:25:22.667 回答