为啥不同数据源的持久化单元查询同一个数据源?

Posted

技术标签:

【中文标题】为啥不同数据源的持久化单元查询同一个数据源?【英文标题】:Why different persistence units with separated data sources query the same data source?为什么不同数据源的持久化单元查询同一个数据源? 【发布时间】:2013-02-23 02:57:26 【问题描述】:

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

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

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

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

@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 是 qualifiers):

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>

【问题讨论】:

只是复制粘贴错误,还是您的@PercistenceContext 中有错误的单位名称?应该是FenixRadarPU 复制粘贴错误。我已经更正了。谢谢! @PersistenceContext(unitName = "...") 注释直接添加到Service 类中的EntityManagers 以尝试确定这是CDI 问题还是JPA 问题。 @RichardSitze 我最近尝试过,结果是相同的,将实体管理器的声明更改为:@PersistenceContext(unitName="OraclePU") private EntityManager emOracle; @PersistenceContext(unitName="H2PU") private EntityManager emH2; 只是一个注释,以确保 beans.xml 没有任何配置设置 - 将覆盖代码中的 CDI 注释。我看到这没有发生 - @PC(unitName=...) 测试上面的两点绕过 CDI。 【参考方案1】:

容器管理的持久性上下文

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

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

来自 JPA 规范的一些引用:

当使用容器管理的实体管理器时, 持久性上下文总是自动管理,透明地 应用程序,并且持久性上下文通过 JTA 交易。

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

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

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

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

持久性上下文传播的要求

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

... 如果调用了一个组件并且 JTA 事务传播到该组件: • 如果组件是已绑定扩展持久性上下文的有状态会话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() - 最好通过最终的 子句或通过 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
           // ...
        
    // ...
    

【讨论】:

这绝对有帮助。我按照您的建议创建了 EM,并在工作完成后关闭了它们,尽管我不得不将 EJB 注释为 @TransactionManagement(TransactionManagementType.BEAN),因为 @TransactionManagement(TransactionManagementType.CONTAINER) 仍然存在“单个 JTA 事务”问题。此外,我现在收到此异常org.apache.openjpa.persistence.TransactionRequiredException: Can only perform operation while a transaction is active。好像我需要手动创建/提交/回滚/关闭事务。这是正确的吗? 很高兴它有帮助。如果你使用@TransactionManagement(TransactionManagementType.BEAN,那么你需要做你自己的JTA事务划分/同步: ... 实际上 - 忘记那条评论:我已经附加到上面帖子的末尾而不是 B^) 总结:(1)您应该能够将 TransactionManagementType.CONTAINER 与应用程序管理的实体管理器一起使用。它们应该加入容器的 JTA 事务,而无需与容器/JTA 传递/共享它们的持久性上下文——这意味着容器应该允许两个 EM 针对两个不同的数据库独立操作。 (2) 如果你选择使用TransactionManagement.BEAN,那么是的,你必须手动控制JTA事务——注入JTA接口为:@Resource UserTransaction jtaTx;确保 EM 加入交易:em.joinTransaction()。 老兄...谢谢你。我一直在试图弄清楚如何进行跨越多个 bean/pojo 调用(来自 REST 服务环境)的事务,我很确定这会对我有所帮助。【参考方案2】:

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

【讨论】:

我评论了emH2 及其注释,并且只对emOracle 进行了JPQL 查询。它有效,但这不是解决方案。当持久性单元/数据源/限定符明确引用不同的事物时,我不明白它们是如何发生冲突的。 确保 Bar 类位于您列出的包中。试试两个 PU 的 true 包没问题。如您所见,&lt;exclude-unlisted-classes&gt;true&lt;/exclude-unlisted-classes&gt; 包含在 persistence.xml 中。

以上是关于为啥不同数据源的持久化单元查询同一个数据源?的主要内容,如果未能解决你的问题,请参考以下文章

为啥mysql在持久化上比redis用的多

为啥需要在执行修改查询之前清除 jpa 持久性上下文?

使用Spring时如何注入多个JPA EntityManager(持久化单元)

当persistence.xml存在时,为啥持久性单元命名为null

UICollectionView 单元格不保留数据 --- 如何使它们的数据持久化?

JPA的核心配置