跳转到内容

Java 持久性/事务

来自 Wikibooks,开放世界中的开放书籍

一个 事务 是一个操作集,要么全部失败,要么全部成功。事务是持久性中的一个基本部分。数据库事务由一组 SQL DML (数据操作语言)操作组成,这些操作作为单个单元提交或回滚。对象级事务是指将对一组对象进行的一组更改作为单个单元提交到数据库。

JPA 提供了两种事务机制。当在 Java EE 中使用时,JPA 提供了与 JTA (Java 事务 API)的集成。JPA 还提供自己的 EntityTransaction 实现,用于 Java SE 以及在 Java EE 中的非托管模式下使用。JPA 中的事务始终位于对象级别,这意味着对持久性上下文中的所有持久性对象进行的所有更改都是事务的一部分。

资源本地事务

[编辑 | 编辑源代码]

资源本地事务用于 JSE 或 Java EE 中的应用程序管理(非托管)模式。要使用资源本地事务,persistence.xml 中的 transaction-type 属性应设置为 RESOURCE_LOCAL。如果资源本地事务与 DataSource 一起使用,则应使用 <non-jta-data-source> 元素来引用已配置为非 JTA 管理的服务器 DataSource

本地 JPA 事务通过 EntityTransaction 类定义。它包含基本的交易 API,包括 begincommitrollback

从技术上讲,在 JPA 中,EntityManager 从创建的那一刻起就处于事务中。因此,begin 有点多余。在调用 begin 之前,不能调用某些操作,例如 persistmergeremove。仍然可以执行查询,并且可以更改已查询的对象,尽管这在 JPA 规范中有点未定义,通常它们将被提交,但是最好在对对象进行任何更改之前调用 begin。通常最好为每个事务创建一个新的 EntityManager,以避免在持久性上下文中保留过时对象,并允许以前管理的对象进行垃圾回收。

在成功执行 commit 后,EntityManager 可以继续使用,并且所有管理的对象都保持管理状态。但是,通常最好 closeclear EntityManager,以允许进行垃圾回收并避免过时数据。如果提交失败,则管理的对象被视为分离状态,并且 EntityManager 被清除。这意味着提交失败无法捕获和重试,如果发生失败,则必须重新执行整个事务。以前管理的对象也可能处于不一致状态,这意味着某些对象的锁定版本可能已增加。如果事务已标记为回滚,提交也将失败。这可以通过调用 setRollbackOnly 显式完成,或者如果任何查询或查找操作失败,则必须设置。这可能是一个问题,因为某些查询可能会失败,但可能不希望导致整个事务回滚。

rollback 操作将回滚数据库事务。持久性上下文中的管理对象将成为分离状态,并且 EntityManager 将被清除。这意味着先前读取的任何对象都不应再使用,并且不再是持久性上下文的一部分。对对象的更改将保持原样,对象的更改不会被还原。

示例资源本地事务 persistence.xml

[编辑 | 编辑源代码]
<persistence 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 persistence_1_0.xsd" version="1.0">
    <persistence-unit name="acme" transaction-type="RESOURCE_LOCAL">
        <non-jta-data-source>acme</non-jta-data-source>
    </persistence-unit>
</persistence>

示例资源本地事务

[编辑 | 编辑源代码]
EntityManager em = createEntityManager();
em.getTransaction().begin();
Employee employee = em.find(Employee.class, id);
employee.setSalary(employee.getSalary() + 1000);
em.getTransaction().commit();
em.close();

JTA 事务

[编辑 | 编辑源代码]

JTA 事务用于 Java EE 中的托管模式(EJB)。要使用 JTA 事务,persistence.xml 中的 transaction-type 属性应设置为 JTA。如果 JTA 事务与 DataSource 一起使用,则应使用 <jta-datasource> 元素来引用已配置为 JTA 管理的服务器 DataSource

JTA 事务通过 JTA UserTransaction 类定义,或者更可能通过 SessionBean 的使用/方法隐式定义。在 SessionBean 中,每个 SessionBean 方法默认使用 TransactionAttribute TransactionAttributeType.REQUIRED,因此调用它会启动 JTA 事务(如果当前没有 JTA 事务正在进行)。如果一个方法是第一个需要事务的调用方法,它将在完成时提交更改。

