事務管理
事務管理是JPA中另一項重要的內容,瞭解了JPA中的事務管理,能夠進一步掌握JPA的使用。
事務管理是對一系列操作的管理,它最終只有兩個結果,要麼成功,要麼失敗。一旦失敗,所有的操作將回滾到初始狀態。一旦成功,才最終提交,最終持久化。事務管理對銀行系統最爲典型。例如一個人去銀行取款,他取款的錢此時大於銀行賬戶中的錢,此時交易失敗,所以取款不成功,事務回滾到操作前的狀態。
在JPA中,對於實體的“CRUD”基本操作,其中涉及事務的是“C”、“U”和“D”,即“新建”、“更新”和“刪除”,因爲這些操作都會影響數據庫中的數據變化,所以必須使用事務保證其一致性;對於“R”查詢,只是查詢數據,沒有對數據產生變化,所以並不需要控制事務。
所以,一說到事務,讀者首先應確定所使用的操作是否需要關聯事務,先要界定事務所有效使用的範圍。
11.4.1 事務與EntityManager
EntityManager對象的事務管理方式有兩種,分別爲JTA和RESOURCE_LOCAL,即Java Transaction API方法和本地的事務管理。
JPA中的事務類型通過persistence.xml文件中的“transaction-type”元素配置。例如,配置事務爲JTA方式的代碼如下所示。
<persistence>
<persistence-unit name="demo" transaction-type="JTA">
//其他配置省略
</persistence-unit>
</persistence>
如果使用RESOURCE_LOCAL管理事務,則配置代碼如下所示。
<persistence>
<persistence-unit name="demo" transaction-type="RESOURCE_LOCAL">
//其他配置省略
</persistence-unit>
</persistence>
除了在配置文件時指明瞭事務的類型,不同的事務類型,不同類型的EntityManager對象,在代碼中控制事務也是不同的。表11-2爲不同的運行環境、不同的EntityManager對象所支持的事務類型。
表11-2 事務類型與EntityManager
運行環境 類型 | J2EE環境 | J2SE環境 | |
EJB容器 | Web容器 | ||
應用託管的EntityManager | JTA,RESOURCE_LOCAL | JTA,RESOURCE_LOCAL | RESOURCE_LOCAL |
容器託管的EntityManager | JTA | 不支持 | 不支持 |
從表11-2中可以看出,對於不同的EntityManager類型與所運行的環境,所支持的事務類型是不一樣的。
其中兩種情況下最爲簡單,一種是容器託管的EntityManager只能運行在EJB容器中,只能採用JTA的方式管理事務;另一種是J2SE環境下,只能使用應用託管的EntityManager並且只能採用RESOURCE_LOCAL的方式管理事務。本節的事務只針對這兩種情況講述,而對於應用託管的EntityManager在EJB容器和Web容器中由於都可以選擇不同的事務管理方式,情況比較複雜,所以將在第11.5節中詳細講述。
11.4.2 JTA管理事務
JTA事務(Java Transaction API)是J2EE規範中有關事務的標準。它是容器級別的事務,只能運行在J2EE服務器中。它的最大優勢是可以支持分佈式的事務,如果系統採用的是分佈式的數據庫,那麼只能選擇JTA管理EntityManager事務。
使用JTA管理EntityManager事務時,需要注意以下幾個問題。
— JTA事務只能運行在J2EE的環境中,即EJB容器中和Web容器中;而在J2SE環境中只能使用RESOURCE_LOCAL管理事務。
— 容器託管的EntityManager對象只能採用JTA的事務,而不能採用RESOURCE_LOCAL事務。
在第11.3節中,已經簡單瞭解了一些JTA事務與EntityManager之間的關係,但當Bean的方法中又調用了另一個Bean的方法時,那麼此時事務傳播(Propagation)是如何進行的?下面就深入瞭解事務的傳播與持久化上下文的關係。
有這樣一個記錄日誌的會話Bean,它負責記錄相關的日誌信息等,它有一個記錄日誌的方法recordLog,代碼如下所示。
@Stateless
public class LogService implements ILogService {
@PersistenceContext(unitName = "jpaUnit")
private EntityManager entityManager;
/**記錄日誌*/
public void recordLog(Integer id, String reason) {
LogEO log = new LogEO();
log.setId(id);
log.setReason(reason);
entityManager.persist(log);
}
}
此時在CustomerService的會話Bean中,addCustomer方法中需要新建客戶後,再調用日誌組件來記錄日誌信息,代碼如下所示。
@Stateless
public class CustomerService implements ICustomerService {
@PersistenceContext(unitName = "jpaUnit")
private EntityManager entityManager;
@EJB
private ILogService logService ;
public CustomerEO addCustomer(CustomerEO customer) {
entityManager.persist(customer);
logService.recordLog(customer.getId(), "新建Customer");
return customer;
}
}
此時EntityManager對象是容器託管的,並且設置的事務類型爲JPA。下面仔細分析一下,當在一個EJB組件中調用另外一個EJB組件時,事務的傳播與持久化上下文環境的關係。
— 當客戶端調用addCustomer方法時,此時容器自動關聯一個JTA的事務,一個事務開始,這裏將該事務記爲事務A。
— 當調用persist方法持久化客戶時,EntityManager對象發現當前有一個JTA的事務A,則此時將EntityManager對象的事務附加到JTA的事務A中,並且創建了一個新的持久化上下文。
— 調用日誌組件的recordLog方法,容器發現調用了另外一個EJB的方法,所以首先檢查當前是否存在事務,由於當前狀態下存在事務A,所以將recordLog方法的事務附加到事務A中(由於默認情況下,CustomerService的事務類型是REQUIRED)。
— 當進入recordLog方法時,再次調用persist方法持久化日誌時,由於此時EntityManager對象的事務是附加到JTA事務A中的,所以仍與之前調用的persist方法時所在的持久化上下文相同,所以,可以直接調用持久化客戶後的customer.getId(),來獲得持久化客戶的Id值。雖然在一個EJB組件中調用了另外一個EJB組件的方法,但兩次調用的persist方法所在的持久化上下文是相同的。
— recordLog方法結束,又回到addCustomer方法中,此時事務A提交,一個持久化上下文也就隨之結束了。
11.4.3 RESOURCE_LOCAL管理事務
RESOURCE_LOCAL事務數據庫本地的事務。它是數據庫級別的事務,只能針對一種數據庫,不支持分佈式的事務。對於中小型的應用,可以採用RESOURCE_LOCAL管理EntityManager事務。
使用RESOURCE_LOCAL管理EntityManager事務時需要注意以下幾個問題。
— 在J2SE環境中,只能使用RESOURCE_LOCAL管理EntityManager事務,並且EntityManager對象是以應用託管方式獲得的。
— 代碼中使用RESOURCE_LOCAL管理事務時,要通過調用EntityManager的getTransac- tion()方法獲得本地事務對象。
例如,在J2SE環境中,使用RESOURCE_LOCAL管理EntityManager事務的代碼如下所示。
public class CustomerClient {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence
.createEntityManagerFactory("jpaUnit");
EntityManager entityManager = emf.createEntityManager();
try {
/** 事務開始 */
entityManager.getTransaction().begin();
CustomerEO customer = new CustomerEO();
customer.setName("Janet");
customer.setEmail("[email protected]");
customer.setAsset(100000.00);
/** 事務提交 */
entityManager.getTransaction().commit();
} finally {
entityManager.close();
emf.close();
}
}
}
★ 提示 ★
採用RESOURCE_LOCAL管理事務時,要保證數據庫支持事務。例如使用MySQL時,需要設置數據庫的引擎類型爲“InnoDB”,而“MyISAM”類型是不支持事務的。
— 在代碼中,entityManager.getTransaction()方法獲得本地事務EntityTransaction對象,然後通過該對象提供的方法來控制本地的事務。有關EntityTransaction的API將在下一節講述。
— 控制本地事務時,開始一個新事務,使用begin()方法;事務完成後,使用commit()方法提交。控制事務時,並沒有調用rollback()方法回滾,這是因爲在事務開始後,一旦有異常拋出,EntityTransaction對象將自動回滾,所以並不需要顯式地調用rollback()方法回滾。
11.4.4 EntityTransaction API
下面來看本地事務EntityTransaction中所定義的方法EntityTransaction API,以及它們的作用,如下所示。
EntityTransaction API
package javax.persistence;
public interface EntityTransaction {
public void begin();
public void commit();
public void rollback();
public void setRollbackOnly();
public boolean getRollbackOnly();
public boolean isActive();
}
下面具體來看各個方法所表示的意義,每個方法都從作用、方法參數、異常信息,以及返回值這幾個方面來講述。
— public void begin()
作用:聲明事務開始。
方法參數:無。
異常信息:如果此時事務處於激活狀態,即isActive()爲true,將拋出IllegalStateException異常。
返回值:無返回值。
— public void commit()
作用:提交事務,事務所涉及的數據的更新將全部同步到數據庫中。
方法參數:無。
異常信息:如果此時事務處於未激活狀態,即isActive()爲false,將拋出IllegalState Exception異常;如果此時提交不成功,則拋出RollbackException異常。
返回值:無返回值。
— public void rollback()
作用:事務回滾。
方法參數:無。
異常信息:如果此時事務處於未激活狀態,即isActive()爲false,將拋出IllegalState Exception異常;如果此時回滾失敗,則拋出PersistenceException異常。
返回值:無返回值。
— public void setRollbackOnly()
作用:設置當前的事務只能是回滾狀態。
方法參數:無。
異常信息:如果此時事務處於未激活狀態,即isActive()爲false,將拋出IllegalState Exception異常。
返回值:無返回值。
— public boolean getRollbackOnly()
作用:獲得當前事務的回滾狀態。
方法參數:無。
異常信息:如果此時事務處於未激活狀態,即isActive()爲false,將拋出IllegalState Exception異常。
返回值:true表示只能回滾狀態。
— public boolean isActive ()
作用:判斷當前事務是否處於激活狀態。
方法參數:無。
異常信息:如果發生了未知的異常,將拋出PersistenceException異常。
返回值:true表示當前事務處於激活狀態,false表示當前事務未處於激活狀態。
11.5 應用託管的EntityManager的持久化上下文
通過表11-2所總結的各種情況,應用託管EntityManager對象在EJB容器中和Web容器中,可選擇的事務類型比較複雜,既可以支持JTA,又可以支持RESOURCE_LOCAL。下面講述在這兩種情況下,如何控制事務。
11.5.1 無狀態的會話Bean與JTA事務(事務範圍)
在會話Bean裏以注入的方式獲得EntityManagerFactory對象,不需要負責它的關閉,所以此時,只需要控制EntityManager的打開和關閉。當客戶端每次調用Bean中的方法時,都首先創建EntityManager對象,然後在方法結束前關閉EntityManager對象。EntityManager對象的事務使用的是容器自動管理的事務JTA。
代碼如下所示。
@Stateless
public class CustomerService implements ICustomerService {
@PersistenceUnit(unitName="jpaUnit")
private EntityManagerFactory emf;
public CustomerEO findCustomerById(Integer customerId) {
EntityManager em = emf.createEntityManager();
CustomerEO customer = em.find(CustomerEO.class, customerId);
em.close();
return customer;
}
public void placeOrder(Integer customerId, OrderEO order) {
EntityManager em = emf.createEntityManager();
CustomerEO customer = em.find(CustomerEO.class, customerId);
customer.getOrders().add(order);
em.merge(customer);
em.close();
}
}
11.5.2 無狀態的會話Bean與JTA事務(擴展範圍)
與上個會話Bean中的管理方式不同,此時EntityManager對象爲Bean的屬性,當Bean初始化後,也就是標註@PostConstruct方法後,創建EntityManager對象;當Bean銷燬前,也就是標註@PreDestroy方法後,關閉EntityManager對象,所以EntityManager對象是整個的Bean的聲明週期中。當客戶端調用需要關聯事務的方法時,需要使用joinTransaction()方法合併到上一次的事務中。
代碼如下所示。
@Stateless
public class CustomerService implements ICustomerService {
@PersistenceUnit(unitName="jpaUnit")
private EntityManagerFactory emf;
private EntityManager em;
@PostConstruct
public void init (){
em = emf.createEntityManager();
}
public CustomerEO findCustomerById(Integer customerId) {
/**查詢不需要關聯事務*/
CustomerEO customer = em.find(CustomerEO.class, customerId);
em.clear();
return customer;
}
public void placeOrder(Integer customerId, OrderEO order) {
/**
*EntityManager 對象的作用範圍是這個Bean的生命週期
*所以,每次使用時要合併到上一次的事務中
*/
em.joinTransaction();
CustomerEO customer = em.find(CustomerEO.class, customerId);
customer.getOrders().add(order);
em.merge(customer);
/**
* 手動脫離當前事務和持久化上下文
*/
em.flush();
em.clear();
}
@PreDestroy
public void destroy(){
em.close();
}
}
11.5.3 有狀態的會話Bean與JTA事務
同樣是EntityManager對象在整個的Bean的聲明週期中,但由於會話Bean此時是有狀態的Bean,所以當客戶端調用任何方法時,都處在同一個持久化上下文中。所以每次並不需要調用clear()方法來手動地脫離當前的上下文,但每次客戶端的調用仍需要使用joinTransaction()方法合併到上一次的事務中。
代碼如下所示。
@Stateful
public class CustomerService implements ICustomerService {
@PersistenceUnit(unitName="jpaUnit")
private EntityManagerFactory emf;
private EntityManager em;
private CustomerEO customer ;
@PostConstruct
public void init (){
em = emf.createEntityManager();
}
public CustomerEO findCustomerById(Integer customerId) {
customer = em.find(CustomerEO.class, customerId);
return customer;
}
public void placeOrder(Integer customerId, OrderEO order) {
em.joinTransaction();
customer.getOrders().add(order);
}
@Remove
public void destroy(){
em.close();
}
}