为什么具有不同数据源的不同持久性单元查询同一数据源?

2022-09-01 20:32:13

我正在开发一个web应用程序,它需要访问两个不同的数据库服务器(H2和Oracle)。容器是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是限定符):

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

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

我的执着.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>

答案 1

容器托管的持久性上下文

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

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

JPA规范中的一些引用:

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

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

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

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

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

持久性上下文传播的要求

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

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

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

嗯,这是因为这是一种刻意的权衡,为EJB带来了强大的EntityManager魔法。

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

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

问题的答案:使用应用程序托管的持久性上下文(通过应用程序管理的实体管理器))

通过以下方法之一声明您的实体管理器:

// "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 事务 - 但这些 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) EJB 与 CONTAINER 管理的 JTA 事务

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

    基本示例 - 只需使用更多的实体管理器进行跨多个数据库的事务:

    @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) EJB 与 BEAN 管理的 JTA 事务

    使用 @TransactionManagement.BEAN。
    注入JTA用户事务接口,这样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/Non-EJB,带有手动编码(POJO 管理的)JTA 事务

    这假设 POJO/组件在某个支持 JTA 的容器中运行。
    如果在 Java EE 容器中,可以使用 Java EE 资源注入 JTA UserTransaction 接口。
    (或者,可以显式查找 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
               // ...
            }
        // ...
        }
    }
    

答案 2

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


推荐