JTA 事务可以在 SessionBean 方法之间共享,因此,如果通过它的业务接口从另一个已经启动了事务的方法调用一个方法,第二个方法将在现有的事务中运行。此外,第二个方法只能通过持久化对象或执行其他操作来更改 EntityManager 的状态,当它将控制权返回给调用方法时,它会委托给它执行实际的 commit。要在与正在进行的任何事务隔离的事务中执行 DB 操作,SessionBean 方法可以被注释为 TransactionAttribute TransactionAttributeType.REQUIRES_NEW。可以与 SessionBean 方法关联的 TransactionAttributeType

类型 描述 异常 提交策略
REQUIRED 需要事务:如果调用该方法的客户端已与事务上下文相关联,则该方法将托管在客户端的上下文中,否则将启动新事务 在完成时执行,如果未托管在另一个方法的事务上下文中
REQUIRES_NEW 需要单独的事务:在任何情况下都启动事务,如果通过已与事务上下文相关联的客户端调用该方法,则现有事务将被挂起 在完成时执行
MANDATORY 需要现有事务:它要求调用该方法的客户端已与事务上下文相关联,并且该方法将托管在客户端的上下文中 如果客户端在未与事务关联的情况下调用该方法,则会抛出异常 委托给托管上下文
NOT_SUPPORTED 不使用任何事务:不会启动事务,如果调用该方法的客户端已与事务上下文相关联,则现有事务将被挂起 无直接异常。如果在方法内部尝试了 persist() 等操作,则会抛出异常 不执行
NEVER 如果事务已存在,则无法执行:客户端必须在任何事务上下文之外调用 如果调用该方法的客户端与事务上下文相关联,则会抛出异常。如果在方法内部尝试了 persist() 等操作,则会抛出异常 不执行
SUPPORTS 不需要事务,但如果调用该方法的客户端已与事务上下文相关联,则该方法将在事务上下文中执行 无直接异常。如果在方法内部尝试了 persist() 等操作,并且调用该方法的客户端没有与事务上下文相关联,则会抛出异常 不需要

UserTransaction 可以通过大多数应用程序服务器中的 JNDI 查找获取,也可以从 EJB 2.0 风格的 SessionBean 中的 EJBContext 获取。

JTA 事务可以在 Java EE 中使用两种模式。在 Java EE 托管模式下,例如注入到 SessionBean 中的 EntityManagerEntityManager 引用表示每个事务的新持久性上下文。这意味着在一个事务中读取的对象在事务结束时会成为分离状态,并且不应再使用,或者需要合并到下一个事务中。在托管模式下,您永远不会创建或关闭 EntityManager

第二种模式允许 EntityManager 被应用程序管理(通常从注入的 EntityManagerFactory 获取,或直接从 JPA Persistence 获取)。这允许持久性上下文在事务边界内存活,并遵循类似于资源本地的正常 EntityManager 生命周期。如果 EntityManager 在活动 JTA 事务的上下文中创建,它将自动成为 JTA 事务的一部分,并随 JTA 事务一起提交/回滚。否则,它必须使用 EntityManager.joinTransaction() 加入 JTA 事务以提交/回滚。

示例 JTA 事务 persistence.xml

[编辑 | 编辑源代码]
<persistence 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 persistence_1_0.xsd" version="1.0">
    <persistence-unit name="acme" transaction-type="JTA">
        <jta-data-source>acme</jta-data-source>
    </persistence-unit>
</persistence>

示例 JTA 事务

[编辑 | 编辑源代码]
UserTransaction transaction = (UserTransaction)new InitialContext().lookup("java:comp/UserTransaction");
transaction.begin();
EntityManager em = getEntityManager();
Employee employee = em.find(Employee.class, id);
employee.setSalary(employee.getSalary() + 1000);
transaction.commit();

加入事务

[编辑 | 编辑源代码]

EntityManager.joinTransaction() API 允许应用程序管理的 EntityManager 加入到活动 JTA 事务上下文。这允许 EntityManager 在 JTA 事务范围之外创建,并将更改作为当前事务的一部分提交。这通常与 stateful SessionBean 一起使用,或者与 JSP 或 Servlet 一起使用,其中 EXTENDED EntityManager有状态的架构中使用。有状态的架构是服务器在客户端连接上存储信息直到客户端会话结束的一种架构,它不同于无状态的架构,其中在客户端请求之间服务器上不存储任何信息(每个请求都单独处理)。

