參考文檔的這一部分涉及數據訪問和數據訪問層與業務或服務層之間的交互。
本文詳細介紹了Spring的全面事務管理支持,然後全面介紹了Spring框架集成的各種數據訪問框架和技術。
1. 事務管理
全面的事務支持是使用Spring框架的最重要原因之一。Spring框架爲事務管理提供了一致的抽象,提供了以下好處:
- 跨不同事務API(如Java事務API (JTA)、JDBC、Hibernate和Java持久性API (JPA))的一致編程模型。
- 支持聲明式事務管理。
- 用於程序化事務管理的API比複雜事務API(如JTA)更簡單。
- 與Spring數據訪問抽象的優秀集成。
以下部分描述了Spring框架的事務特性和技術:
- Spring框架事務支持模型的優點描述了爲什麼要使用Spring框架的事務抽象,而不是EJB容器管理的事務(CMT)或選擇通過專有API(如Hibernate)驅動本地事務。
- 理解Spring框架事務抽象概述了核心類,並描述瞭如何配置和從各種數據源獲取數據源實例。
- 將資源與事務同步描述應用程序代碼如何確保正確地創建、重用和清理資源。
- 聲明性事務管理描述了對聲明性事務管理的支持。
- 程序化事務管理包括對程序化(即顯式編碼)事務管理的支持。
- 事務綁定事件描述如何在事務中使用應用程序事件。
本章還討論了最佳實踐、應用服務器集成和常見問題的解決方案。
1.1 Spring框架的事務支持模型的優點
傳統上,Java EE開發人員在事務管理方面有兩種選擇:全局事務或本地事務,兩者都有很大的侷限性。在接下來的兩部分中,將回顧全局和本地事務管理,然後討論Spring框架的事務管理支持如何解決全局和本地事務模型的限制。
1.1.1 全局事務
全局事務允許您使用多個事務資源,通常是關係數據庫和消息隊列。應用服務器通過JTA管理全局事務,JTA是一個繁瑣的API(部分原因是它的異常模型)。此外,JTA UserTransaction通常需要從JNDI獲得,這意味着您也需要使用JNDI來使用JTA。全局事務的使用限制了應用程序代碼的任何潛在重用,因爲JTA通常只在應用程序服務器環境中可用。
以前,使用全局事務的首選方法是通過EJB CMT(容器管理事務)。CMT是聲明式事務管理的一種形式(與程序化事務管理不同)。EJB CMT消除了對與事務相關的JNDI查找的需要,儘管使用EJB本身需要使用JNDI。它消除了編寫Java代碼來控制事務的大部分(但不是全部)需求。其顯著的缺點是CMT與JTA和應用服務器環境綁定在一起。而且,只有在選擇在EJB中實現業務邏輯(或者至少在事務EJB facade之後)時,纔可以使用它。EJB的負面影響通常如此之大,以至於這不是一個有吸引力的命題,特別是在聲明性事務管理的替代方案面前。
1.1.2 本地事務
本地事務是特定於資源的,例如與JDBC連接相關聯的事務。本地事務可能更容易使用,但有一個明顯的缺點:它們不能跨多個事務資源工作。例如,使用JDBC連接管理事務的代碼不能在全局JTA事務中運行。由於應用服務器不參與事務管理,因此它無法幫助確保多個資源之間的正確性。(值得注意的是,大多數應用程序使用單一事務資源。)另一個缺點是,本地事務對編程模型具有侵入性。
1.1.3 Spring框架的一致編程模型
Spring解決了全局事務和本地事務的缺點。它允許應用程序開發人員在任何環境中使用一致的編程模型。您只需編寫一次代碼,就可以從不同環境中的不同事務管理策略中獲益。Spring框架同時提供了聲明式和程序化事務管理。大多數用戶更喜歡聲明式事務管理,我們在大多數情況下推薦這種管理。
通過程序化事務管理,開發人員可以使用Spring框架事務抽象,它可以在任何底層事務基礎設施上運行。使用首選的聲明性模型,開發人員通常只編寫很少或根本不編寫與事務管理相關的代碼,因此不依賴於Spring Framework事務API或任何其他事務API。
您需要一個應用程序服務器來進行事務管理嗎?
Spring框架的事務管理支持改變了企業Java應用程序何時需要應用服務器的傳統規則。
特別是,您不需要一個應用服務器來通過ejb進行聲明性事務。實際上,即使您的應用程序服務器具有強大的JTA功能,您也可能認爲Spring框架的聲明性事務比EJB CMT提供了更強大的功能和更高效的編程模型。
通常,只有在應用程序需要處理跨多個資源的事務時,才需要應用服務器的JTA功能,而這對於許多應用程序來說並不是必需的。許多高端應用程序使用單一的、高度可伸縮的數據庫(如Oracle RAC)。獨立事務管理器(如Atomikos事務和JOTM)是其他選項。當然,您可能需要其他應用服務器功能,比如Java Message Service (JMS)和Java EE Connector Architecture (JCA)。
Spring框架允許您選擇何時將應用程序擴展到完全加載的應用服務器。使用EJB CMT或JTA的唯一替代方法是使用本地事務(例如JDBC連接上的事務)編寫代碼,如果需要這些代碼在全局的、容器管理的事務中運行,那麼就需要進行大量的返工,這樣的日子已經一去不復返了。在Spring框架中,只需要更改配置文件中的一些bean定義(而不需要更改代碼)。
1.2 理解Spring框架事務抽象
Spring事務抽象的關鍵是事務策略的概念。事務策略由org.springframe .transaction定義。PlatformTransactionManager接口,如下所示:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
這主要是一個服務提供者接口(SPI),儘管您可以從應用程序代碼中以編程方式使用它。因爲PlatformTransactionManager是一個接口,它可以根據需要輕鬆地模擬或存根。它不與查找策略(如JNDI)綁定。PlatformTransactionManager實現的定義與Spring框架IoC容器中的任何其他對象(或bean)一樣。僅這一點就使Spring框架事務成爲有價值的抽象,即使在使用JTA時也是如此。您可以比直接使用JTA更容易地測試事務代碼。
同樣,爲了與Spring的理念保持一致,可以由任何PlatformTransactionManager接口的方法拋出的TransactionException是未選中的(也就是說,它擴展了java.lang。RuntimeException類)。事務基礎架構失敗幾乎總是致命的。在極少數情況下,應用程序代碼實際上可以從事務失敗中恢復,應用程序開發人員仍然可以選擇捕獲和處理TransactionException。重要的一點是,開發人員並不是被迫這樣做的。
getTransaction(..)方法根據TransactionDefinition參數返回一個TransactionStatus對象。如果當前調用堆棧中存在匹配的事務,則返回的TransactionStatus可以表示新事務,也可以表示現有事務。後一種情況的含義是,與Java EE事務上下文一樣,TransactionStatus與執行線程相關聯。
TransactionDefinition接口指定:
- 傳播:通常,在事務範圍內執行的所有代碼都在該事務中運行。但是,如果在事務上下文已經存在時執行事務方法,則可以指定行爲。例如,代碼可以繼續在現有事務中運行(通常情況下),或者可以掛起現有事務並創建一個新事務。Spring提供了所有與EJB CMT相似的事務傳播選項。要了解Spring中事務傳播的語義,請參閱事務傳播。
- 隔離:此事務與其他事務的工作隔離的程度。例如,該事務是否可以看到來自其他事務的未提交寫操作?
- 超時:此事務在超時並由基礎事務基礎設施自動回滾之前運行的時間。
- 只讀狀態:當代碼讀取但不修改數據時,可以使用只讀事務。在某些情況下,例如使用Hibernate時,只讀事務可能是一種有用的優化。
這些設置反映了標準的事務概念。如果需要,請參閱討論事務隔離級別和其他核心事務概念的參考資料。理解這些概念對於使用Spring框架或任何事務管理解決方案都是必不可少的。
TransactionStatus接口爲事務代碼提供了一種簡單的方法來控制事務執行並查詢事務狀態。這些概念應該很熟悉,因爲它們對於所有事務api都是通用的。下面的清單顯示了TransactionStatus接口:
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
無論您在Spring中選擇聲明式事務管理還是程序化事務管理,定義正確的PlatformTransactionManager實現都是絕對必要的。通常通過依賴項注入定義此實現。
PlatformTransactionManager實現通常需要了解其工作環境:JDBC、JTA、Hibernate等。下面的例子展示瞭如何定義一個本地的PlatformTransactionManager實現(在本例中,使用的是普通JDBC)。
你可以通過創建一個類似如下的bean來定義JDBC數據源:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
然後,相關的PlatformTransactionManager bean定義有一個對數據源定義的引用。它應該類似於下面的例子:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
如果您在Java EE容器中使用JTA,那麼您將與Spring的JtaTransactionManager一起使用通過JNDI獲得的容器數據源。下面的示例顯示了JTA和JNDI查找版本的外觀:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
https://www.springframework.org/schema/jee/spring-jee.xsd">
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
<!-- other <bean/> definitions here -->
</beans>
JtaTransactionManager不需要知道數據源(或任何其他特定資源),因爲它使用容器的全局事務管理基礎結構。
注意:前面的數據源bean定義使用了jee名稱空間中的<jndi-lookup/>標記。有關更多信息,請參見JEE模式。
您還可以輕鬆地使用Hibernate本地事務,如下面的示例所示。在這種情況下,您需要定義一個Hibernate LocalSessionFactoryBean,您的應用程序代碼可以使用它來獲得Hibernate會話實例。
DataSource bean定義類似於前面顯示的本地JDBC示例,因此在下面的示例中沒有顯示。
注意:如果數據源(由任何非jta事務管理器使用)通過JNDI查找並由Java EE容器管理,那麼它應該是非事務性的,因爲是Spring框架(而不是Java EE容器)管理事務。
本例中的txManager bean是HibernateTransactionManager類型。與DataSourceTransactionManager需要對數據源的引用一樣,HibernateTransactionManager需要對SessionFactory的引用。下面的例子聲明瞭sessionFactory和txManager bean:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
如果您使用Hibernate和Java EE容器管理的JTA事務,您應該使用與前面的JTA JDBC示例相同的JtaTransactionManager,如下面的示例所示:
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
注意:如果您使用JTA,那麼您的事務管理器定義應該看起來是一樣的,不管您使用什麼數據訪問技術,它是JDBC、Hibernate JPA還是任何其他受支持的技術。這是因爲JTA事務是全局事務,它可以徵募任何事務資源。
在所有這些情況下,應用程序代碼不需要更改。您可以僅通過更改配置來更改事務的管理方式,即使更改意味着從本地事務轉移到全局事務,或者反之亦然。
1.3。將資源與事務同步
如何創建不同的事務管理器,以及如何將它們鏈接到需要同步到事務的相關資源(例如,DataSourceTransactionManager到JDBC數據源,HibernateTransactionManager到Hibernate SessionFactory,等等)現在應該很清楚了。本節描述應用程序代碼(通過使用JDBC、Hibernate或JPA等持久性API,直接或間接地)如何確保正確地創建、重用和清理這些資源。本節還討論瞭如何(可選地)通過相關的PlatformTransactionManager觸發事務同步。
1.3.1。高級的同步方法
首選的方法是使用Spring最高級的基於模板的持久性集成api,或者使用帶有事務感知的工廠bean或代理的本機ORM api來管理本機資源工廠。這些事務感知解決方案在內部處理資源的創建和重用、清理、資源的可選事務同步和異常映射。因此,用戶數據訪問代碼不必處理這些任務,但可以只關注非樣板持久性邏輯。通常,您使用本機ORM API,或者通過使用JdbcTemplate採用JDBC訪問的模板方法。這些解決方案將在本參考文檔的後續章節中詳細介紹。
1.3.2。低級的同步方法
諸如DataSourceUtils(用於JDBC)、EntityManagerFactoryUtils(用於JPA)、SessionFactoryUtils(用於Hibernate)等類存在於較低的級別。當你想讓應用程序代碼直接處理原生資源類型的持久性API,您使用這些類來確保適當的Spring Framework-managed實例,事務是(可選)同步的,在這個過程中發生的和異常正確映射到一個一致的API。
例如,對於JDBC,您可以使用Spring的org.springframe . JDBC . DataSource,而不是調用數據源上的getConnection()方法的傳統JDBC方法。DataSourceUtils類,如下:
Connection conn = DataSourceUtils.getConnection(dataSource);
如果現有事務已經有一個與之同步(鏈接)的連接,則返回該實例。否則,方法調用將觸發新連接的創建,該連接(可選地)與任何現有事務同步,並可用於隨後在同一事務中重用。如前所述,任何SQLException都被包裝在Spring框架中,無法獲得jdbcconnectionexception,這是Spring框架中未檢查的DataAccessException類型的層次結構之一。這種方法提供的信息比從SQLException獲得的信息要多,並且確保了跨數據庫甚至跨不同持久性技術的可移植性。
這種方法也可以在沒有Spring事務管理的情況下工作(事務同步是可選的),因此無論您是否將Spring用於事務管理,都可以使用它。
當然,一旦您使用了Spring的JDBC支持、JPA支持或Hibernate支持,您通常不喜歡使用DataSourceUtils或其他幫助類,因爲您更樂於使用Spring抽象而不是直接使用相關api。例如,如果您使用Spring JdbcTemplate或jdbc。爲了簡化JDBC的使用,正確的連接檢索是在後臺進行的,不需要編寫任何特殊的代碼。
1.3.3。TransactionAwareDataSourceProxy
最底層是TransactionAwareDataSourceProxy類。這是目標數據源的代理,它包裝目標數據源以增加對spring管理的事務的感知。在這方面,它類似於Java EE服務器提供的事務性JNDI數據源。
除了必須調用現有代碼並傳遞標準的JDBC數據源接口實現時,您幾乎不需要或不想使用這個類。在這種情況下,這段代碼可能是可用的,但是參與了spring管理的事務。您可以使用前面提到的高級抽象來編寫新代碼。
1.4。聲明式事務管理
注意:大多數Spring框架用戶選擇聲明式事務管理。這個選項對應用程序代碼的影響最小,因此最符合無創輕量級容器的理想。
Spring框架的聲明性事務管理是通過Spring面向方面編程(AOP)實現的。但是,由於事務性方面的代碼是隨Spring框架發佈而來的,並且可以以樣板方式使用,所以AOP概念通常不需要理解就可以有效地使用這些代碼。
Spring框架的聲明式事務管理類似於EJB CMT,因爲您可以在單個方法級別指定事務行爲(或缺少事務行爲)。如果需要,可以在事務上下文中調用setRollbackOnly()。兩種類型的事務管理的區別是:
- 與綁定到JTA的EJB CMT不同,Spring框架的聲明性事務管理可以在任何環境中工作。它可以使用JTA事務或本地事務(通過使用JDBC、JPA或Hibernate調整配置文件)。
- 您可以將Spring框架聲明性事務管理應用於任何類,而不僅僅是ejb之類的特殊類。
- Spring框架提供了聲明式回滾規則,這是一個沒有等效EJB的特性。提供了對回滾規則的程序性和聲明性支持。
- Spring框架允許您使用AOP定製事務行爲。例如,您可以在事務回滾的情況下插入自定義行爲。您還可以添加任意的建議以及事務建議。使用EJB CMT,您不能影響容器的事務管理,除非使用setRollbackOnly()。
- 與高端應用服務器不同,Spring框架不支持在遠程調用之間傳播事務上下文。如果您需要此功能,我們建議您使用EJB。但是,在使用這種特性之前要仔細考慮,因爲通常不希望事務跨越遠程調用。
回滾規則的概念非常重要。它們允許您指定哪些異常(以及可拋出的異常)應該導致自動回滾。您可以在配置中以聲明的方式指定它,而不是在Java代碼中。因此,儘管您仍然可以調用TransactionStatus對象上的setRollbackOnly()來回滾當前事務,但通常您可以指定一條規則,即MyApplicationException必須總是導致回滾。此選項的顯著優點是業務對象不依賴於事務基礎結構。例如,它們通常不需要導入Spring事務api或其他Spring api。
雖然EJB容器默認行爲會自動回滾系統異常上的事務(通常是運行時異常),但是EJB CMT不會自動回滾應用程序異常上的事務(即除java.rmi.RemoteException之外的已檢查異常)。雖然聲明性事務管理的Spring默認行爲遵循EJB約定(僅在未檢查的異常時自動回滾),但是定製此行爲通常很有用。
1.4.1。理解Spring框架的聲明性事務實現
僅僅告訴您使用@Transactional註釋註釋您的類、將@EnableTransactionManagement添加到您的配置並期望您理解它是如何工作的是不夠的。爲了提供更深入的理解,本節將解釋在發生與事務相關的問題時Spring框架的聲明性事務基礎結構的內部工作方式。
關於Spring框架的聲明性事務支持,需要掌握的最重要的概念是,這種支持是通過AOP代理啓用的,而事務通知是由元數據(目前是基於XML或註釋的)驅動的。AOP與事務元數據的結合產生了一個AOP代理,它使用一個TransactionInterceptor和一個適當的PlatformTransactionManager實現來驅動圍繞方法調用的事務。
AOP部分將討論Spring AOP。
下圖顯示了調用事務代理上的方法的概念視圖:
1.4.2。聲明性事務實現的示例
考慮以下接口及其伴隨的實現。本例使用Foo和Bar類作爲佔位符,這樣您就可以專注於事務的使用,而不必關注特定的域模型。對於本例,DefaultFooService類在每個實現的方法體中拋出UnsupportedOperationException實例的事實是好的。該行爲允許您查看創建的事務,然後回滾到UnsupportedOperationException實例中。下面的清單顯示了FooService接口:
// the service interface that we want to make transactional
package x.y.service;
public interface FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}
下面的例子展示了上述接口的實現:
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Foo getFoo(String fooName) {
// ...
}
@Override
public Foo getFoo(String fooName, String barName) {
// ...
}
@Override
public void insertFoo(Foo foo) {
// ...
}
@Override
public void updateFoo(Foo foo) {
// ...
}
}
假設FooService接口的前兩個方法getFoo(String)和getFoo(String, String)必須在具有隻讀語義的事務上下文中執行,而其他方法insertFoo(Foo)和updateFoo(Foo)必須在具有讀寫語義的事務上下文中執行。下面幾段詳細解釋了以下配置:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- ensure that the above transactional advice runs for any execution
of an operation defined by the FooService interface -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>
<!-- don't forget the DataSource -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>
<!-- similarly, don't forget the PlatformTransactionManager -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
檢查前面的配置。它假設您希望使一個服務對象(fooService bean)成爲事務性的。要應用的事務語義封裝在<tx:advice/>定義中。<tx:advice/>定義的意思是:“所有方法,從get開始,都要在只讀事務的上下文中執行,所有其他方法都要用默認的事務語義執行”。<tx:advice/>標記的transaction-manager屬性被設置爲將要驅動事務的PlatformTransactionManager bean的名稱(在本例中是txManager bean)。
注意:如果您想要連接的平臺transactionManager的bean名稱爲transactionManager,那麼您可以在事務通知(<tx:advice/>)中省略transaction-manager屬性。如果要連接的PlatformTransactionManager bean具有任何其他名稱,則必須顯式地使用transaction-manager屬性,如前面的示例所示。
<aop:config/>定義確保txAdvice bean定義的事務通知在程序中的適當位置執行。首先,定義一個與FooService接口(fooServiceOperation)中定義的任何操作的執行相匹配的切入點。然後使用advisor工具將切入點與txAdvice關聯起來。結果表明,在執行fooServiceOperation時,將運行txAdvice定義的通知。
在<aop:pointcut/>元素中定義的表達式是一個AspectJ切入點表達式。有關Spring中切入點表達式的更多細節,請參閱AOP部分。
一個常見的需求是使整個服務層具有事務性。最好的方法是改變切入點表達式來匹配服務層中的任何操作。下面的例子演示瞭如何做到這一點:
<aop:config>
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
注意:在前面的示例中,假設所有服務接口都在x.y中定義。服務包。有關更多細節,請參見AOP部分。
現在我們已經分析了配置,您可能會問自己,“所有這些配置實際上做了什麼?”
前面顯示的配置用於圍繞從fooService bean定義創建的對象創建事務代理。代理使用事務通知進行配置,以便在代理上調用適當的方法時,根據與該方法關聯的事務配置啓動、掛起、標記爲只讀等事務。請考慮以下測試驅動前面顯示的配置的程序:
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);
FooService fooService = (FooService) ctx.getBean("fooService");
fooService.insertFoo (new Foo());
}
}
運行上述程序的輸出應該類似於以下內容(爲了清晰起見,已截斷了DefaultFooService類的insertFoo(..)方法拋出的UnsupportedOperationException的Log4J輸出和堆棧跟蹤):
<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors
<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]
<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource
Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)
1.4.3 回滾聲明性事務
前一節概述瞭如何在應用程序中聲明性地爲類(通常是服務層類)指定事務設置的基礎知識。本節描述如何以簡單的聲明式方式控制事務的回滾。
要向Spring框架的事務基礎結構表明要回滾事務的工作,建議的方法是從當前在事務上下文中執行的代碼拋出異常。Spring框架的事務基礎結構代碼在彈出調用堆棧並決定是否將事務標記爲回滾時捕獲任何未處理的異常。
在其缺省配置中,Spring框架的事務基礎結構代碼僅在運行時未檢查異常的情況下將事務標記爲回滾。也就是說,當拋出的異常是RuntimeException的一個實例或子類時。(默認情況下,錯誤實例也會導致回滾)。從事務方法拋出的已檢查異常不會導致默認配置中的回滾。
您可以準確地配置哪些異常類型將事務標記爲回滾,包括已檢查的異常。下面的XML片段演示如何爲已檢查的、特定於應用程序的異常類型配置回滾:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
如果在拋出異常時不希望事務回滾,還可以指定“無回滾規則”。下面的例子告訴Spring框架的事務基礎結構,即使面對未處理的InstrumentNotFoundException,也要提交相應的事務:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
當Spring框架的事務基礎結構捕獲異常並參考配置的回滾規則以確定是否將事務標記爲回滾時,最強的匹配規則獲勝。因此,在以下配置的情況下,除了一個儀表notfoundexception之外的任何異常都會導致相應事務的回滾:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>
您還可以通過編程方式指示所需的回滾。儘管這個過程很簡單,但是它具有很強的侵入性,並且將您的代碼與Spring框架的事務基礎結構緊密地耦合在一起。下面的示例演示如何以編程方式指示所需的回滾:
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
強烈建議您儘可能使用聲明性方法回滾。如果您絕對需要,編程回滾是可用的,但是它的使用與實現一個乾淨的基於pojo的體系結構背道而馳。
1.4.4 爲不同的bean配置不同的事務語義
考慮這樣一種場景:您有許多服務層對象,並且希望對每個對象應用完全不同的事務配置。可以通過定義不同的<aop:advisor/>元素來實現這一點,這些元素具有不同的切入點和advice-ref屬性值。
作爲比較,首先假設所有服務層類都定義在根x.y中。服務包。要使所有在該包(或子包)中定義的類實例以及名稱以服務結尾的bean具有默認的事務配置,您可以編寫以下代碼:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* x.y.service..*Service.*(..))"/>
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>
<!-- these two beans will be transactional... -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<bean id="barService" class="x.y.service.extras.SimpleBarService"/>
<!-- ... and these two beans won't -->
<bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
<bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->
</beans>
下面的示例展示瞭如何使用完全不同的事務設置配置兩個不同的bean:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="defaultServiceOperation"
expression="execution(* x.y.service.*Service.*(..))"/>
<aop:pointcut id="noTxServiceOperation"
expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>
<aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>
<aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>
</aop:config>
<!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this bean will also be transactional, but with totally different transactional settings -->
<bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>
<tx:advice id="defaultTxAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<tx:advice id="noTxAdvice">
<tx:attributes>
<tx:method name="*" propagation="NEVER"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->
</beans>
1.4.5 <tx:advice/> Settings
本節總結了可以使用<tx:advice/>標記指定的各種事務設置。默認<tx:advice/>設置爲:
- 傳播設置是必需的。
- 隔離級別是默認的。
- 事務是讀寫的。
- 事務超時默認爲底層事務系統的默認超時,如果不支持超時,則爲none。
- 任何RuntimeException都會觸發回滾,而任何已檢查的異常則不會。
您可以更改這些默認設置。下表總結了嵌套在<tx:advice/>和<tx:attributes/>標籤內的<tx:method/>標籤的各種屬性:
Attribute | Required? | Default | Description |
---|---|---|---|
|
Yes |
要與事務屬性關聯的方法名。通配符(*)可用於將相同的事務屬性設置與許多方法(例如,get*、handle*、on*Event等)關聯起來。 | |
|
No |
|
事務傳播行爲。 |
|
No |
|
事務超時(秒)。僅適用於傳播REQUIRED或REQUIRES_NEW。 |
|
No |
-1 |
Transaction timeout (seconds). Only applicable to propagation |
|
No |
false |
讀寫事務與只讀事務。僅適用於REQUIRED或REQUIRES_NEW。 |
|
No |
觸發回滾的異常實例的逗號分隔列表。例如,com.foo.MyBusinessException ServletException。 |
|
|
No |
|
1.4.6 使用@Transactional
除了基於xml的聲明式事務配置方法之外,還可以使用基於註釋的方法。直接在Java源代碼中聲明事務語義使聲明更接近受影響的代碼。不存在過多耦合的危險,因爲以事務方式使用的代碼幾乎總是以這種方式部署的。
注意:標準javax.transaction.Transactional註釋作爲Spring自己的註釋的替代。更多細節請參閱JTA 1.2文檔。
使用@Transactional註釋所提供的易用性最好通過一個示例來說明,下面的文本將對此進行解釋。考慮以下類定義:
// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {
Foo getFoo(String fooName) {
// ...
}
Foo getFoo(String fooName, String barName) {
// ...
}
void insertFoo(Foo foo) {
// ...
}
void updateFoo(Foo foo) {
// ...
}
}
如上所述,該註釋用於類級別,它表示聲明類(及其子類)的所有方法的默認值。另外,每個方法都可以得到單獨的註釋。請注意,類級別的註釋不應用於類層次結構上的祖先類;在這種情況下,爲了參與子類級別的註釋,需要在本地重新聲明方法。
當像上面這樣的POJO類在Spring上下文中定義爲bean時,您可以通過@Configuration類中的@EnableTransactionManagement註釋使bean實例具有事務性。有關詳細信息,請參閱 javadoc。
在XML配置中,<tx:annotation-driven/>標籤提供了類似的便利:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
注意:如果要連接的平臺transactionManager的bean名稱爲transactionManager,則可以省略<tx: annotationdriven />標記中的transaction-manager屬性。如果要依賴注入的PlatformTransactionManager bean有任何其他名稱,就必須使用transaction-manager屬性,如前面的示例所示。
方法可見性和@Transactional
使用代理時,應該只將@Transactional註釋應用於具有公共可見性的方法。如果使用@Transactional註釋註釋受保護的、私有的或包可見的方法,則不會引發錯誤,但是註釋的方法不顯示配置的事務設置。如果需要註釋非公共方法,可以考慮使用AspectJ(後面會介紹)。
您可以將@Transactional註釋應用於接口定義、接口上的方法、類定義或類上的公共方法。然而,僅僅存在@Transactional註釋並不足以激活事務行爲。@Transactional註釋只是一些運行時基礎設施可以使用的元數據,這些運行時基礎設施支持@ transaction,並且可以使用元數據配置適當的bean和事務行爲。在前面的示例中,<tx:註釋驅動/>元素切換到事務行爲。
注意:Spring團隊建議只使用@Transactional註釋註釋具體類(和具體類的方法),而不是註釋接口。當然,您可以將@Transactional註釋放在接口(或接口方法)上,但這僅在使用基於接口的代理時纔有效。Java註釋的事實並不意味着繼承接口,如果使用基於類的代理(proxy-target-class = " true ")或weaving-based方面(模式=“aspectj”),事務設置不認可的代理和編織的基礎設施,和對象不是包在一個事務代理。
在代理模式(這是缺省模式)中,只攔截通過代理傳入的外部方法調用。這意味着自調用(實際上,目標對象中的一個方法調用目標對象的另一個方法)在運行時不會導致實際的事務,即使被調用的方法被標記爲@Transactional。另外,代理必須被完全初始化以提供預期的行爲,因此您不應該在初始化代碼(即@PostConstruct)中依賴該特性。
如果您希望自調用也用事務包裝,那麼可以考慮使用AspectJ模式(請參閱下表中的mode屬性)。在這種情況下,首先沒有代理。相反,目標類被編織(即其字節碼被修改)來將@Transactional轉換爲任何類型方法的運行時行爲。
XML Attribute | Annotation Attribute | Default | Description |
---|---|---|---|
|
N/A (see |
|
要使用的事務管理器的名稱。只有在事務管理器的名稱不是transactionManager時才需要,如前面的示例所示。 |
|
|
|
默認模式(代理)處理要通過使用Spring的AOP框架代理的帶註釋的bean(遵循代理語義,如前所述,僅應用於通過代理傳入的方法調用)。替代模式(aspectj)用Spring的aspectj事務方面編織受影響的類,修改目標類的字節碼以應用於任何類型的方法調用。AspectJ編織需要類路徑中的spring-aspect .jar以及啓用加載時編織(或編譯時編織)。(有關如何設置加載時編織的詳細信息,請參閱Spring配置)。 |
|
|
|
僅適用於代理模式。控制使用@Transactional註釋爲類創建什麼類型的事務代理。如果將proxy-target-class屬性設置爲true,則創建基於類的代理。如果proxy-target-class爲false,或者屬性被省略,那麼就會創建標準的JDK基於接口的代理。(有關不同代理類型的詳細檢查,請參閱代理機制。) |
|
|
|
定義應用於使用@Transactional註釋的bean的事務通知的順序。(有關AOP通知排序的規則的更多信息,請參見通知排序。)沒有指定的順序意味着AOP子系統決定通知的順序。 |
注意:處理@Transactional註釋的默認通知模式是proxy,它只允許通過代理攔截調用。同一類內的本地調用不能通過這種方式被截獲。對於更高級的攔截模式,可以考慮結合編譯時或加載時編織切換到aspectj模式。
注意:代理目標類屬性控制使用@Transactional註釋爲類創建什麼類型的事務代理。如果將代理目標類設置爲true,則創建基於類的代理。如果proxy-target-class爲false,或者該屬性被省略,則創建標準JDK基於接口的代理。(參見[aop-proxy - ying]討論不同的代理類型。)
注意:@EnableTransactionManagement和<tx:註釋驅動/>僅在定義@Transactional的相同應用程序上下文中查找@Transactional。這意味着,如果您將註釋驅動的配置放在DispatcherServlet的WebApplicationContext中,它將只檢查控制器中的@Transactional bean,而不檢查服務中的@Transactional bean。更多信息請參見MVC。
在計算方法的事務設置時,最派生的位置優先。在下面的示例中,DefaultFooService類在類級別使用只讀事務的設置進行註釋,但是同一類中updateFoo(Foo)方法上的@Transactional註釋優先於在類級別定義的事務設置。
@Transactional(readOnly = true)
public class DefaultFooService implements FooService {
public Foo getFoo(String fooName) {
// ...
}
// these settings have precedence for this method
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateFoo(Foo foo) {
// ...
}
}
@Transactional設置
@Transactional註釋是指定接口、類或方法必須具有事務語義的元數據(例如,“在調用此方法時啓動全新的只讀事務,掛起任何現有事務”)。默認的@Transactional設置如下:
- 傳播設置是PROPAGATION_REQUIRED。
- 隔離級別是ISOLATION_DEFAULT。
- 事務是讀寫的。
- 事務超時默認爲底層事務系統的默認超時,如果不支持超時,則爲none。
- 任何RuntimeException都會觸發回滾,而任何已檢查的異常則不會。
您可以更改這些默認設置。下表總結了@Transactional註釋的各種屬性:
Property | Type | Description |
---|---|---|
|
指定要使用的事務管理器的可選限定符。 |
|
|
Optional propagation setting. |
|
|
|
Optional isolation level. Applies only to propagation values of |
|
|
Optional transaction timeout. Applies only to propagation values of |
|
|
Read-write versus read-only transaction. Only applicable to values of |
|
Array of |
Optional array of exception classes that must cause rollback. |
|
Array of class names. The classes must be derived from |
Optional array of names of exception classes that must cause rollback. |
|
Array of |
Optional array of exception classes that must not cause rollback. |
|
Array of |
Optional array of names of exception classes that must not cause rollback. |
目前,您不能顯式地控制事務的名稱,其中“名稱”表示出現在事務監視器(如WebLogic的事務監視器)和日誌輸出中的事務名稱。對於聲明性事務,事務名始終是完全限定的類名+。事務通知類的方法名。例如,如果BusinessService類的handlePayment(..)方法啓動了一個事務,該事務的名稱將是:com.example.BusinessService.handlePayment。
使用@Transactional的多個事務管理器
大多數Spring應用程序只需要一個事務管理器,但是在某些情況下,您可能希望在一個應用程序中有多個獨立的事務管理器。您可以使用@Transactional註釋的value屬性選擇性地指定要使用的PlatformTransactionManager的標識。這可以是bean名,也可以是事務管理器bean的限定符值。例如,使用限定符符號,您可以在應用程序上下文中將下列Java代碼與下列事務管理器bean聲明結合起來:
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
}
下面的清單顯示了bean聲明:
<tx:annotation-driven/>
<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="order"/>
</bean>
<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="account"/>
</bean>
在本例中,TransactionalService上的兩個方法在單獨的事務管理器下運行,由訂單和帳戶限定符區分。如果沒有找到特別符合條件的PlatformTransactionManager bean,則仍然使用缺省的<tx:註釋驅動的>目標bean名transactionManager。
自定義快捷鍵的註釋
如果您發現在許多不同的方法上重複使用與@Transactional相同的屬性,Spring的元註釋支持允許您爲特定的用例定義自定義快捷註釋。例如,考慮以下注釋定義:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}
前面的註釋讓我們把上一節的例子寫成如下:
public class TransactionalService {
@OrderTx
public void setSomething(String name) {
// ...
}
@AccountTx
public void doSomething() {
// ...
}
}
在前面的示例中,我們使用語法來定義事務管理器限定符,但是我們也可以包含傳播行爲、回滾規則、超時和其他特性。
1.4.7 事務傳播
本節描述Spring中事務傳播的一些語義。注意,本節並不是對事務傳播的介紹。相反,它詳細描述了Spring中關於事務傳播的一些語義。
在spring管理的事務中,請注意物理事務和邏輯事務之間的差異,以及傳播設置如何應用於這種差異。
理解PROPAGATION_REQUIRED
PROPAGATION_REQUIRED執行物理事務,如果當前範圍內還不存在事務,則在本地執行物理事務,或者參與爲更大範圍定義的現有“外部”事務。在同一線程中的公共調用堆棧安排中,這是一個很好的默認設置(例如,一個服務facade,它將委託給幾個存儲庫方法,其中所有底層資源都必須參與服務級事務)。
注意:默認情況下,參與事務連接外部範圍的特徵,默認忽略本地隔離級別、超時值或只讀標誌(如果有的話)。如果您希望在參與具有不同隔離級別的現有事務時拒絕隔離級別聲明,請考慮在您的事務管理器上將validateExistingTransactions標誌切換爲true。這種非寬鬆模式還拒絕只讀不匹配(即,內部的讀寫事務試圖參與只讀外部範圍)。
當propagation設置爲PROPAGATION_REQUIRED時,將爲應用該設置的每個方法創建一個邏輯事務範圍。每個這樣的邏輯事務範圍都可以單獨確定僅回滾的狀態,外部事務範圍在邏輯上獨立於內部事務範圍。對於標準的PROPAGATION_REQUIRED行爲,所有這些作用域都映射到同一個物理事務。因此,內部事務範圍中設置的僅回滾標記確實會影響外部事務實際提交的機會。
但是,在內部事務範圍設置僅回滾標記的情況下,外部事務沒有決定回滾本身,因此回滾(由內部事務範圍靜默觸發)是意外的。這時拋出一個對應的tedrollbackexception。這是預期的行爲,因此事務的調用者永遠不會被誤導,以爲提交是在實際沒有執行的情況下執行的。因此,如果一個內部事務(外部調用者不知道它的存在)悄悄地將一個事務標記爲僅回滾,那麼外部調用者仍然調用commit。外部調用者需要接收一個意想不到的drollbackexception,以清楚地表明執行了回滾。
理解PROPAGATION_REQUIRES_NEW
與PROPAGATION_REQUIRED不同,PROPAGATION_REQUIRES_NEW總是爲每個受影響的事務範圍使用獨立的物理事務,而從不參與外部範圍的現有事務。在這種安排中,底層的資源事務是不同的,因此可以獨立地提交或回滾,外部事務不受內部事務回滾狀態的影響,內部事務的鎖在完成後立即釋放。這樣一個獨立的內部事務還可以聲明自己的隔離級別、超時和只讀設置,而不會繼承外部事務的特徵。
理解PROPAGATION_NESTED
propagation_嵌套使用一個具有多個保存點的物理事務,它可以回滾到這些保存點。這樣的部分回滾允許內部事務範圍觸發其範圍的回滾,而外部事務能夠繼續物理事務,儘管已經回滾了一些操作。該設置通常映射到JDBC保存點,因此它只適用於JDBC資源事務。看看Spring的 DataSourceTransactionManager
。
1.4.8 建議事務操作
假設您希望同時執行事務操作和一些基本的分析建議。如何在<tx:註釋驅動/>的上下文中實現這一點?
當您調用updateFoo(Foo)方法時,您希望看到以下操作:
- 啓動已配置的概要方面。
- 執行事務通知。
- 執行被建議對象上的方法。
- 事務提交。
- 分析方面報告整個事務方法調用的確切持續時間。
本章不涉及對AOP的任何詳細解釋(除非它適用於事務)。有關AOP配置和AOP的詳細內容,請參閱AOP。
下面的代碼顯示了前面討論的簡單概要方面:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;
public class SimpleProfiler implements Ordered {
private int order;
// allows us to control the ordering of advice
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
// this method is the around advice
public Object profile(ProceedingJoinPoint call) throws Throwable {
Object returnValue;
StopWatch clock = new StopWatch(getClass().getName());
try {
clock.start(call.toShortString());
returnValue = call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
return returnValue;
}
}
通知的排序通過有序接口進行控制。有關通知訂購的詳細信息,請參 Advice ordering.。
下面的配置創建了一個fooService bean,它按照所需的順序應用了分析和事務方面:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this is the aspect -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- execute before the transactional advice (hence the lower order number) -->
<property name="order" value="1"/>
</bean>
<tx:annotation-driven transaction-manager="txManager" order="200"/>
<aop:config>
<!-- this advice will execute around the transactional advice -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</aop:config>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
您可以以類似的方式配置任意數量的附加方面。
下面的示例創建了與前兩個示例相同的設置,但使用的是純XML聲明方法:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the profiling advice -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- execute before the transactional advice (hence the lower order number) -->
<property name="order" value="1"/>
</bean>
<aop:config>
<aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
<!-- will execute after the profiling advice (c.f. the order attribute) -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
<!-- order value is higher than the profiling aspect -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- other <bean/> definitions such as a DataSource and a PlatformTransactionManager here -->
</beans>
前面的配置的結果是一個fooService bean,它按照這個順序應用了分析和事務方面。如果希望分析建議在事務建議之後執行,在事務建議之前執行,那麼可以交換分析方面bean的order屬性的值,使其高於事務建議的order值。
您可以以類似的方式配置其他方面。
1.4.9。通過AspectJ使用@Transactional
您還可以通過AspectJ方面在Spring容器之外使用Spring框架的@Transactional支持。爲此,首先使用@Transactional註釋註釋您的類(以及可選的類方法),然後使用在spring-aspects.jar文件中org.springframework.transaction.aspectj.AnnotationTransactionAspect。您還必須使用事務管理器配置方面。您可以使用Spring框架的IoC容器來處理依賴注入方面。配置事務管理方面的最簡單方法是使用<tx:annotation-driven/>元素,並將mode屬性指定爲aspectj,如Using @Transactional中所述。因爲我們在這裏關注的是在Spring容器之外運行的應用程序,所以我們將向您展示如何通過編程實現這一點。
注意:在繼續之前,您可能希望分別使用@Transactional和AOP進行閱讀。
下面的例子演示瞭如何創建事務管理器並配置AnnotationTransactionAspect來使用它:
// construct an appropriate transaction manager
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());
// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);
注意:當您使用這個方面時,您必須註釋實現類(或該類中的方法或兩者),而不是類實現的接口(如果有的話)。AspectJ遵循Java的規則,接口上的註釋不是繼承的。
類上的@Transactional註釋爲類中任何公共方法的執行指定默認的事務語義。
類中的方法上的@Transactional註釋覆蓋類註釋(如果存在)給出的默認事務語義。您可以註釋任何方法,而不管其可見性如何。
要使用AnnotationTransactionAspect編織應用程序,必須使用AspectJ構建應用程序(請參閱AspectJ開發指南),或者使用加載時編織。有關使用AspectJ進行加載時編織的討論,請參閱Spring框架中使用AspectJ進行加載時編織。
1.5 編程式事務管理
Spring框架提供了兩種程序化事務管理的方法,它們是:
- TransactionTemplate。
- 一個直接的PlatformTransactionManager實現。
Spring團隊通常建議使用TransactionTemplate進行程序化事務管理。第二種方法類似於使用JTA UserTransaction API,儘管異常處理沒有那麼麻煩。
1.5.1 使用TransactionTemplate
TransactionTemplate採用與其他Spring模板(如JdbcTemplate)相同的方法。它使用回調方法(將應用程序代碼從必須進行樣板獲取和釋放事務性資源的過程中解放出來),並生成由意圖驅動的代碼,因爲您的代碼只關注您想要做的事情。
注意:如下面的示例所示,使用TransactionTemplate絕對可以將您與Spring的事務基礎結構和api結合起來。程序化事務管理是否適合您的開發需求是您必須自己做出的決定。
必須在事務上下文中執行並顯式使用TransactionTemplate的應用程序代碼類似於下面的示例。作爲應用程序開發人員,您可以編寫一個TransactionCallback實現(通常表示爲一個匿名內部類),其中包含需要在事務上下文中執行的代碼。然後可以將定製TransactionCallback的實例傳遞給TransactionTemplate上公開的execute(..)方法。下面的例子演示瞭如何做到這一點:
public class SimpleService implements Service {
// single TransactionTemplate shared amongst all methods in this instance
private final TransactionTemplate transactionTemplate;
// use constructor-injection to supply the PlatformTransactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
// the code in this method executes in a transactional context
public Object doInTransaction(TransactionStatus status) {
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
如果沒有返回值,可以使用方便的TransactionCallbackWithoutResult類和一個匿名類,如下所示:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
回調中的代碼可以通過調用提供的TransactionStatus對象上的setRollbackOnly()方法來回滾事務,如下所示:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessException ex) {
status.setRollbackOnly();
}
}
});
指定事務設置
可以在TransactionTemplate上以編程方式或在配置中指定事務設置(例如傳播模式、隔離級別、超時等)。默認情況下,TransactionTemplate實例具有默認的事務設置 default transactional settings。下面的示例顯示了對特定TransactionTemplate的事務設置的程序化自定義:
public class SimpleService implements Service {
private final TransactionTemplate transactionTemplate;
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
// the transaction settings can be set here explicitly if so desired
this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
this.transactionTemplate.setTimeout(30); // 30 seconds
// and so forth...
}
}
下面的例子通過使用Spring XML配置定義了帶有一些自定義事務設置的TransactionTemplate:
<bean id="sharedTransactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
<property name="timeout" value="30"/>
</bean>
然後您可以將sharedTransactionTemplate注入到需要的任意數量的服務中。
最後,TransactionTemplate類的實例是線程安全的,在這種情況下,實例不維護任何會話狀態。然而,TransactionTemplate實例維護配置狀態。因此,雖然許多類可能共享一個TransactionTemplate實例,但是如果一個類需要使用具有不同設置的TransactionTemplate(例如,不同的隔離級別),則需要創建兩個不同的TransactionTemplate實例。
1.5.2 使用PlatformTransactionManager
您還可以使用org.springframe .transaction。PlatformTransactionManager直接管理您的事務。爲此,通過一個bean引用將您使用的PlatformTransactionManager的實現傳遞給bean。然後,通過使用TransactionDefinition和TransactionStatus對象,您可以啓動事務、回滾和提交。下面的例子演示瞭如何做到這一點:
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
// execute your business logic here
}
catch (MyException ex) {
txManager.rollback(status);
throw ex;
}
txManager.commit(status);
1.6 在程序性和聲明性事務管理之間進行選擇
編程式事務管理通常是一個好主意,只有當您有少量的事務操作時。例如,如果您的web應用程序只需要事務來執行某些更新操作,那麼您可能不希望使用Spring或任何其他技術來設置事務代理。在這種情況下,使用TransactionTemplate可能是一種很好的方法。能夠顯式地設置事務名稱也只能通過使用程序化的事務管理方法來完成。
另一方面,如果您的應用程序有許多事務操作,那麼聲明性事務管理通常是值得的。它將事務管理置於業務邏輯之外,並且易於配置。當使用Spring框架而不是EJB CMT時,聲明性事務管理的配置成本將大大降低。
1.7 Transaction-bound事件
從Spring 4.2開始,事件的偵聽器可以綁定到事務的某個階段。典型的例子是在事務成功完成時處理事件。這樣做可以在當前事務的結果對偵聽器很重要時更靈活地使用事件。
可以使用@EventListener註釋註冊常規事件監聽器。如果需要將其綁定到事務,請使用@TransactionalEventListener。這樣做時,默認情況下偵聽器被綁定到事務的提交階段。
下一個例子展示了這個概念。假設組件發佈了一個訂單創建的事件,並且我們希望定義一個偵聽器,該偵聽器應該只在發佈該事件的事務成功提交後才處理該事件。下面的例子設置了這樣一個事件監聽器:
@Component
public class MyComponent {
@TransactionalEventListener
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
// ...
}
}
@TransactionalEventListener註釋公開了一個phase屬性,該屬性允許您自定義應該將偵聽器綁定到的事務的階段。有效的階段是BEFORE_COMMIT、AFTER_COMMIT(默認)、AFTER_ROLLBACK和AFTER_COMPLETION,它們聚合事務完成(無論是提交還是回滾)。
如果沒有事務在運行,則根本不會調用偵聽器,因爲我們不能遵從所需的語義。但是,您可以通過將註釋的fallbackExecution屬性設置爲true來覆蓋該行爲。
1.8 特定於應用服務器的集成
Spring的事務抽象通常與應用服務器無關。此外,Spring的JtaTransactionManager類(可以選擇性地對JTA UserTransaction和TransactionManager對象執行JNDI查找)自動檢測後一個對象的位置,該位置隨應用服務器的不同而不同。訪問JTA TransactionManager允許增強事務語義——特別是支持事務暫停。有關詳細信息,請參閱JtaTransactionManager javadoc。
pring的JtaTransactionManager是在Java EE應用服務器上運行的標準選擇,並且可以在所有通用服務器上運行。高級功能(如事務暫停)也可以在許多服務器上工作(包括GlassFish、JBoss和Geronimo),而不需要任何特殊配置。然而,對於完全支持的事務掛起和進一步的高級集成,Spring包含用於WebLogic Server和WebSphere的特殊適配器。下面幾節將討論這些適配器。
對於標準場景,包括WebLogic Server和WebSphere,考慮使用方便的<tx:jta-transaction-manager/>配置元素。配置後,此元素將自動檢測底層服務器,併爲平臺選擇可用的最佳事務管理器。這意味着您不需要顯式地配置特定於服務器的適配器類(如下面的部分所述)。相反,它們是自動選擇的,使用標準的JtaTransactionManager作爲默認回退。
1.8.1 IBM WebSphere
在WebSphere 6.1.0.9及以上版本中,推薦使用的Spring JTA事務管理器是WebSphereUowTransactionManager。這個特殊適配器使用IBM的UOWManager API,該API在WebSphere Application Server 6.1.0.9及更高版本中可用。有了這個適配器,IBM正式支持spring驅動的事務掛起(由PROPAGATION_REQUIRES_NEW啓動的掛起和恢復)。
1.8.2 Oracle WebLogic Server
在WebLogic Server 9.0或更高版本上,您通常會使用WebLogicJtaTransactionManager而不是股票JtaTransactionManager類。常規JtaTransactionManager的這個特殊的特定於weblogic的子類支持Spring在weblogic管理的事務環境中的事務定義的全部功能,超越了標準的JTA語義。特性包括事務名稱、每個事務的隔離級別,以及在所有情況下正確恢復事務。
1.9 常見問題的解決方案
本節描述一些常見問題的解決方案。
1.9.1 爲特定數據源使用錯誤的事務管理器
根據您對事務技術和需求的選擇,使用正確的PlatformTransactionManager實現。如果使用得當,Spring框架僅僅提供了一個簡單且可移植的抽象。如果使用全局事務,則必須使用org.springframe .transaction.jta。用於所有事務操作的JtaTransactionManager類(或其特定於應用程序服務器的子類)。否則,事務基礎結構將嘗試對容器數據源實例等資源執行本地事務。這樣的本地事務沒有意義,好的應用程序服務器會將它們視爲錯誤。
1.10 進一步的資源
有關Spring框架的事務支持的更多信息,請參見:
- Spring中的分佈式事務,有和沒有XA是一個JavaWorld演示,其中Spring的David Syer指導您瞭解Spring應用程序中分佈式事務的七個模式,其中三個帶有XA,四個沒有。
- 《Java事務設計策略》是InfoQ提供的一本書,該書對Java事務進行了節奏緊湊的介紹。它還附帶了一些示例,說明如何使用Spring框架和EJB3配置和使用事務。
2. DAO支持
Spring中的數據訪問對象(DAO)支持旨在以一致的方式簡化數據訪問技術(如JDBC、Hibernate或JPA)的使用。這使您可以相當容易地在上述持久性技術之間進行切換,而且還使您不必擔心捕獲特定於每種技術的異常。
2.1 一致的異常層次結構
Spring提供了從特定於技術的異常(比如SQLException)到它自己的異常類層次結構的方便轉換,後者將DataAccessException作爲根異常。這些異常封裝了原始的異常,這樣就不會有丟失關於可能出錯的任何信息的風險。
除了JDBC異常,Spring還可以包裝JPA和hibernate特定的異常,將它們轉換成一組集中的運行時異常。這使您可以僅在適當的層中處理大多數不可恢復的持久性異常,而不必在DAOs中使用煩人的樣板捕獲拋出塊和異常聲明。(不過,您仍然可以在任何需要的地方捕獲和處理異常。)如上所述,JDBC異常(包括特定於數據庫的方言)也被轉換爲相同的層次結構,這意味着您可以在一致的編程模型中使用JDBC執行某些操作。
前面的討論適用於Spring對各種ORM框架的支持中的各種模板類。如果使用基於攔截器的類,應用程序必須關心如何處理hibernateexception和persistenceexception本身,最好分別委託給SessionFactoryUtils的convertHibernateAccessException(..)或convertJpaAccessException()方法。這些方法將異常轉換爲與org.springframework中的異常兼容的異常。dao異常層次結構。當persistenceexception未被檢查時,它們也會被拋出(儘管在異常方面犧牲了一般的DAO抽象)。
下圖顯示了Spring提供的異常層次結構。(注意,圖中詳細描述的類層次結構僅顯示整個DataAccessException層次結構的一個子集。)
2.2 用於配置DAO或存儲庫類的註釋
確保數據訪問對象(DAOs)或存儲庫提供異常轉換的最佳方法是使用@Repository註釋。該註釋還允許組件掃描支持查找和配置DAOs和存儲庫,而不必爲它們提供XML配置項。下面的例子演示瞭如何使用@Repository註釋:
@Repository
public class SomeMovieFinder implements MovieFinder {
// ...
}
任何DAO或存儲庫實現都需要訪問持久性資源,這取決於使用的持久性技術。例如,基於JDBC的存儲庫需要訪問JDBC數據源,而基於jpa的存儲庫需要訪問EntityManager。完成此任務的最簡單方法是通過使用@Autowired、@Inject、@Resource或@PersistenceContext註釋之一注入此資源依賴項。下面的例子適用於JPA存儲庫:
@Repository
public class JpaMovieFinder implements MovieFinder {
@PersistenceContext
private EntityManager entityManager;
// ...
}
如果你使用經典的Hibernate api,你可以注入SessionFactory,如下面的例子所示:
@Repository
public class HibernateMovieFinder implements MovieFinder {
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
// ...
}
我們在這裏展示的最後一個例子是典型的JDBC支持。可以將數據源注入到初始化方法或構造函數中,通過使用該數據源創建JdbcTemplate和其他數據訪問支持類(如SimpleJdbcCall和其他類)。下面的例子自動裝配一個數據源:
@Repository
public class JdbcMovieFinder implements MovieFinder {
private JdbcTemplate jdbcTemplate;
@Autowired
public void init(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// ...
}
注意:有關如何配置應用程序上下文以利用這些註釋的詳細信息,請參閱每種持久性技術的具體內容。
3.使用JDBC訪問數據
Spring Framework JDBC抽象提供的值最好由下表中列出的操作序列來顯示。該表顯示了Spring負責哪些操作以及哪些操作是您的職責。
表4 Spring JDBC--誰來做什麼?
Action | Spring | You |
---|---|---|
Define connection parameters. |
X |
|
Open the connection. |
X |
|
Specify the SQL statement. |
X |
|
Declare parameters and provide parameter values |
X |
|
Prepare and execute the statement. |
X |
|
Set up the loop to iterate through the results (if any). |
X |
|
Do the work for each iteration. |
X |
|
Process any exception. |
X |
|
Handle transactions. |
X |
|
Close the connection, the statement, and the resultset. |
X |
Spring框架負責處理所有底層細節,正是這些細節使得JDBC成爲如此乏味的API。
3.1 爲JDBC數據庫訪問選擇一種方法
您可以在幾種方法中進行選擇,以形成JDBC數據庫訪問的基礎。除了三種風格的JdbcTemplate之外,一種新的SimpleJdbcInsert和SimpleJdbcCall方法對數據庫元數據進行了優化,RDBMS對象風格採用了一種與JDO查詢設計類似的更面向對象的方法。一旦您開始使用這些方法中的一種,您仍然可以混合和匹配以包含來自不同方法的特性。所有的方法都需要兼容JDBC 2.0的驅動程序,一些高級特性需要JDBC 3.0驅動程序。
- JdbcTemplate是經典的、最流行的Spring JDBC方法。這種“最低級別”的方法和所有其他方法都在幕後使用JdbcTemplate。
- NamedParameterJdbcTemplate包裝了一個JdbcTemplate來提供命名參數,而不是傳統的JDBC ?佔位符。當您有一個SQL語句的多個參數時,這種方法提供了更好的文檔和易用性。
- SimpleJdbcInsert和SimpleJdbcCall優化數據庫元數據以限制必要配置的數量。這種方法簡化了編碼,因此只需要提供表或過程的名稱,並提供與列名匹配的參數映射。只有當數據庫提供了足夠的元數據時,這才能工作。如果數據庫不提供此元數據,則必須提供參數的顯式配置。
- RDBMS對象,包括MappingSqlQuery、SqlUpdate和StoredProcedure,要求您在初始化數據訪問層期間創建可重用的、線程安全的對象。此方法模仿JDO查詢,其中定義查詢字符串、聲明參數並編譯查詢。一旦您這樣做了,就可以使用各種參數值多次調用execute方法。
3.2 包的層次結構
Spring框架的JDBC抽象框架由四個不同的包組成:
- core:org.springframework.jdbc。核心包包含JdbcTemplate類及其各種回調接口,以及各種相關的類。一個名爲org.springframework.jdbc.core的子包。simple包含SimpleJdbcInsert和SimpleJdbcCall類。另一個名爲org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate類和相關的支持類。請參閱使用JDBC核心類來控制基本的JDBC處理和錯誤處理、JDBC批處理操作以及使用SimpleJdbc類簡化JDBC操作。
- datasource:org.springframework.jdbc.datasource包包含一個實用程序類,用於簡單的數據源訪問和各種簡單的數據源實現,您可以使用它在Java EE容器外測試和運行未經修改的JDBC代碼。一個名爲org.springfamework.jdbc.datasource的子包。嵌入式提供了通過使用Java數據庫引擎(如HSQL、H2和Derby)創建嵌入式數據庫的支持。參見控制數據庫連接和嵌入式數據庫支持。
- object:org.springframework.jdbc。對象包包含表示RDBMS查詢、更新和存儲過程的類,這些類是線程安全的、可重用的對象。請參閱將JDBC操作建模爲Java對象。此方法由JDO建模,儘管查詢返回的對象自然與數據庫斷開連接。這種高層的JDBC抽象依賴於org.springframework.jdbc.core包中的低層抽象。
- support:org.springframework.jdbc.support包提供了SQLException翻譯功能和一些實用工具類。JDBC處理期間拋出的異常被轉換成org.springframework中定義的異常。dao包。這意味着使用Spring JDBC抽象層的代碼不需要實現JDBC或特定於rdbms的錯誤處理。所有轉換後的異常都是未選中的,這爲您提供了捕獲異常的選項,您可以從中恢復,同時讓其他異常傳播到調用者。使用SQLExceptionTranslator看到。
3.3 使用JDBC核心類來控制基本的JDBC處理和錯誤處理
本節介紹如何使用JDBC核心類來控制基本的JDBC處理,包括錯誤處理。它包括下列主題:
3.3.1 使用JdbcTemplate
JdbcTemplate是JDBC核心包中的中心類。它處理資源的創建和釋放,這有助於避免常見的錯誤,比如忘記關閉連接。它執行核心JDBC工作流的基本任務(如語句創建和執行),留下應用程序代碼來提供SQL和提取結果。JdbcTemplate類:
- 運行SQL查詢
- 更新語句和存儲過程調用
- 對ResultSet實例執行迭代並提取返回的參數值。
- 捕獲JDBC異常,並將它們轉換爲org.springframework中定義的通用的、信息更豐富的異常層次結構。dao包。(參見一致的異常層次結構。)
當您爲代碼使用JdbcTemplate時,您只需要實現回調接口,給它們一個明確定義的契約。對於由JdbcTemplate類提供的連接,PreparedStatementCreator回調接口創建一個準備好的語句,提供SQL和任何必要的參數。對於創建可調用語句的CallableStatementCreator接口也是如此。RowCallbackHandler接口從ResultSet的每一行中提取值。
您可以通過直接實例化數據源引用在DAO實現中使用JdbcTemplate,也可以在Spring IoC容器中配置它,並將其作爲bean引用提供給DAOs。
注意:數據源應該始終在Spring IoC容器中配置爲bean。在第一種情況下,bean直接提供給服務;在第二種情況下,它被提供給準備好的模板。
這個類發出的所有SQL都記錄在調試級別,對應於模板實例的完全限定類名(通常是JdbcTemplate,但是如果使用JdbcTemplate類的自定義子類,情況可能會有所不同)。
以下部分提供了一些使用JdbcTemplate的示例。這些示例並不是JdbcTemplate公開的所有功能的詳盡列表。請參閱相關的javadoc。
Querying (SELECT
)
下面的查詢獲取關係中的行數:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
以下查詢使用綁定變量:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
下面的查詢查找一個字符串:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
new Object[]{1212L}, String.class);
以下查詢查找並填充單個域對象:
Actor actor = this.jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
new Object[]{1212L},
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
以下查詢查找並填充大量域對象:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
如果最後兩個片段代碼實際上存在於相同的應用程序,它將意義刪除重複出現在兩個RowMapper匿名內部類和他們提取到一個單獨的類(通常是一個靜態嵌套類),然後可以引用的DAO方法。例如,最好將前面的代碼片段寫成如下:
public List<Actor> findAllActors() {
return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}
private static final class ActorMapper implements RowMapper<Actor> {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
Updating (INSERT
, UPDATE
, and DELETE
) with JdbcTemplate
使用JdbcTemplate更新(插入、更新和刪除)
可以使用update(..)方法執行插入、更新和刪除操作。參數值通常作爲變量參數或對象數組提供。
下面的例子插入了一個新條目:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
下面的例子更新了一個現有的條目:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
下面的例子刪除了一個條目:
this.jdbcTemplate.update(
"delete from actor where id = ?",
Long.valueOf(actorId));
其他JdbcTemplate操作
您可以使用execute(..)方法來運行任意SQL。因此,該方法通常用於DDL語句。它被帶有回調接口、綁定變量數組等的變量重載。下面的例子創建了一個表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
下面的例子調用了一個存儲過程:
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
稍後將介紹更復雜的存儲過程支持。
JdbcTemplate最佳實踐
配置後,JdbcTemplate類的實例是線程安全的。這很重要,因爲這意味着您可以配置一個JdbcTemplate的單個實例,然後安全地將這個共享引用注入到多個DAOs(或存儲庫)中。JdbcTemplate是有狀態的,因爲它維護對數據源的引用,但是這種狀態不是會話狀態。
使用JdbcTemplate類(以及相關的NamedParameterJdbcTemplate類)時的一個常見實踐是在Spring配置文件中配置一個數據源,然後將該共享數據源bean依賴地注入到DAO類中。JdbcTemplate是在數據源的setter中創建的。這將導致類似於以下的dao:
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
下面的例子展示了相應的XML配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
顯式配置的另一種替代方法是使用組件掃描和註釋支持依賴項注入。在這種情況下,您可以使用@Repository來註釋類(這使它成爲組件掃描的候選對象),並使用@Autowired來註釋DataSource setter方法。下面的例子演示瞭如何做到這一點:
@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
- 用@Repository註釋類。
- 使用@Autowired註解數據源setter方法。
- 使用數據源創建一個新的JdbcTemplate。
下面的例子展示了相應的XML配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Scans within the base package of the application for @Component classes to configure as beans -->
<context:component-scan base-package="org.springframework.docs.test" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
如果您使用Spring的JdbcDaoSupport類,並且您的各種jdbc支持的DAO類都是從它擴展而來,那麼您的子類將繼承來自JdbcDaoSupport類的setDataSource(..)方法。您可以選擇是否從該類繼承。提供JdbcDaoSupport類只是爲了方便。
無論您選擇使用(或不使用)上述哪一種模板初始化樣式,在每次希望運行SQL時都很少需要創建JdbcTemplate類的新實例。配置之後,JdbcTemplate實例就是線程安全的。如果您的應用程序訪問多個數據庫,您可能需要多個JdbcTemplate實例,這需要多個數據源,然後需要多個不同配置的JdbcTemplate實例。
3.3.2 使用NamedParameterJdbcTemplate
NamedParameterJdbcTemplate類通過使用命名參數來增加對JDBC語句編程的支持,而不是僅使用傳統佔位符('?')參數來編寫JDBC語句。NamedParameterJdbcTemplate類包裝了一個JdbcTemplate並將其委託給包裝好的JdbcTemplate來完成其大部分工作。本節只描述NamedParameterJdbcTemplate類中與JdbcTemplate本身不同的部分—即通過使用命名參數來編寫JDBC語句。下面的例子展示瞭如何使用NamedParameterJdbcTemplate:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
注意,在分配給sql變量的值和插入到namedParameters變量(類型爲MapSqlParameterSource)的對應值中使用了命名參數表示法。
或者,可以使用基於映射的樣式將命名參數及其對應的值傳遞給NamedParameterJdbcTemplate實例。NamedParameterJdbcOperations公開並由NamedParameterJdbcTemplate類實現的其餘方法遵循類似的模式,本文不討論。
下面的例子展示瞭如何使用基於地圖的樣式:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
與NamedParameterJdbcTemplate(存在於同一個Java包中)相關的一個很好的特性是SqlParameterSource接口。您已經在前面的一個代碼片段(MapSqlParameterSource類)中看到了此接口的實現示例。SqlParameterSource是NamedParameterJdbcTemplate的命名參數值的源。MapSqlParameterSource類是一個簡單的實現,它是一個圍繞java.util的適配器。Map,其中鍵是參數名,值是參數值。
另一個SqlParameterSource實現是BeanPropertySqlParameterSource類。這個類包裝一個任意的JavaBean(即,一個遵循JavaBean約定的類的實例),並使用包裝的JavaBean的屬性作爲命名參數值的源。
下面的例子展示了一個典型的JavaBean:
public class Actor {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
// setters omitted...
}
下面的示例使用NamedParameterJdbcTemplate返回前一個示例中顯示的類成員的計數:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// notice how the named parameters match the properties of the above 'Actor' class
String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
記住,NamedParameterJdbcTemplate類包裝了一個經典的JdbcTemplate模板。如果需要訪問包裝好的JdbcTemplate實例來訪問僅在JdbcTemplate類中出現的功能,那麼可以使用getJdbcOperations()方法通過JdbcOperations接口訪問包裝好的JdbcTemplate。
有關在應用程序上下文中使用NamedParameterJdbcTemplate類的指南,請參閱JdbcTemplate最佳實踐。
3.3.3 使用SQLExceptionTranslator
SQLExceptionTranslator是一個由類實現的接口,可以在SQLExceptions和Spring自己的org.springframe .dao之間進行轉換。DataAccessException,它與數據訪問策略無關。實現可以是通用的(例如,爲JDBC使用SQLState代碼),也可以是專用的(例如,使用Oracle錯誤代碼),以獲得更高的精度。
SQLErrorCodeSQLExceptionTranslator是默認使用的SQLExceptionTranslator的實現。此實現使用特定的供應商代碼。它比SQLState實現更精確。錯誤代碼的轉換基於JavaBean類型類SQLErrorCodes中的代碼。這個類是由一個SQLErrorCodesFactory創建和填充的,它(顧名思義)是一個根據名爲sql-error-code .xml的配置文件內容創建SQLErrorCodes的工廠。此文件使用供應商代碼填充,並基於從DatabaseMetaData獲取的DatabaseProductName。使用您正在使用的實際數據庫的代碼。
SQLErrorCodeSQLExceptionTranslator按照以下順序應用匹配規則:
- 由子類實現的任何自定義翻譯。通常使用提供的具體SQLErrorCodeSQLExceptionTranslator,因此不適用此規則。它只適用於您實際提供了一個子類實現的情況。
- 作爲SQLErrorCodes類的customSqlExceptionTranslator屬性提供的SQLExceptionTranslator接口的任何自定義實現。
- 將搜索CustomSQLErrorCodesTranslation類(爲SQLErrorCodes類的customtranslate屬性提供)的實例列表以查找匹配項。
- 應用錯誤代碼匹配。
- 使用後備翻譯器。SQLExceptionSubclassTranslator是默認的回退轉換器。如果這個翻譯不可用,那麼下一個後備翻譯器是SQLStateSQLExceptionTranslator。
注意:默認情況下,SQLErrorCodesFactory用於定義錯誤代碼和自定義異常轉換。它們在類路徑中名爲sql-error-codes.xml的文件中查找,匹配的SQLErrorCodes實例根據使用的數據庫元數據中的數據庫名稱定位。
您可以擴展SQLErrorCodeSQLExceptionTranslator,如下面的示例所示:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
if (sqlEx.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlEx);
}
return null;
}
}
在前面的示例中,翻譯特定的錯誤代碼(-12345),而其他錯誤則由默認的轉換器實現進行翻譯。要使用這個自定義轉換器,必須通過setExceptionTranslator方法將其傳遞給JdbcTemplate,並且必須在需要這個轉換器的所有數據訪問處理中使用這個JdbcTemplate。下面的例子展示瞭如何使用這個自定義翻譯:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
爲了在sql-error-code .xml中查找錯誤代碼,向自定義轉換器傳遞一個數據源。
3.3.4 運行報表
運行SQL語句只需要很少的代碼。您需要一個數據源和一個JdbcTemplate,包括JdbcTemplate提供的便利方法。下面的例子展示了創建新表的最小但功能完整的類需要包含什麼:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAStatement {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void doExecute() {
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
}
}
3.3.5 運行查詢
一些查詢方法返回單個值。要從一行中檢索一個計數或特定值,可以使用queryForObject(..)。後者將返回的JDBC類型轉換爲作爲參數傳入的Java類。如果類型轉換無效,則拋出InvalidDataAccessApiUsageException。下面的示例包含兩個查詢方法,一個用於int,另一個用於查詢字符串:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class RunAQuery {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}
public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}
除了單個結果查詢方法之外,還有幾個方法爲查詢返回的每一行返回一個帶有條目的列表。最通用的方法是queryForList(..),它返回一個列表,其中每個元素是一個映射,每個列包含一個條目,使用列名作爲鍵。如果在前面的示例中添加一個方法來檢索所有行的列表,它可能如下所示:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getList() {
return this.jdbcTemplate.queryForList("select * from mytable");
}
返回的列表如下:
[{name=Bob, id=1}, {name=Mary, id=2}]
3.3.6 更新數據庫
下面的示例更新某個主鍵的列:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAnUpdate {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void setName(int id, String name) {
this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
}
}
在前面的示例中,SQL語句的行參數有佔位符。您可以將參數值作爲變量傳遞,或者作爲對象數組傳遞。因此,應該在原語包裝器類中顯式地包裝原語,或者應該使用自動裝箱。
3.3.7 獲取自動生成的鍵
update()便利方法支持檢索數據庫生成的主鍵。這種支持是JDBC 3.0標準的一部分。詳見本規範第13.6章。該方法的第一個參數是PreparedStatementCreator,這是指定所需insert語句的方式。另一個參數是KeyHolder,它包含更新成功返回時生成的密鑰。沒有標準的單一方法來創建適當的PreparedStatement(這解釋了爲什麼方法簽名是這樣的)。下面的例子適用於Oracle,但可能不適用於其他平臺:
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
ps.setString(1, name);
return ps;
}
},
keyHolder);
// keyHolder.getKey() now contains the generated key
3.4 控制數據庫連接
本節將介紹:
3.4.1. Using DataSource
Spring通過數據源獲得到數據庫的連接。數據源是JDBC規範的一部分,是一個通用的連接工廠。它允許容器或框架從應用程序代碼中隱藏連接池和事務管理問題。作爲開發人員,您不需要知道如何連接到數據庫的詳細信息。這是設置數據源的管理員的責任。您很可能在開發和測試代碼時同時擔任這兩個角色,但是您不必瞭解如何配置生產數據源。
當您使用Spring的JDBC層時,您可以從JNDI獲得數據源,或者您可以使用第三方提供的連接池實現來配置您自己的數據源。流行的實現是Apache Jakarta Commons DBCP和C3P0。Spring發行版中的實現僅用於測試目的,不提供池。
本節使用Spring的DriverManagerDataSource實現,後面將介紹幾個其他實現。
注意:您應該只將DriverManagerDataSource類用於測試目的,因爲它不提供池,並且在發出一個連接的多個請求時性能很差。
配置一個DriverManagerDataSource:
- 獲得與DriverManagerDataSource的連接,就像通常獲得JDBC連接一樣。
- 指定JDBC驅動程序的完全限定類名,以便驅動程序管理器可以加載驅動程序類。
- 提供一個在JDBC驅動程序之間變化的URL。(有關正確的值,請參閱驅動程序的文檔。)
- 提供連接到數據庫的用戶名和密碼。
下面的例子展示瞭如何在Java中配置一個DriverManagerDataSource:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
下面的例子展示了相應的XML配置:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
接下來的兩個示例展示了DBCP和C3P0的基本連接和配置。要了解更多有助於控制池功能的選項,請參閱相應的連接池實現的產品文檔。
下面的例子顯示了DBCP配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
下面的例子顯示了C3P0配置:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
3.4.2 使用DataSourceUtils
DataSourceUtils類是一個方便而強大的助手類,它提供了從JNDI獲取連接的靜態方法,並在必要時關閉連接。它支持與DataSourceTransactionManager等線程綁定的連接。
3.4.3 實現SmartDataSource
SmartDataSource接口應該由能夠提供到關係數據庫連接的類來實現。它擴展了DataSource接口,讓使用它的類可以查詢給定操作之後是否應該關閉連接。當您知道需要重用一個連接時,這種用法是有效的。
3.4.4 延長AbstractDataSource
AbstractDataSource是Spring數據源實現的抽象基類。它實現對所有數據源實現都通用的代碼。如果您編寫自己的數據源實現,則應該擴展AbstractDataSource類。
3.4.5 使用SingleConnectionDataSource
SingleConnectionDataSource類是SmartDataSource接口的一個實現,它封裝了一個在每次使用後都沒有關閉的連接。這不是多線程的能力。
如果任何客戶機代碼調用都是基於池連接的假設(如使用持久性工具時),那麼應該將suppressClose屬性設置爲true。此設置返回一個封裝物理連接的關閉抑制代理。注意,您不能再將此轉換爲本機Oracle連接或類似對象。
SingleConnectionDataSource主要是一個測試類。例如,它支持與簡單的JNDI環境一起在應用服務器外部輕鬆地測試代碼。與DriverManagerDataSource不同,它始終重用相同的連接,避免過多地創建物理連接。
3.4.6 使用DriverManagerDataSource
DriverManagerDataSource類是標準DataSource接口的實現,它通過bean屬性配置普通JDBC驅動程序,每次都返回一個新連接。
此實現對於Java EE容器外部的測試和獨立環境非常有用,可以作爲Spring IoC容器中的數據源bean,也可以與簡單的JNDI環境結合使用。close()調用close連接,因此任何數據源感知的持久性代碼都可以工作。然而,使用javabean風格的連接池(例如commons-dbcp)非常簡單,甚至在測試環境中也是如此,因此在DriverManagerDataSource上使用這樣的連接池幾乎總是可取的。
3.4.7 使用TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy是目標數據源的代理。代理包裝了目標數據源,以增加對spring管理的事務的感知。在這方面,它類似於Java EE服務器提供的事務性JNDI數據源。
注意:很少需要使用這個類,除非已經存在的代碼必須被調用並傳遞一個標準的JDBC數據源接口實現。在這種情況下,您仍然可以讓這些代碼可用,同時讓這些代碼參與Spring管理的事務。通過使用更高級別的資源管理抽象(如JdbcTemplate或DataSourceUtils)來編寫自己的新代碼通常是更好的選擇。
有關詳細信息,請參閱TransactionAwareDataSourceProxy
javadoc。
3.4.8。使用DataSourceTransactionManager
DataSourceTransactionManager類是單個JDBC數據源的平臺transactionmanager實現。它將指定數據源的JDBC連接綁定到當前執行的線程,這可能允許每個數據源有一個線程連接。
要通過datasourceutil . getconnection (DataSource)而不是Java EE的標準DataSource. getconnection檢索JDBC連接,需要應用程序代碼。它拋出未檢查的org.springframework。dao異常,而不是已檢查的SQLExceptions。所有框架類(如JdbcTemplate)都隱式地使用此策略。如果沒有與此事務管理器一起使用,則查找策略的行爲與普通策略完全相同。因此,它可以在任何情況下使用。
DataSourceTransactionManager類支持作爲適當的JDBC語句查詢超時應用的自定義隔離級別和超時。要支持後一種方法,應用程序代碼必須使用JdbcTemplate或爲每個創建的語句調用datasourceutil . applytransactiontimeout(..)方法。
在單資源的情況下,您可以使用這個實現代替JtaTransactionManager,因爲它不需要容器來支持JTA。如果您堅持使用所需的連接查找模式,則在兩者之間進行切換隻是配置問題。JTA不支持自定義隔離級別。
3.5 JDBC批處理操作
如果您批量處理對同一預備語句的多個調用,大多數JDBC驅動程序都可以提供更好的性能。通過將更新分組爲批,可以限制到數據庫的往返次數。
3.5.1 使用JdbcTemplate進行基本的批處理操作
通過實現一個特殊接口的兩個方法BatchPreparedStatementSetter來完成JdbcTemplate批處理,並將該實現作爲batchUpdate方法調用中的第二個參數傳遞。您可以使用getBatchSize方法來提供當前批處理的大小。可以使用setValues方法設置準備好的語句的參數值。調用此方法的次數是在getBatchSize調用中指定的次數。下面的示例根據列表中的條目更新actor表,整個列表用作批處理:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
如果您處理更新流或從文件讀取數據,您可能有一個首選的批大小,但最後一批可能沒有那麼多的條目。在這種情況下,您可以使用InterruptibleBatchPreparedStatementSetter接口,它允許您在輸入源耗盡後中斷批處理。isbatch方法允許您發出批處理結束的信號。
3.5.2 使用對象列表進行批處理操作
JdbcTemplate和NamedParameterJdbcTemplate都提供了提供批量更新的替代方法。不需要實現特殊的批處理接口,而是將調用中的所有參數值作爲列表提供。框架循環遍歷這些值並使用內部準備好的語句setter。根據是否使用命名參數,API會有所不同。對於已命名的參數,您提供一個SqlParameterSource數組,其中每個成員有一個條目。您可以使用SqlParameterSourceUtils。createBatch方便的方法來創建這個數組,傳入一個bean樣式的對象數組(帶有與參數相對應的getter方法),字符串鍵控的映射實例(包含作爲值的相應參數),或者兩者混合。
下面的例子顯示了一個使用命名參數的批量更新:
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
對於使用classic的SQL語句?佔位符,傳遞一個包含對象數組和更新值的列表。對於SQL語句中的每個佔位符,這個對象數組必須有一個條目,並且它們必須與SQL語句中定義的順序相同。
下面的示例與前面的示例相同,不同之處是它使用了經典JDBC ?佔位符:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<Object[]>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... additional methods
}
我們前面描述的所有批處理更新方法都返回一個int數組,其中包含每個批處理條目的受影響行數。這個計數由JDBC驅動程序報告。如果計數不可用,JDBC驅動程序將返回一個-2值。
注意:在這種情況下,通過在底層PreparedStatement上自動設置值,需要從給定的Java類型派生出每個值的對應JDBC類型。雖然這通常工作得很好,但也存在潛在的問題(例如,使用包含映射的空值)。默認情況下,Spring調用參數元數據。在這種情況下,getParameterType對於JDBC驅動程序來說非常昂貴。您應該使用最新的驅動程序版本,並考慮設置spring.jdbc.getParameterType。忽略屬性爲true(作爲JVM系統屬性或在spring中)。如果您遇到性能問題—例如,如Oracle 12c (sprl -16139)報告的那樣。
或者,您可能會考慮顯式地指定相應的JDBC類型,通過“BatchPreparedStatementSetter”(如圖所示),通過顯式類型數組給基於“列表< Object[] >”,通過“registerSqlType”自定義“MapSqlParameterSource”實例上調用,或通過“BeanPropertySqlParameterSource”SQL類型來自Java-declared屬性類型即使對於一個null值。
3.5.3 具有多個批次的批處理操作
前面的批處理更新示例處理的批非常大,您希望將它們分成幾個較小的批。您可以通過多次調用batchUpdate方法來使用前面提到的方法,但是現在有一個更方便的方法。除了SQL語句外,此方法還使用包含參數的對象集合、每個批處理的更新次數和ParameterizedPreparedStatementSetter來設置準備好的語句的參數值。框架循環遍歷提供的值,並將更新調用分成指定大小的批。
下面的例子展示了一個批量更新,它使用的批量大小爲100:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection<Actor> actors) {
int[][] updateCounts = jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors,
100,
new ParameterizedPreparedStatementSetter<Actor>() {
public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
ps.setString(1, argument.getFirstName());
ps.setString(2, argument.getLastName());
ps.setLong(3, argument.getId().longValue());
}
});
return updateCounts;
}
// ... additional methods
}
此調用的批處理更新方法返回一個int數組,其中包含每個批處理的一個數組條目,以及每個更新的受影響行數的數組。第一級數組的長度表示執行的批數,第二級數組的長度表示該批中的更新數。每個批中的更新數量應該是爲所有批提供的批大小(最後一個批大小可能更小),這取決於所提供的更新對象的總數。每個更新語句的更新計數是JDBC驅動程序報告的更新計數。如果計數不可用,JDBC驅動程序將返回一個-2值。
3.6 使用SimpleJdbc類簡化JDBC操作
SimpleJdbcInsert和SimpleJdbcCall類通過利用可以通過JDBC驅動程序檢索的數據庫元數據來提供簡化的配置。這意味着您需要預先配置的內容更少,儘管如果希望在代碼中提供所有細節,您可以覆蓋或關閉元數據處理。
3.6.1。使用SimpleJdbcInsert插入數據
我們首先查看帶有最少配置選項的SimpleJdbcInsert類。您應該在數據訪問層的初始化方法中實例化SimpleJdbcInsert。對於本例,初始化方法是setDataSource方法。您不需要子類化SimpleJdbcInsert類。相反,您可以創建一個新實例,並使用withTableName方法設置表名。該類的配置方法遵循返回SimpleJdbcInsert實例的流體樣式,該實例允許您鏈接所有配置方法。下面的示例只使用了一種配置方法(稍後我們將展示多種方法的示例):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<String, Object>(3);
parameters.put("id", actor.getId());
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
insertActor.execute(parameters);
}
// ... additional methods
}
這裏使用的execute方法採用普通java.util。Map是它唯一的參數。這裏需要注意的重要一點是,用於映射的鍵必須與數據庫中定義的表的列名匹配。這是因爲我們讀取元數據來構造實際的insert語句。
3.6.2 使用SimpleJdbcInsert檢索自動生成的密鑰
下一個示例使用與前一個示例相同的insert,但是它檢索自動生成的密鑰並將其設置在新的Actor對象上,而不是傳遞id。當它創建SimpleJdbcInsert時,除了指定表名之外,它還使用usingGeneratedKeyColumns方法指定生成的鍵列的名稱。下面的清單展示了它是如何工作的:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<String, Object>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
使用第二種方法運行插入時的主要區別是,不向映射添加id,而是調用executeAndReturnKey方法。這將返回java.lang。對象,您可以使用該對象創建在域類中使用的數值類型的實例。在這裏,您不能依賴所有數據庫來返回特定的Java類。. lang。Number是您可以依賴的基類。如果您有多個自動生成的列,或者生成的值是非數值的,那麼您可以使用從executeAndReturnKeyHolder方法返回的KeyHolder。
3.6.3。爲SimpleJdbcInsert指定列
您可以通過使用usingColumns方法指定列名列表來限制插入的列,如下面的示例所示:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<String, Object>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
插入的執行與依賴元數據來決定使用哪些列是一樣的。
3.6.4。使用SqlParameterSource提供參數值
使用映射來提供參數值工作得很好,但是它不是最方便使用的類。Spring提供了SqlParameterSource接口的兩個實現,您可以使用它們。第一個是BeanPropertySqlParameterSource,如果您有一個兼容javabean的類,其中包含您的值,那麼它是一個非常方便的類。它使用相應的getter方法來提取參數值。下面的例子展示瞭如何使用BeanPropertySqlParameterSource:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
另一個選項是MapSqlParameterSource,它類似於映射,但提供了更方便的addValue方法,可以將其鏈接起來。下面的例子展示瞭如何使用它:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
正如您所看到的,配置是相同的。只有執行中的代碼需要更改才能使用這些替代輸入類。
3.6.5 使用SimpleJdbcCall調用存儲過程
SimpleJdbcCall類使用數據庫中的元數據來查找in和out參數的名稱,這樣您就不必顯式地聲明它們。您可以聲明參數,如果您願意這樣做,或者如果您有參數(例如數組或結構)沒有自動映射到Java類的話。第一個示例展示了一個簡單的過程,該過程僅從MySQL數據庫返回VARCHAR和日期格式的標量值。示例過程讀取指定的actor條目,並以out參數的形式返回first_name、last_name和birth_date列。下面的清單顯示了第一個例子:
CREATE PROCEDURE read_actor (
IN in_id INTEGER,
OUT out_first_name VARCHAR(100),
OUT out_last_name VARCHAR(100),
OUT out_birth_date DATE)
BEGIN
SELECT first_name, last_name, birth_date
INTO out_first_name, out_last_name, out_birth_date
FROM t_actor where id = in_id;
END;
in_id參數包含您正在查找的參與者的id。out參數返回從表中讀取的數據。
您可以以類似於聲明SimpleJdbcInsert的方式聲明SimpleJdbcCall。您應該在數據訪問層的初始化方法中實例化和配置該類。與StoredProcedure類相比,您不需要創建子類,也不需要聲明可以在數據庫元數據中查找的參數。下面的SimpleJdbcCall配置示例使用前面的存儲過程(除了數據源之外,惟一的配置選項是存儲過程的名稱):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
this.procReadActor = new SimpleJdbcCall(dataSource)
.withProcedureName("read_actor");
}
public Actor readActor(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
Map out = procReadActor.execute(in);
Actor actor = new Actor();
actor.setId(id);
actor.setFirstName((String) out.get("out_first_name"));
actor.setLastName((String) out.get("out_last_name"));
actor.setBirthDate((Date) out.get("out_birth_date"));
return actor;
}
// ... additional methods
}
爲執行調用而編寫的代碼涉及創建一個包含IN參數的SqlParameterSource。必須將爲輸入值提供的名稱與存儲過程中聲明的參數名稱相匹配。這種情況不需要匹配,因爲您使用元數據來確定如何在存儲過程中引用數據庫對象。在源中爲存儲過程指定的不一定是存儲在數據庫中的方式。一些數據庫將名稱轉換爲全大寫,而其他數據庫使用小寫或指定的大小寫。
execute方法接受IN參數並返回一個映射,其中包含按名稱鍵控的所有out參數,如存儲過程中指定的那樣。在本例中,它們是out_first_name、out_last_name和out_birth_date。
execute方法的最後一部分創建一個Actor實例,用於返回檢索到的數據。同樣,在存儲過程中聲明out參數時使用它們的名稱也很重要。此外,存儲在結果映射中的out參數名稱的大小寫與數據庫中的out參數名稱的大小寫相匹配,而數據庫之間的out參數名稱可能有所不同。爲了使您的代碼更具可移植性,您應該執行不區分大小寫的查找,或者指示Spring使用LinkedCaseInsensitiveMap。要實現後者,您可以創建自己的JdbcTemplate並將setResultsMapCaseInsensitive屬性設置爲true。然後可以將這個定製的JdbcTemplate實例傳遞到SimpleJdbcCall的構造函數中。下面的例子顯示了這種配置:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor");
}
// ... additional methods
}
通過執行此操作,可以避免在使用返回的out參數的名稱時出現衝突。
3.6.6 顯式聲明SimpleJdbcCall使用的參數
在本章的前面,我們描述瞭如何從元數據推導參數,但是如果願意,您可以顯式地聲明它們。您可以通過使用declareParameters方法創建和配置SimpleJdbcCall來實現這一點,該方法將可變數量的SqlParameter對象作爲輸入。有關如何定義SqlParameter的詳細信息,請參閱下一節。
注意:如果使用的數據庫不是spring支持的數據庫,則需要顯式聲明。目前,Spring支持以下數據庫的存儲過程調用的元數據查詢:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle和Sybase。我們還支持MySQL、Microsoft SQL Server和Oracle存儲函數的元數據查詢。
您可以選擇顯式地聲明一個、一些或所有參數。在沒有顯式聲明參數的情況下,仍然使用參數元數據。要繞過對潛在參數的所有元數據查找處理,只使用聲明的參數,可以調用方法withoutprocedure recolumnmetadataaccess作爲聲明的一部分。假設您爲一個數據庫函數聲明瞭兩個或多個不同的調用簽名。在這種情況下,您可以調用useInParameterNames來指定要包含給定簽名的參數名稱列表。
下面的例子展示了一個完整聲明的過程調用,並使用了來自前一個例子的信息:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
new SqlOutParameter("out_last_name", Types.VARCHAR),
new SqlOutParameter("out_birth_date", Types.DATE)
);
}
// ... additional methods
}
這兩個示例的執行和最終結果是相同的。第二個示例顯式地指定所有細節,而不是依賴於元數據。
3.6.7 如何定義SqlParameters
要爲SimpleJdbc類和RDBMS操作類(在將JDBC操作建模爲Java對象中涉及到)定義一個參數,可以使用SqlParameter或它的一個子類。爲此,通常要在構造函數中指定參數名稱和SQL類型。SQL類型是通過使用java.sql指定的。類型的常量。在本章的前面,我們看到了類似以下的聲明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
帶有SqlParameter的第一行聲明瞭一個IN參數。通過使用SqlQuery及其子類(在理解SqlQuery中有所涉及),可以在存儲過程調用和查詢中使用參數。
第二行(帶有SqlOutParameter)聲明一個out參數,用於存儲過程調用。InOut參數還有一個SqlInOutParameter(爲過程提供IN值並返回值的參數)。
注意:只有聲明爲SqlParameter和SqlInOutParameter的參數才用於提供輸入值。這與StoredProcedure類不同,後者(出於向後兼容的原因)允許爲聲明爲SqlOutParameter的參數提供輸入值。
對於IN參數,除了名稱和SQL類型外,還可以爲數字數據指定比例,或爲自定義數據庫類型指定類型名稱。對於out參數,您可以提供一個行映射器來處理從REF遊標返回的行映射。另一個選項是指定SqlReturnType,它提供了一個機會來定義對返回值的自定義處理。
3.6.8 使用SimpleJdbcCall調用存儲函數
您可以以幾乎與調用存儲過程相同的方式調用存儲函數,只不過提供的是函數名而不是過程名。您可以使用withFunctionName方法作爲配置的一部分,以指示您想要對一個函數進行調用,並生成相應的函數調用字符串。專門的執行調用(executeFunction)用於執行函數,它將函數返回值作爲指定類型的對象返回,這意味着不必從結果映射中檢索返回值。對於只有一個out參數的存儲過程,也可以使用類似的便利方法(名爲executeObject)。下面的示例(對於MySQL)基於一個名爲get_actor_name的存儲函數,該函數返回一個參與者的全名:
CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
DECLARE out_name VARCHAR(200);
SELECT concat(first_name, ' ', last_name)
INTO out_name
FROM t_actor where id = in_id;
RETURN out_name;
END;
爲了調用這個函數,我們再次在初始化方法中創建一個SimpleJdbcCall,如下面的示例所示:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name");
}
public String getActorName(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
String name = funcGetActorName.executeFunction(String.class, in);
return name;
}
// ... additional methods
}
使用的executeFunction方法返回一個字符串,該字符串包含函數調用的返回值。
3.6.9。從SimpleJdbcCall返回ResultSet或REF遊標
調用返回結果集的存儲過程或函數有點棘手。一些數據庫在JDBC結果處理期間返回結果集,而另一些則需要顯式註冊特定類型的out參數。這兩種方法都需要額外的處理來遍歷結果集並處理返回的行。使用SimpleJdbcCall,您可以使用returningResultSet方法,並聲明一個用於特定參數的行映射器實現。如果在結果處理期間返回結果集,則沒有定義名稱,因此返回的結果必須與聲明RowMapper實現的順序匹配。指定的名稱仍然用於將處理過的結果列表存儲在從execute語句返回的結果映射中。
下一個例子(對於MySQL)使用了一個存儲過程,它不接受參數,並返回t_actor表中的所有行:
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
要調用此過程,可以聲明行映射器。因爲要映射到的類遵循JavaBean規則,所以可以使用BeanPropertyRowMapper,它是通過在newInstance方法中傳遞需要映射到的類而創建的。下面的例子演示瞭如何做到這一點:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadAllActors;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor.class));
}
public List getActorsList() {
Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
return (List) m.get("actors");
}
// ... additional methods
}
execute調用傳遞一個空映射,因爲這個調用不接受任何參數。然後從結果映射檢索參與者列表並返回給調用者。
3.7 將JDBC操作建模爲Java對象
org.springframework.jdbc。object package包含一些類,這些類允許您以一種更面向對象的方式訪問數據庫。例如,您可以執行查詢並將結果作爲包含業務對象的列表返回,其中關係列數據映射到業務對象的屬性。您還可以運行存儲過程和運行更新、刪除和插入語句。
注意:許多Spring開發人員認爲,下面描述的各種RDBMS操作類(StoredProcedure類除外)通常可以用直接的JdbcTemplate調用替換。通常,直接調用JdbcTemplate上的方法的DAO方法更簡單(與將查詢封裝爲完整的類相反)。
但是,如果您從使用RDBMS操作類中獲得可度量的價值,那麼您應該繼續使用這些類。
3.7.1。理解SqlQuery
SqlQuery是一個可重用的、線程安全的類,它封裝了一個SQL查詢。子類必須實現newRowMapper(..)方法,以提供一個RowMapper實例,該實例可以在查詢執行期間創建的ResultSet上迭代獲得的每一行中創建一個對象。SqlQuery類很少直接使用,因爲MappingSqlQuery子類爲將行映射到Java類提供了更方便的實現。擴展SqlQuery的其他實現有MappingSqlQueryWithParameters和UpdatableSqlQuery。
3.7.2章。使用MappingSqlQuery
MappingSqlQuery是一個可重用的查詢,具體的子類必須實現抽象的mapRow(..)方法來將提供的ResultSet的每一行轉換成指定類型的對象。下面的示例顯示了一個自定義查詢,該查詢將來自t_actor關係的數據映射到Actor類的一個實例:
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
super(ds, "select id, first_name, last_name from t_actor where id = ?");
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
@Override
protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
Actor actor = new Actor();
actor.setId(rs.getLong("id"));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
該類使用Actor類型擴展了參數化的MappingSqlQuery。此客戶查詢的構造函數將數據源作爲惟一的參數。在這個構造函數中,可以使用DataSource調用超類的構造函數,並調用應該執行的SQL來檢索此查詢的行。此SQL用於創建PreparedStatement,因此它可能包含佔位符,用於在執行期間傳入的任何參數。您必須使用傳遞SqlParameter的declareParameter方法來聲明每個參數。SqlParameter接受名稱和java.sql.Types中定義的JDBC類型。在定義所有參數之後,可以調用compile()方法,以便準備語句並稍後運行。該類在編譯後是線程安全的,因此,只要在初始化DAO時創建了這些實例,就可以將它們作爲實例變量保存並重用。下面的例子展示瞭如何定義這樣一個類:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}
前面示例中的方法檢索具有作爲惟一參數傳入的id的客戶。因爲我們只想返回一個對象,所以我們調用了id作爲參數的findObject便利方法。如果我們有一個返回對象列表並獲取額外參數的查詢,那麼我們將使用其中一個execute方法,該方法獲取作爲varargs傳遞的參數值數組。下面的例子展示了這樣一個方法:
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
3.7.3。使用SqlUpdate
SqlUpdate類封裝了一個SQL更新。與查詢一樣,更新對象是可重用的,並且與所有RdbmsOperation類一樣,更新可以有參數,並且是在SQL中定義的。這個類提供了許多類似於查詢對象的execute(..)方法的update(..)方法。SQLUpdate類是具體的。它可以被子類化——例如,添加一個自定義更新方法。但是,您不必子類化SqlUpdate類,因爲可以通過設置SQL和聲明參數輕鬆地對它進行參數化。下面的示例創建了一個名爲execute的自定義更新方法:
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateCreditRating extends SqlUpdate {
public UpdateCreditRating(DataSource ds) {
setDataSource(ds);
setSql("update customer set credit_rating = ? where id = ?");
declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
declareParameter(new SqlParameter("id", Types.NUMERIC));
compile();
}
/**
* @param id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
public int execute(int id, int rating) {
return update(rating, id);
}
}
3.7.4。使用StoredProcedure
StoredProcedure類是用於RDBMS存儲過程的對象抽象的超類。
該類是抽象的,它的各種execute(..)方法具有保護訪問權限,除了通過提供更嚴格的類型的子類之外,還可以防止使用其他方法。
繼承的sql屬性是RDBMS中存儲過程的名稱。
要爲StoredProcedure類定義參數,可以使用SqlParameter或它的一個子類。您必須在構造函數中指定參數名和SQL類型,如下面的代碼片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SQL類型是使用java.sql指定的。類型的常量。
第一行(帶有SqlParameter)聲明瞭一個IN參數。可以在參數中使用存儲過程調用和使用SqlQuery及其子類(在理解SqlQuery中涉及)的查詢。
第二行(帶有SqlOutParameter)聲明一個out參數,用於存儲過程調用。InOut參數還有一個SqlInOutParameter(爲過程提供in值並返回值的參數)。
對於in參數,除了名稱和SQL類型外,還可以爲數字數據指定比例,或爲自定義數據庫類型指定類型名稱。對於out參數,您可以提供一個行映射器來處理從REF遊標返回的行映射。另一個選項是指定SqlReturnType,它允許您定義對返回值的自定義處理。
下一個簡單DAO示例使用StoredProcedure調用函數(sysdate()),該函數隨任何Oracle數據庫一起提供。要使用存儲過程功能,您必須創建一個擴展StoredProcedure的類。在本例中,StoredProcedure類是一個內部類。但是,如果需要重用StoredProcedure,可以將其聲明爲頂級類。這個示例沒有輸入參數,但是通過使用SqlOutParameter類將輸出參數聲明爲日期類型。execute()方法運行這個過程,並從結果映射中提取返回的日期。通過使用參數名作爲鍵,結果映射的每個聲明的輸出參數都有一個條目(在本例中只有一個)。下面的清單顯示了我們自定義的StoredProcedure類:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class StoredProcedureDao {
private GetSysdateProcedure getSysdate;
@Autowired
public void init(DataSource dataSource) {
this.getSysdate = new GetSysdateProcedure(dataSource);
}
public Date getSysdate() {
return getSysdate.execute();
}
private class GetSysdateProcedure extends StoredProcedure {
private static final String SQL = "sysdate";
public GetSysdateProcedure(DataSource dataSource) {
setDataSource(dataSource);
setFunction(true);
setSql(SQL);
declareParameter(new SqlOutParameter("date", Types.DATE));
compile();
}
public Date execute() {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
下面的StoredProcedure示例有兩個輸出參數(在本例中是Oracle REF遊標):
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAndGenresStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "AllTitlesAndGenres";
public TitlesAndGenresStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
compile();
}
public Map<String, Object> execute() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
注意,在TitlesAndGenresStoredProcedure構造函數中使用的declareParameter(..)方法的重載變體是如何被傳遞給RowMapper實現實例的。這是一種非常方便和強大的重用現有功能的方法。下面的兩個示例提供了兩種RowMapper實現的代碼。
TitleMapper類爲提供的ResultSet中的每一行將一個ResultSet映射到一個Title域對象,如下所示:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;
public final class TitleMapper implements RowMapper<Title> {
public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
Title title = new Title();
title.setId(rs.getLong("id"));
title.setName(rs.getString("name"));
return title;
}
}
GenreMapper類爲提供的ResultSet中的每一行將一個ResultSet映射到一個類型域對象,如下所示:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;
public final class GenreMapper implements RowMapper<Genre> {
public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Genre(rs.getString("name"));
}
}
要將參數傳遞給在RDBMS中定義有一個或多個輸入參數的存儲過程,您可以編寫一個強類型的execute(..)方法,該方法將委託給超類中的非類型化的execute(Map)方法,如下面的示例所示:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAfterDateStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "TitlesAfterDate";
private static final String CUTOFF_DATE_PARAM = "cutoffDate";
public TitlesAfterDateStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
compile();
}
public Map<String, Object> execute(Date cutoffDate) {
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
return super.execute(inputs);
}
}
3.8。參數和數據值處理的常見問題
參數和數據值的常見問題存在於Spring Framework的JDBC支持提供的不同方法中。本節將介紹如何解決這些問題。
3.8.1。爲參數提供SQL類型信息
通常,Spring根據傳入的參數類型確定參數的SQL類型。可以顯式地提供設置參數值時使用的SQL類型。這有時對於正確設置空值是必要的。
您可以通過以下幾種方式提供SQL類型信息:
- JdbcTemplate的許多更新和查詢方法都採用int數組形式的附加參數。該數組用於通過使用java.sql中的常量值來指示相應參數的SQL類型。類型的類。爲每個參數提供一個條目。
- 您可以使用SqlParameterValue類來包裝需要此附加信息的參數值。爲此,爲每個值創建一個新實例,並在構造函數中傳遞SQL類型和參數值。還可以爲數值提供可選的縮放參數。
- 對於使用命名參數的方法,可以使用SqlParameterSource類、BeanPropertySqlParameterSource或MapSqlParameterSource。它們都有用於註冊任何指定參數值的SQL類型的方法。
3.8.2。處理BLOB和CLOB對象
您可以在數據庫中存儲圖像、其他二進制數據和大塊文本。這些大對象對於二進制數據稱爲blob(二進制大對象),對於字符數據稱爲clob(字符大對象)。在Spring中,您可以直接使用JdbcTemplate來處理這些大型對象,也可以使用RDBMS對象和SimpleJdbc類提供的高級抽象。所有這些方法都使用LobHandler接口的實現來實際管理LOB(大對象)數據。LobHandler通過getLobCreator方法提供對一個LobCreator類的訪問,該方法用於創建要插入的新LOB對象。
LobCreator和LobHandler爲LOB輸入和輸出提供以下支持:
BLOB
- byte[]: getBlobAsBytes和setBlobAsBytes
- InputStream: getBlobAsBinaryStream和setBlobAsBinaryStream
CLOB
- String:getClobAsString和setClobAsString
- InputStream: getClobAsAsciiStream和setclobasasciream
- Reader:getClobAsCharacterStream和setClobAsCharacterStream
下一個示例展示如何創建和插入一個BLOB。稍後,我們將展示如何從數據庫中讀取它。
本例使用了JdbcTemplate和abstractcreatingpreparedstatementcallback的實現。它實現了一個方法setValues。這個方法提供了一個LobCreator,我們使用它來設置SQL insert語句中的LOB列的值。
對於本例,我們假設有一個變量lobHandler,它已經被設置爲DefaultLobHandler的一個實例。通常通過依賴項注入設置此值。
下面的示例演示如何創建和插入一個BLOB:
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
new AbstractLobCreatingPreparedStatementCallback(lobHandler) { //1
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); //2
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); //3
}
}
);
blobIs.close();
clobReader.close();
- 傳入一個簡單的DefaultLobHandler(在本例中)。
- 使用setClobAsCharacterStream方法傳遞CLOB的內容。
- 使用setBlobAsBinaryStream方法傳入BLOB的內容。
注意:如果您調用了DefaultLobHandler.getLobCreator()返回的LobCreator上的setBlobAsBinaryStream、setClobAsAsciiStream或setClobAsCharacterStream方法,您可以爲contentLength參數指定一個負值。如果指定的內容長度爲負,則DefaultLobHandler使用集流方法的JDBC 4.0變體,但不使用長度參數。否則,它將指定的長度傳遞給驅動程序。
請參閱用於驗證它是否支持不提供內容長度的LOB流的JDBC驅動程序的文檔。
現在該從數據庫讀取LOB數據了。同樣,使用具有相同實例變量lobHandler和對DefaultLobHandler的引用的JdbcTemplate。下面的例子演示瞭如何做到這一點:
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
new RowMapper<Map<String, Object>>() {
public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
Map<String, Object> results = new HashMap<String, Object>();
String clobText = lobHandler.getClobAsString(rs, "a_clob");
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
results.put("BLOB", blobBytes);
return results;
}
});
使用getClobAsString方法檢索CLOB的內容。
使用getBlobAsBytes方法檢索BLOB的內容。
3.8.3。傳入in子句的值列表
SQL標準允許根據包含變量值列表的表達式選擇行。一個典型的例子是select * from T_ACTOR,其中id在(1,2,3)中。不能聲明數量不定的佔位符。您需要準備大量的佔位符,或者需要在知道需要多少佔位符之後動態地生成SQL字符串。NamedParameterJdbcTemplate中提供的命名參數支持採用後一種方法。可以將值作爲java.util.List傳入。此列表用於插入所需的佔位符並在語句執行期間傳遞值。
注意:在傳遞許多值時要小心。JDBC標準並不保證您可以爲一個in表達式列表使用超過100個值。各種數據庫都超過這個數字,但是它們通常對允許的值有一個硬限制。例如,Oracle的上限是1000。
除了值列表中的原語值之外,您還可以創建java.util。對象數組的列表。這個列表可以支持爲in子句定義的多個表達式,比如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'\))。當然,這需要數據庫支持這種語法。
3.8.4。處理存儲過程調用的複雜類型
調用存儲過程時,有時可以使用特定於數據庫的複雜類型。爲了適應這些類型,Spring提供了一個SqlReturnType,用於在從存儲過程調用返回這些類型時處理它們,並在將它們作爲參數傳遞給存儲過程時處理SqlTypeValue。
SqlReturnType接口有一個必須實現的方法(名爲getTypeValue)。此接口用作SqlOutParameter聲明的一部分。下面的示例顯示了返回用戶聲明類型ITEM_TYPE的Oracle STRUCT對象的值:
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
new SqlReturnType() {
public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String ) throws SQLException {
STRUCT struct = (STRUCT) cs.getObject(colIndx);
Object[] attr = struct.getAttributes();
TestItem item = new TestItem();
item.setId(((Number) attr[0]).longValue());
item.setDescription((String) attr[1]);
item.setExpirationDate((java.util.Date) attr[2]);
return item;
}
}));
// ...
}
可以使用SqlTypeValue將Java對象(如睾丸)的值傳遞給存儲過程。SqlTypeValue接口有一個必須實現的方法(名爲createTypeValue)。活動連接被傳遞進來,您可以使用它來創建特定於數據庫的對象,例如StructDescriptor實例或ArrayDescriptor實例。下面的例子創建了一個StructDescriptor實例:
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
Struct item = new STRUCT(itemDescriptor, conn,
new Object[] {
testItem.getId(),
testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime())
});
return item;
}
};
現在可以將這個SqlTypeValue添加到包含存儲過程執行調用的輸入參數的映射中。
SqlTypeValue的另一個用途是將值數組傳遞給Oracle存儲過程。Oracle有自己的內部數組類,在這種情況下必須使用它,您可以使用SqlTypeValue創建Oracle數組的實例,並用Java數組中的值填充它,如下面的例子所示:
final Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
return idArray;
}
};
3.9。嵌入式數據庫的支持
org.springframework.jdbc.datasource。嵌入式包提供對嵌入式Java數據庫引擎的支持。本地提供對HSQL、H2和Derby的支持。您還可以使用可擴展的API來插入新的嵌入式數據庫類型和數據源實現。
3.9.1。爲什麼使用嵌入式數據庫?
嵌入式數據庫在項目的開發階段非常有用,因爲它是輕量級的。它的優點包括配置簡單、啓動時間快、可測試性以及在開發過程中快速發展SQL的能力。
3.9.2。使用Spring XML創建嵌入式數據庫
如果您想在Spring ApplicationContext中將嵌入式數據庫實例作爲bean公開,您可以使用Spring -jdbc名稱空間中的嵌入式數據庫標記:
<jdbc:embedded-database id="dataSource" generate-name="true">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
前面的配置創建了一個嵌入式HSQL數據庫,該數據庫使用來自模式的SQL填充。sql和測試數據。類路徑根中的sql資源。此外,作爲一種最佳實踐,爲嵌入式數據庫分配一個惟一生成的名稱。Spring容器可以使用嵌入式數據庫作爲javax.sql類型的bean。然後可以根據需要將數據源注入到數據訪問對象中。
3.9.3。以編程方式創建嵌入式數據庫
EmbeddedDatabaseBuilder類爲以編程方式構建嵌入式數據庫提供了一個流暢的API。當您需要在獨立環境或獨立集成測試中創建嵌入式數據庫時,可以使用此功能,如下例所示:
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
db.shutdown()
有關所有受支持選項的詳細信息,請參閱EmbeddedDatabaseBuilder的javadoc。
您還可以使用EmbeddedDatabaseBuilder通過使用Java配置來創建嵌入式數據庫,如下面的示例所示:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
}
}
3.9.4。選擇嵌入式數據庫類型
本節介紹如何選擇Spring支持的三種嵌入式數據庫之一。它包括下列主題:
使用HSQL
Spring支持HSQL 1.8.0及以上版本。如果沒有顯式指定類型,則HSQL是默認的嵌入式數據庫。要顯式地指定HSQL,請將嵌入式數據庫標記的type屬性設置爲HSQL。如果使用builder API,則使用EmbeddedDatabaseType. hsql調用setType(EmbeddedDatabaseType)方法。
使用H2
Spring支持H2數據庫。要啓用H2,請將嵌入數據庫標記的type屬性設置爲H2。如果使用builder API,則使用EmbeddedDatabaseType. h2調用setType(EmbeddedDatabaseType)方法。
使用Derby
Spring支持Apache Derby 10.5及以上版本。要啓用Derby,請將嵌入數據庫標記的type屬性設置爲Derby。如果使用builder API,則使用EmbeddedDatabaseType. derby調用setType(EmbeddedDatabaseType)方法。
3.9.5。使用嵌入式數據庫測試數據訪問邏輯
嵌入式數據庫提供了一種測試數據訪問代碼的輕量級方法。下一個示例是使用嵌入式數據庫的數據訪問集成測試模板。當嵌入式數據庫不需要跨測試類重用時,使用這樣的模板對於一次性使用非常有用。然而,如果您希望創建一個共享的嵌入式數據庫在一個測試套件,考慮使用Spring和TestContext框架和配置Spring ApplicationContext的嵌入式數據庫作爲一個bean創建所述嵌入式數據庫通過使用Spring XML和創建嵌入式數據庫編程。下面的清單顯示了測試模板:
public class DataAccessIntegrationTestTemplate {
private EmbeddedDatabase db;
@BeforeEach
public void setUp() {
// creates an HSQL in-memory database populated from default scripts
// classpath:schema.sql and classpath:data.sql
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.addDefaultScripts()
.build();
}
@Test
public void testDataAccess() {
JdbcTemplate template = new JdbcTemplate(db);
template.query( /* ... */ );
}
@AfterEach
public void tearDown() {
db.shutdown();
}
}
3.9.6。爲嵌入式數據庫生成唯一的名稱
如果開發團隊的測試套件無意中試圖重新創建相同數據庫的其他實例,那麼他們在使用嵌入式數據庫時經常會遇到錯誤。這可以很容易地發生,如果一個XML配置文件或@Configuration
類負責創建嵌入式數據庫和相應的配置然後重用跨多個相同測試套件中的測試場景(即在同一JVM進程)——例如,對嵌入式數據庫的集成測試ApplicationContext配置不同只對bean定義概要文件是活躍的。
這些錯誤的根本原因是Spring的EmbeddedDatabaseFactory(由<jdbc: embeddatabase > XML namespace元素和用於Java配置的EmbeddedDatabaseBuilder在內部使用)將嵌入式數據庫的名稱設置爲testdb(如果沒有另外指定的話)。對於<jdbc:嵌入式數據庫>,嵌入式數據庫通常被分配一個與bean的id相等的名稱(通常類似於dataSource)。因此,創建嵌入式數據庫的後續嘗試不會導致新的數據庫。相反,重用相同的JDBC連接URL,並且嘗試創建一個新的嵌入式數據庫,實際上指向一個由相同配置創建的現有嵌入式數據庫。
爲了解決這個常見問題,Spring Framework 4.2提供了對爲嵌入式數據庫生成惟一名稱的支持。要啓用生成的名稱,請使用以下選項之一。
-
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
-
EmbeddedDatabaseBuilder.generateUniqueName()
-
<jdbc:embedded-database generate-name="true" … >
3.9.7。擴展嵌入式數據庫支持
您可以通過兩種方式擴展Spring JDBC嵌入式數據庫支持:
- 實現EmbeddedDatabaseConfigurer來支持新的嵌入式數據庫類型。
- 實現DataSourceFactory來支持新的數據源實現,比如管理嵌入式數據庫連接的連接池。
我們鼓勵您在GitHub Issues上爲Spring社區提供擴展。
3.10。初始化數據源
org.springframework.jdbc.datasource。init包提供對初始化現有數據源的支持。嵌入式數據庫支持爲創建和初始化應用程序的數據源提供了一個選項。但是,有時可能需要初始化某個服務器上運行的實例。
3.10.1。使用Spring XML初始化數據庫
如果你想要初始化一個數據庫,你可以提供一個對DataSource bean的引用,你可以使用spring-jdbc命名空間中的initialize-database標籤:
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>
前面的示例針對數據庫運行兩個指定的腳本。第一個腳本創建一個模式,第二個腳本用一個測試數據集填充表。腳本位置也可以是帶有通配符的模式,通配符是Spring中用於資源的常見Ant樣式(例如,classpath*:/com/foo/**/sql/*-data.sql)。如果使用模式,腳本將按照URL或文件名的詞法順序運行。
數據庫初始化器的默認行爲是無條件地運行提供的腳本。這可能不是您想要的—例如,如果您對一個已經包含測試數據的數據庫運行腳本。通過遵循常見的模式(如前面所示),先創建表,然後插入數據,可以減少意外刪除數據的可能性。如果表已經存在,則第一步將失敗。
但是,爲了獲得對現有數據的創建和刪除的更多控制,XML名稱空間提供了一些附加選項。第一個是用來開關初始化的標誌。您可以根據環境來設置它(例如從系統屬性或環境bean中提取一個布爾值)。下面的示例從系統屬性獲取一個值:
<jdbc:initialize-database data-source="dataSource"
enabled="#{systemProperties.INITIALIZE_DATABASE}">
<jdbc:script location="..."/>
</jdbc:initialize-database>
控制現有數據所發生的情況的第二種選擇是更加容忍失敗。爲此,可以控制初始化器忽略它從腳本執行的SQL中的某些錯誤,如下面的示例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我們說的是,我們預期腳本有時會對空數據庫運行,因此腳本中有一些DROP語句會失敗。因此,失敗的SQL DROP語句將被忽略,但其他失敗將導致異常。如果您的SQL方言不支持DROP,如果存在(或類似的情況),但是您希望在重新創建之前無條件地刪除所有測試數據,那麼這是非常有用的。在這種情況下,第一個腳本通常是一組DROP語句,然後是一組CREATE語句。
可以將ignore-failure選項設置爲NONE(默認值)、DROPS(忽略失敗的drop)或ALL(忽略所有失敗)。
每個語句之間應該用;或新行,如果;角色在劇本中根本不存在。你可以控制全局或腳本的腳本,如下面的例子所示:
<jdbc:initialize-database data-source="dataSource" separator="@@">
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
在這個例子中,兩個測試數據腳本使用@@作爲語句分隔符,並且只使用db-schema。sql使用;。此配置指定默認分隔符爲@@,並覆蓋了db-schema腳本的默認分隔符。
如果需要比XML名稱空間更多的控制,可以直接使用DataSourceInitializer並將其定義爲應用程序中的組件。
依賴於數據庫的其他組件的初始化
大量的應用程序(那些在Spring上下文啓動之後才使用數據庫的應用程序)可以使用數據庫初始化器,而不會帶來更多的麻煩。如果您的應用程序不是其中之一,您可能需要閱讀本節的其餘部分。
數據庫初始化器依賴於數據源實例並運行其初始化回調中提供的腳本(類似於XML bean定義中的init-method、組件中的@PostConstruct方法或實現InitializingBean的組件中的afterPropertiesSet()方法)。如果其他bean依賴於相同的數據源並在初始化回調中使用該數據源,則可能會出現問題,因爲數據尚未初始化。一個常見的例子是在應用程序啓動時急切地初始化並從數據庫加載數據的緩存。
要解決這個問題,您有兩個選項:將緩存初始化策略更改爲稍後的階段,或者確保先初始化數據庫初始化器。
如果應用程序在您的控制之下,則更改緩存初始化策略可能很容易,否則就不容易了。關於如何實現這一點,一些建議包括:
- 讓緩存在第一次使用時延遲初始化,這樣可以提高應用程序的啓動時間。
- 讓你的緩存或一個單獨的組件初始化緩存實現生命週期或SmartLifecycle。當應用程序上下文啓動時,您可以通過設置它的autoStartup標誌來自動啓動一個SmartLifecycle,您還可以通過在封閉上下文上調用ConfigurableApplicationContext.start()來手動啓動一個生命週期。
- 使用Spring ApplicationEvent或類似的自定義觀察者機制來觸發緩存初始化。ContextRefreshedEvent在準備使用時(在所有bean初始化之後)總是由上下文發佈,所以這通常是一個有用的掛鉤(默認情況下SmartLifecycle就是這樣工作的)。
確保首先初始化數據庫初始化器也很容易。關於如何實施這一點,一些建議包括:
- 依賴於Spring BeanFactory的默認行爲,即bean是按註冊順序初始化的。您可以通過在XML配置中採用一組<import/>元素的常見實踐來輕鬆地安排這些元素,這些元素對應用程序模塊進行排序,並確保首先列出數據庫和數據庫初始化。
- 將數據源和使用它的業務組件分離,並通過將它們放在單獨的ApplicationContext實例中來控制它們的啓動順序(例如,父上下文包含數據源,子上下文包含業務組件)。這種結構在Spring web應用程序中很常見,但可以更廣泛地應用。