有状态无状态架构都有优缺点。使用有状态架构和EXTENDED EntityManager 的优点之一是,您不必担心合并对象。您可以在一个请求中读取对象,让客户端修改它们,然后将它们作为新事务的一部分提交。这就是joinTransaction 的用途。这个问题是,您通常希望在没有活动 JTA 事务时创建EntityManager,否则它将在该事务中提交。但是,即使它确实提交,您仍然可以继续使用它并加入未来的事务。您必须避免使用事务性 API,例如mergeremove,直到您准备提交事务。

joinTransaction 仅与 JTA 管理的 EntityManager(persistence.xml 中的 JTA 事务类型)一起使用。对于RESOURCE_LOCAL EntityManager,您可以在需要时提交 JPA 事务。

示例 joinTransaction 使用方法

[编辑 | 编辑源代码]
EntityManager em = getEntityManagerFactory().createEntityManager();
Employee employee = em.find(Employee.class, id);
employee.setSalary(employee.getSalary() + 1000);

UserTransaction transaction = (UserTransaction)new InitialContext().lookup("java:comp/UserTransaction");
transaction.begin();
em.joinTransaction();
transaction.commit();

重试事务,处理提交失败

[编辑 | 编辑源代码]

有时希望处理持久性错误,并恢复和重试事务。这通常需要大量的应用程序知识来了解哪些失败了,系统处于什么状态,以及如何修复它。

不幸的是,JPA 没有提供处理提交失败或错误处理的直接方法。当事务提交失败时,事务将自动回滚,持久性上下文清除,所有管理的对象都分离。不仅没有办法处理提交失败,而且如果在提交之前查询中发生任何错误,事务将被标记为回滚,因此实际上没有办法处理任何错误。这是因为任何查询都可能潜在地改变数据库的状态,因此 JPA 不知道数据库是否处于无效或不一致的状态,因此必须回滚事务。同样,如果提交失败,在持久性上下文中注册的对象的状态也可能不一致(例如,部分提交的对象的乐观锁版本已递增),因此持久性上下文将被清除以避免进一步的错误。

一些 JPA 提供程序可能会提供扩展的 API 来允许处理提交失败或处理查询中的错误。

有一些方法可以通用地处理提交失败和其他错误。使用RESOURCE_LOCAL 事务时,错误处理通常更容易,而不是使用 JTA 事务。

错误处理的一种方法是在提交失败后,将每个管理的对象调用merge 到一个新的EntityManager 中,然后尝试提交新的EntityManager。一个问题可能是,任何分配的 ID 或分配或递增的乐观锁版本可能需要重置。此外,如果原始EntityManagerEXTENDED,任何正在使用的对象仍将分离,需要重置。

另一种更复杂的方法是始终使用非事务性EntityManager。当要提交时,创建一个新的EntityManager,将非事务性对象合并到其中,并提交新的EntityManager。如果提交失败,只有新的EntityManager 的状态可能不一致,原始EntityManager 将不受影响。这允许更正问题,并将EntityManager 重新合并到另一个新的EntityManager 中。如果提交成功,任何提交的更改都可以合并回原始EntityManager,然后可以继续像平常一样使用它。此解决方案需要相当多的开销,因此仅应在确实需要错误处理并且 JPA 提供程序没有提供其他选择时使用。

嵌套事务

[编辑 | 编辑源代码]

JPA 和 JTA 不支持嵌套事务。

嵌套事务用于为在较大事务范围内执行的操作子集提供事务保证。这样做允许您独立于较大事务提交和中止操作子集。

嵌套事务的使用规则如下

在嵌套(子)事务处于活动状态时,父事务可能不会执行任何操作,除了提交或中止,或者创建更多子事务。

提交嵌套事务不会影响父事务的状态。父事务仍然未提交。但是,父事务现在可以看到子事务所做的任何修改。当然,这些修改对于所有其他事务来说仍然是隐藏的,直到父事务也提交。

同样,中止嵌套事务不会影响父事务的状态。中止的唯一结果是,无论是父事务还是任何其他事务都将看不到在嵌套事务保护下执行的任何数据库修改。

如果父事务在具有活动子事务的情况下提交或中止,则子事务将以与父事务相同的方式解决。也就是说,如果父事务中止,则子事务也中止。如果父事务提交,则子事务执行的任何修改也将提交。

嵌套事务持有的锁不会在该事务提交时释放。相反,它们现在由父事务持有,直到父事务提交为止。

嵌套事务执行的任何数据库修改在父事务提交之前,都不会在更大的包含事务之外可见。

您可以使用嵌套事务实现的嵌套深度仅受内存限制。

事务隔离

[编辑 | 编辑源代码]
华夏公益教科书