主要內(nèi)容
- 定義Spring的數(shù)據(jù)訪問支持
- 配置數(shù)據(jù)庫資源
- 使用Spring提供的JDBC模板
寫在前面:經(jīng)過上一篇文章的學(xué)習(xí),我們掌握了如何寫web應(yīng)用的控制器層,不過由于只定義了SpitterRepository和SpittleRepository接口,在本地啟動該web服務(wù)的時候會遇到控制器無法注入對應(yīng)的bean的錯誤,因此我決定跳過6~9章,先搞定數(shù)據(jù)庫訪問者一章。
在企業(yè)級應(yīng)用開發(fā)中不可避免得會涉及到數(shù)據(jù)持久化層,在數(shù)據(jù)持久化層的開發(fā)過程中,可能遇到很多陷阱。你需要初始化數(shù)據(jù)庫訪問框架、打開數(shù)據(jù)庫連接、處理各種異常,最后還要記得關(guān)閉連接。如果在這些步驟中你有一步做錯了,那就又丟失公司數(shù)據(jù)的風(fēng)險。妥當?shù)锰幚磉@些并不容易,Spring提供了一套完整的數(shù)據(jù)庫訪問框架,用于簡化各種數(shù)據(jù)庫訪問技術(shù)的使用。
在開發(fā)Spttr應(yīng)用的持久層時,你需要在JDBC、Hibernate、Java Perssitence或者其他ORM框架等技術(shù)中進行選擇。Spring扮演的角色是盡量消除你在使用這些技術(shù)時需要寫的重復(fù)代碼,以便開發(fā)人員專注于業(yè)務(wù)邏輯。
10.1 學(xué)習(xí)Spring的數(shù)據(jù)庫訪問哲學(xué)
Spring框架的目標之一就是讓開發(fā)者面向接口編程,Spring的數(shù)據(jù)訪問支持也不例外。
和很多其他應(yīng)用一樣,Spittr應(yīng)用也需要從數(shù)據(jù)庫中讀取信息或者寫入信息到數(shù)據(jù)庫。為了避免持久化相關(guān)的代碼遍布應(yīng)用的各個地方,一般我們會將這些任務(wù)整合到一個模塊中完成,這類模塊通常被稱之為數(shù)據(jù)訪問對象(DAOs)或者repositories。
為了避免業(yè)務(wù)層模塊強依賴于某種類型的數(shù)據(jù)庫(關(guān)系型orNoSQL),數(shù)據(jù)庫訪問層應(yīng)以接口形式對外提供服務(wù)。下圖展示了這個思路:
如你所見,service對象通過接口訪問repository對象,這有很多好處:(1)因為service對象并不限制于某個特定的數(shù)據(jù)訪問實現(xiàn),這使得service對象便于測試;(2)你可以創(chuàng)建這些數(shù)據(jù)庫訪問接口的mock實現(xiàn),這樣即使沒有建立數(shù)據(jù)庫連接你也可以測試service對象;(3)可以顯著加速單元測試的執(zhí)行速度;(4)可以避免某個測試用例因數(shù)據(jù)不一致而失敗。
數(shù)據(jù)訪問層通過repository接口中的幾個方法與service層溝通,這使得應(yīng)用設(shè)計非常靈活,即使將來要更換數(shù)據(jù)庫持久層框架,對應(yīng)用的其他部分的影響也非常小。如果數(shù)據(jù)訪問層的實現(xiàn)細節(jié)散步到應(yīng)用的其他部分,則整個應(yīng)用跟數(shù)據(jù)訪問層緊密耦合在一起。
INTERFACES AND SPRING 如果你讀完上面兩段話之后能夠感覺到我有很強的意愿將持久化層隱藏在接口之后,那說明我正確得表達了自己的想法。我相信接口是書寫松耦合的代碼的關(guān)鍵,不僅是數(shù)據(jù)庫訪問層,應(yīng)該在應(yīng)用的所有模塊之間使用接口進行交互。雖然Spring并沒有強制要求面向接口編程,但是Spring的設(shè)計思想鼓勵面向接口編程——最好通過接口將一個bean裝配到另一個bean的屬性中。
Spring提供了方便的異常體系,也可以幫助開發(fā)者隔離數(shù)據(jù)庫訪問層與應(yīng)用的其他模塊。
10.1.1 了解Spring的數(shù)據(jù)訪問的異常體系
在使用原始的JDBC接口時,如果你不捕獲SQLException,就不能做任何事情。SQLException的意思是在嘗試訪問數(shù)據(jù)庫過程中發(fā)生了某些錯誤,但是并沒有提供足夠的信息告訴開發(fā)人員具體的錯誤原因以及如何修正錯誤。
下列這些情況都可能引發(fā)SQLException:
- 連接數(shù)據(jù)庫失敗;
- 查詢語句中存在語法錯誤;
- 查詢中提到的表或者列不存在;
- 插入或者更新操作違背了數(shù)據(jù)庫一致性;
關(guān)于SQLException最大的問題在于:當捕獲它的時候應(yīng)該如何處理。調(diào)查顯示,很多引起SQLException的故障不能在catch代碼塊中修復(fù)。大部分被拋出的SQLException表示應(yīng)用發(fā)生了致命故障。如果應(yīng)用不能連接數(shù)據(jù)庫,通常意味著應(yīng)用不能繼續(xù)執(zhí)行;同樣地,如果在查詢語句中有錯誤,在運行時能做的工作也很少。
既然我們并不能做些操作來恢復(fù)SQLException,為什么必須捕獲它?
即使你計劃處理一些SQLException,你也必須捕獲SQLException對象然后查看它的屬性才能發(fā)掘出問題的本質(zhì)。這是因為SQLException是一個代之所有數(shù)據(jù)庫訪問相關(guān)問題的異常,而不是針對每個可能的問題定義一個異常類型。
一些持久化框架提供了豐富的異常體系。例如,Hibernate提供了幾乎兩打不通的異常,每種代表一個特定的數(shù)據(jù)庫訪問問題。這令使用Hibernate的開發(fā)者可以為自己想處理的異常書寫catch塊。
即使這樣,Hibernate的異常也只對Hibernate框架有用,如果你使用Hibernate自己的異常體系,就可能使程序的剩余部分強依賴于Hibernate,將來如果想升級為其他的持久化框架會非常麻煩。在這節(jié)開頭的時候說過,我們希望隔離數(shù)據(jù)訪問層和持久化機制的特性。如果在數(shù)據(jù)訪問層處理Hibernate框架拋出的專屬異常,則會影響到應(yīng)用中的其余模塊;如果不這么做,你必須捕獲該持久化的專屬異常,然后重新拋出一個平臺無關(guān)的異常。
SPRING'S PERSISTENCE PLATFORM-AGNOSTIC EXCEPTION
一方面,JDBC提供的異常體系過于普遍——根本沒有異常體系可言;另一方面,Hibernate的異常體系是針對這個框架自己的,因此我們需要一套數(shù)據(jù)庫訪問的異常體系,既具備足夠強的描述能力,又不要跟具體的持久化框架直接關(guān)聯(lián)。
Spring JDBC提供的異常體系同時滿足上述兩個條件。不同于傳統(tǒng)的JDBC,Spring JDBC針對某些具體的問題定義了對應(yīng)的數(shù)據(jù)庫訪問異常。下表展示了Spring 數(shù)據(jù)訪問異常和JDBC的異常之間的對應(yīng)關(guān)系。
如你所見,Spring為在讀取或者寫入數(shù)據(jù)庫時可能出錯的原因設(shè)置了對應(yīng)的異常類型,Spring 實際提供的數(shù)據(jù)庫訪問異常要遠多于表10.1所列出的那些。
Spring在提供如此豐富的異常前提下,還保證這些異常類型跟具體的持久化機制隔離。這意味著無論你使用什么持久化框架,你都可以使用同一套異常定義——持久化機制的選擇與數(shù)據(jù)訪問層實現(xiàn)解耦合。
LOOK, MA! NO CATCH BLOCKS!
表10.1中沒有說明的是:所有這些異常的根對象是DataAccessException,這是一個unchecked exception。換句話說,Spring不會強制你捕獲這些數(shù)據(jù)庫訪問異常。
Spring通過提供unchecked exception,讓開發(fā)者決定是否需要捕獲并處理某個異常。為了充分發(fā)揮Spring的數(shù)據(jù)訪問異常,你最好使用Spring提供的數(shù)據(jù)訪問模板。
10.1.2 模式化數(shù)據(jù)訪問
如果你之前通過飛機出行過,你一定明白在行程過程中最重要的事情是將行李從A地托運到B地。要妥當?shù)猛瓿蛇@個事情需要很多步驟:當你到達機場時,你首先需要檢查行李;然后需要通過機場的安全掃描,以免不小心將可能危害飛行安全的東西帶上飛機;然后行李需要通過長長的傳送帶被運上飛機。如果你需要轉(zhuǎn)乘航線,行李也需要跟著你一起運輸。當你到達最終目的地時,行李會被運下飛機然后放置在傳送帶上,最后,你需要去目的地機場的指定地點領(lǐng)取自己的行李。
雖然在這個過程中有需要步驟,但是你僅僅需要參與其中的一部分。在這個例子中,整個過程就是將行李從出發(fā)城市運輸?shù)侥康某鞘校@個過程是固定的不會改變。在運輸過程可以分成明確的幾步:檢查行李、裝載行李、卸載行李等。在這其中一些步驟也是固定的,每次都一樣:當飛機到達目的地之后,所有行李都需要卸載并放在機場的指定地點。
在指定的節(jié)點,總程序會將一部分工作委托給一個子程序,用于完成更加細節(jié)的任務(wù),這就是總程序中的變量部分。例如,行李的托運開始于乘客自己檢查行李,因為每個乘客的動作都不相同——各自檢查自己的行李,因此總程序中的這個步驟如何執(zhí)行具體取決于每個乘客。用軟件開發(fā)中的術(shù)語描述,上述過程就是模板模式:模板方法規(guī)定整個算法的執(zhí)行過程,將每個步驟的具體細節(jié)通過接口委托給子類完成。
Spring提供的數(shù)據(jù)訪問支持也使用了模板模式。無論你選擇使用什么技術(shù),數(shù)據(jù)訪問的步驟就是固定的幾步(例如,在開始時,你一定需要獲取一個數(shù)據(jù)庫連接;在操作完成后,你一定需要釋放之前獲取的資源),但是每一步具體怎么實現(xiàn)有所不同。你用不同的方法查詢或者更新不同的數(shù)據(jù),這些屬于數(shù)據(jù)庫訪問過程中的變量。
Spring將數(shù)據(jù)訪問過程中的固定步驟和變量部分分為兩類:模板(templates)和回調(diào)函數(shù)(callbacks)。模板負責(zé)管理數(shù)據(jù)訪問過程中的固定步驟,而由你定制的業(yè)務(wù)邏輯則寫在回調(diào)函數(shù)中。下圖顯示了這兩類對象的責(zé)任和角色:
如你所見,Spring的模板類處理數(shù)據(jù)訪問的固定步驟——事務(wù)管理、資源管理和異常處理;與此同時,跟應(yīng)用相關(guān)的數(shù)據(jù)訪問任務(wù)——創(chuàng)建語句、綁定參數(shù)和處理結(jié)果集等,則需要在回調(diào)函數(shù)中完成。這種框架十分優(yōu)雅,作為開發(fā)人員你只需要關(guān)注具體的數(shù)據(jù)訪問邏輯。
Spring提供了集中不同的模板,開發(fā)者根據(jù)項目中使用的持久化框架選擇對應(yīng)的模板工具類。如果你使用原始的JDBC方式,則可以使用JdbcTemplate;如果你更傾向于使用ORM框架,則可以使用HibernateTemplate和JpaTemplate。表10.2列出了Spring提供的數(shù)據(jù)訪問模板。
Spring為不同的持久化技術(shù)提供了對應(yīng)的數(shù)據(jù)訪問模板,在這一章中并不能一一講述。因此我們將選擇最有效和你最可能使用的進行講解。
這一章首先介紹JDBC技術(shù),因為它最簡單;在后面還會介紹Hibernate和JPA——兩種最流行的基于POJO的ORM框架。PS:除了《Spring in Action》中的這幾種持久化技術(shù),現(xiàn)在更加流行的是Mybatis框架,后續(xù)我會專門寫對應(yīng)的總結(jié)和學(xué)習(xí)筆記。
但是,所有這些持久化框架都需要依賴于具體的數(shù)據(jù)源,因此在開始學(xué)習(xí)templates和repositories之前,需要學(xué)習(xí)在Spring中如何配置數(shù)據(jù)源——用于連接數(shù)據(jù)庫。
10.2 配置數(shù)據(jù)源
Spring提供了幾種配置數(shù)據(jù)源的方式,列舉如下:
- 通過JDBC驅(qū)動定義數(shù)據(jù)源;
- 從JNDI中查詢數(shù)據(jù)源;
- 從連接池中獲取數(shù)據(jù)源;
對于生產(chǎn)級別的應(yīng)用,我建議使用從數(shù)據(jù)庫連接池中獲取的數(shù)據(jù)源;如果有可能,也可以通過JNDI從應(yīng)用服務(wù)器中獲取數(shù)據(jù)源;接下來首先看下如何配置Spring應(yīng)用從JNDI獲取數(shù)據(jù)源。
10.2.1 使用JNDI數(shù)據(jù)源
Spring應(yīng)用一般部署在某個J2EE容器中,例如WebSphere、JBoss或者Tomcat。開發(fā)者可以在這些服務(wù)器中配置數(shù)據(jù)源,一遍Spring應(yīng)用通過JNDI獲取。按照這種方式配置數(shù)據(jù)源的好處在于:數(shù)據(jù)源配置在應(yīng)用外部,允許應(yīng)用在啟動完成時再請求數(shù)據(jù)源進行數(shù)據(jù)訪問;而且,數(shù)據(jù)源配置在應(yīng)用服務(wù)器中有助于提高性能,且系統(tǒng)管理員可以進行熱切換。
首先,需要在tomcat中配置數(shù)據(jù)源,方法參見stackoverflowHow to use JNDI DataSource provided by Tomcat in Spring?
在SpringXML配置文件中使用<jee:jndi-lookup>元素定義數(shù)據(jù)源對應(yīng)的Spring bean。Spring應(yīng)用根據(jù)jndi-name從Tomcat容器中查找數(shù)據(jù)源;如果應(yīng)用是運行Java應(yīng)用服務(wù)器中,則需要設(shè)置resource-ref為true,這樣在查詢的時候會在jndi-name指定的名字前面加上java:comp/env/。
<jee:jndi-lookup id="dataSource"
jndi-name="/jdbc/SpitterDS"
resource-ref="true" />
如果你使用JavaConfig,則可以使用JndiObjectFactoryBean從JNDI中獲取DataSource:
@Bean
public JndiObjectFactoryBean dataSource() {
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
jndiObjectFB.setJndiName("/jdbc/SpittrDS");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
return jndiObjectFB;
}
顯然,在這里Java配置文件需要寫更多代碼,一般而言JavaConfig要比XML配置文件更簡單,這是個例外。
10.2.2 使用數(shù)據(jù)庫連接池
盡管Spring自身不提供數(shù)據(jù)連接池,但可以和很多第三方庫集成使用,例如:
- Apache Commons DBCP(http://commons.apache.org/proper/commons-dbcp/)
- c3p0(http://sourceforge.net/projects/c3p0/)
- BoneCP(http://jolbox.com/)
最常用的是DBCP,首先需要在pom文件中添加對應(yīng)的依賴,代碼如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0</version>
</dependency>
關(guān)于commons-dbcp版本的區(qū)別:commons-dbcp現(xiàn)在分成了2個大版本,不同的版本要求的JDK不同:
- DBCP 2.X compiles and runs under Java 7 only (JDBC 4.1)
- DBCP 1.4 compiles and runs under Java 6 only (JDBC 4)
如果在XML文件中使用,則可以使用下列代碼配置DBCP的BasicDataSource:
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
p:username="sa"
p:password=""
p:initialSize="5" />
如果你使用Java配置文件,則可以使用下列代碼配置DataSourcebean。
@Bean
public BasicDataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
ds.setInitialSize(5);
return ds;
}
前四個屬性屬于配置BasicDataSource的必備屬性,driverClassName指定JDBC驅(qū)動類的全稱,這里我們配置了H2數(shù)據(jù)庫的驅(qū)動;url屬性用于設(shè)置完整的數(shù)據(jù)庫地址;username和password分別指定用戶名和密碼。BasicDataSource中還有其他的屬性,可以設(shè)置數(shù)據(jù)連接池的屬性,例如,initialSize屬性用于指定連接池初始化時建立幾個數(shù)據(jù)庫連接。對于dbcp1.4系列,BasicDataSource的屬性可列舉如下表10.3所示:
對于dbcp2.x系列,如果你希望了解更多BasicDataSource的屬性,可參照官方文檔:dbcp2配置。
10.2.3 使用基于JDBC驅(qū)動的數(shù)據(jù)源
在Spring中最簡單的數(shù)據(jù)源就是通過JDBC驅(qū)動配置的數(shù)據(jù)源。Spring提供了三個相關(guān)的類供開發(fā)者選擇(都在org.springframework.jdbc.datasource包中):
- DriverManagerDataSource——每次請求連接時都返回新的連接,用過的連接會馬上關(guān)閉并釋放資源;
- SimpleDriverDataSource——功能和DriverManagerDataSource相同,不同之處在于該類直接和JDBC驅(qū)動交互,免去了類在特定環(huán)境(如OSGi容器)中可能遇到的類加載問題。
- SingleConnectionDataSource——每次都返回同一個連接對象,可以理解為只有1個連接的數(shù)據(jù)源連接池。
配置這些數(shù)據(jù)源跟之前配置DBCP的BasicDataSource類似,例如,可以用下列代碼配置DriverManagerDataSource
@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
return ds;
}
上述配置代碼的XML形式如下:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
p:username="sa"
p:password="" />
由于上述這三個數(shù)據(jù)源對象對多線程應(yīng)用的支持都不好,因此強烈建議直接使用數(shù)據(jù)庫連接池。
10.2.4 使用嵌入式數(shù)據(jù)源
嵌入式數(shù)據(jù)源作為應(yīng)用的一部分運行,非常適合在開發(fā)和測試環(huán)境中使用,但是不適合用于生產(chǎn)環(huán)境。因為在使用嵌入式數(shù)據(jù)源的情況下,你可以在每次應(yīng)用啟動或者每次運行單元測試之前初始化測試數(shù)據(jù)。
使用Spring的jdbc名字空間配置嵌入式數(shù)據(jù)源非常簡單,下列代碼顯示了如何使用jdbc名字空間配置嵌入式的H2數(shù)據(jù)庫,并配置需要初始化的數(shù)據(jù)。
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath*:schema.sql" />
<jdbc:script location="classpath*:test-data.sql" />
</jdbc:embedded-database>
<jdbc:embedded-database>的type屬性設(shè)置為H2表明嵌入式數(shù)據(jù)庫的類型是H2數(shù)據(jù)庫(確保引入了H2的依賴庫)。在<jdbc:embedded-database>配置中,可以配置多個<jdbc:script>元素,用于設(shè)置和初始化數(shù)據(jù)庫:在這個例子中,schema.sql文件中包含用于創(chuàng)建數(shù)據(jù)表的關(guān)系;test-data.sql文件中用于插入測試數(shù)據(jù)。
如果你使用JavaConfig,則可以使用EmbeddedDatabaseBuilder構(gòu)建嵌入式數(shù)據(jù)源:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath*:schema.sql")
.addScript("classpath*:test-data.sql")
.build();
}
可以看出,setType()方法的作用等同于<jdbc:embedded-database>元素的type屬性,addScript()方法的作用等同于<jdbc:script>元素。
10.2.5 使用profiles選擇數(shù)據(jù)源
一般需要在不同的環(huán)境(日常環(huán)境、性能測試環(huán)境、預(yù)發(fā)環(huán)境和生產(chǎn)環(huán)境等等)中配置不同的數(shù)據(jù)源,例如,在開發(fā)時非常適合使用嵌入式數(shù)據(jù)源、在QA環(huán)境中比較適合使用DBCP的BasicDataSource、在生產(chǎn)環(huán)境中則適合使用<jee:jndi-lookup>元素,即使用JNDI查詢數(shù)據(jù)源。
在Spring實戰(zhàn)3:裝配bean的進階知識一文中我們探討過Spring的bean-profiles特性,這里就需要給不同的數(shù)據(jù)源配置不同的profiles,Java配置文件的內(nèi)容如下所示:
package org.test.spittr.config;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfiguration {
@Profile("development")
@Bean
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath*:schema.sql")
.addScript("classpath*:test-data.sql")
.build();
}
@Profile("qa")
@Bean
public BasicDataSource basicDataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
ds.setInitialSize(5); //初始大小
ds.setMaxTotal(10); //數(shù)據(jù)庫連接池大小
return ds;
}
@Profile("production")
@Bean
public DataSource dataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("/jdbc/SpittrDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource)jndiObjectFactoryBean.getObject();
}
}
利用@Profile注解,Spring應(yīng)用可以運行時再根據(jù)激活的profile選擇指定的數(shù)據(jù)源。在上述代碼中,當development對應(yīng)的profile被激活時,應(yīng)用會使用嵌入式數(shù)據(jù)源;當qa對應(yīng)的profile被激活時,應(yīng)用會使用DBCP的BasicDataSource;當production對應(yīng)的profile被激活時,應(yīng)用會使用從JNDI中獲取的數(shù)據(jù)源。
上述代碼對應(yīng)的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:p="http://www.springframework.org/schema/p"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<beans profile="qa">
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
p:username="sa"
p:password=""
p:initialSize="5" />
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource"
jndi-name="/jdbc/SpittrDS"
resource-ref="true"/>
</beans>
<beans profile="development">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath*:schema.sql" />
<jdbc:script location="classpath*:test-data.sql" />
</jdbc:embedded-database>
</beans>
</beans>
建立好數(shù)據(jù)庫連接后,就可以執(zhí)行訪問數(shù)據(jù)庫的任務(wù)了。正如之前提到的,Spring對很多持久化技術(shù)提供了支持,包括JDBC、Hibernate和Java Persistence API(API)。在下一小節(jié)中,我們首先介紹如何在Spring應(yīng)用中使用JDBC書寫持久層。
10.3 在Spring應(yīng)用中使用JDBC
在實際開發(fā)過程中有很多持久化技術(shù)可供選擇:Hibernate、iBATIS和JPA等。盡管如此,還是有很多應(yīng)用使用古老的方法即JDBC技術(shù),來訪問數(shù)據(jù)庫。
使用JDBC技術(shù)不需要開發(fā)人員學(xué)習(xí)新的框架,因為它就是基于SQL語言運行的。JDBC技術(shù)更加靈活,開發(fā)人員可以調(diào)整的余地很大,JDBC技術(shù)允許開發(fā)人員充分利用數(shù)據(jù)庫的本地特性,而在其他ORM框架中可能做不到如此靈活和可定制。
除了上述提到的靈活性、可定制能力,JDBC技術(shù)也有一些缺點。
10.3.1 分析JDBC代碼
開發(fā)者使用JDBC技術(shù)提供的API可以非常底層得操作數(shù)據(jù)庫,同時也意味著,開發(fā)者需要負責(zé)處理數(shù)據(jù)訪問過程中的各個具體步驟:管理數(shù)據(jù)庫資源和處理數(shù)據(jù)庫訪問異常。如果你使用JDBC插入數(shù)據(jù)庫,在這個例子中,假設(shè)需要插入一條spitter數(shù)據(jù),則可以使用如下代碼:
@Component
public class SpitterDao {
private static final String SQL_INSERT_SPITTER =
"insert into spitter (username, password, firstName, lastName) values (?, ?, ?, ?)";
@Autowired
private DataSource dataSource;
public void addSpitter(Spitter spitter) {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(SQL_INSERT_SPITTER);
stmt.setString(1, spitter.getUsername());
stmt.setString(2, spitter.getPassword());
stmt.setString(3, spitter.getFirstName());
stmt.setString(4, spitter.getLastName());
stmt.execute();
} catch (SQLException e) {
//do something...not sure what, though
} finally {
try {
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
//I'm even less sure about what to do here
}
}
}
}
addSpitter函數(shù)一共有28行,但是只有6行是真正的業(yè)務(wù)邏輯。為什么如此簡單的操作也需要這么多代碼?JDBC需要開發(fā)者自己管理數(shù)據(jù)庫連接、自己管理SQL語句,以及自己處理可能拋出的異常。
對于SQLException,開發(fā)者并不清楚具體該如何處理該異常(該異常并未指明具體的錯誤原因),卻被迫需要捕獲該異常。如果在執(zhí)行插入語句時發(fā)生錯誤,你需要捕獲該異常;如果在關(guān)閉statement和connection資源時發(fā)生錯誤,你也需要捕獲該異常,但是捕獲后你并不能做實際的有意義的操作。
同樣,如果你需要更新一條spitter記錄,則可使用下列代碼:
private static final String SQL_UPDATE_SPITTER =
"update spitter set username = ?, password = ?, firstName = ?, lastName=? where id = ?";
public void saveSpitter(Spitter spitter) {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(SQL_UPDATE_SPITTER);
stmt.setString(1, spitter.getUsername());
stmt.setString(2, spitter.getPassword());
stmt.setString(3, spitter.getFirstName());
stmt.setString(4, spitter.getLastName());
stmt.setLong(5, spitter.getId());
stmt.execute();
} catch (SQLException e) {
// Still not sure what I'm supposed to do here
} finally {
try {
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
// or here
}
}
}
這一次,saveSpitter函數(shù)用于更新數(shù)據(jù)庫中的一行記錄,可以看出,有很多重復(fù)代碼。理想情況應(yīng)該是:你只需要寫特定功能相關(guān)的代碼。
為了補足JDBC體驗之旅,我們再看看如何使用JDBC從數(shù)據(jù)庫中查詢一條記錄,例子代碼如下:
private static final String SQL_SELECT_SPITTER =
"select id, username, firstName, lastName from spitter where id = ?";
public Spitter findOne(long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(SQL_SELECT_SPITTER);
stmt.setLong(1, id);
rs = stmt.executeQuery();
Spitter spitter = null;
if (rs.next()) {
spitter = new Spitter();
spitter.setId(rs.getLong("id"));
spitter.setUsername(rs.getString("username"));
spitter.setPassword(rs.getString("password"));
spitter.setFirstName(rs.getString("firstName"));
spitter.setLastName(rs.getString("lastName"));
}
return spitter;
} catch (SQLException e) {
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) { }
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) { }
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) { }
}
}
return null;
}
這個函數(shù)跟之前的insert和update例子一樣啰嗦冗長:幾乎只有20%的代碼是真正有用的業(yè)務(wù)邏輯,而80%的代碼則是模板樣式代碼。
可以看出,使用JDBC持久化技術(shù),就需要編寫大量的模板樣式代碼,用于創(chuàng)建連接、創(chuàng)建statements和處理異常。另外,上述提到的模板樣式代碼在數(shù)據(jù)庫訪問過程中又非常重要:釋放資源和處理異常等,這都能提高數(shù)據(jù)訪問的穩(wěn)定性。如果沒有這些操作,應(yīng)用就無法及時處理錯誤、資源始終被占用,會導(dǎo)致內(nèi)存泄露。因此,開發(fā)者需要一個數(shù)據(jù)庫訪問框架,用于處理這些模板樣式代碼。
10.3.2 使用Spring提供的JDBC模板
Spring提供的JDBC框架負責(zé)管理資源和異常處理,從而可以簡化開發(fā)者的JDBC代碼。開發(fā)者只需要編寫寫入和讀取數(shù)據(jù)庫相關(guān)的代碼即可。
正如在之前的小節(jié)中論述過的,Spring將數(shù)據(jù)庫訪問過程中的模板樣式代碼封裝到各個模板類中了,對于JDBC,Spring提供了下列三個模板類:
- JdbcTemplate——最基本的JDBC模板,這個類提供了簡單的接口,通過JDBC和索引參數(shù)訪問數(shù)據(jù)庫;
- NameParameterJdbcTemplate——這個JDBC模板類是的開發(fā)者可以執(zhí)行綁定了指定參數(shù)名稱的SQL,而不是索引參數(shù);
- SimpleJdbcTemplate——這個版本的JDBC模板利用了Java 5的一些特性,例如自動裝箱/拆箱、接口和變參列表等,用于簡化JDBC模板的使用。
從Spring 3.1開始已經(jīng)將SimpleJdbcTemplate廢棄,它所擁有的Java 5那些特性被添加到原來的JdbcTemplate中了,因此你可以直接使用JdbcTemplate;當你希望在查詢中使用命名參數(shù)時,則可以選擇使用NamedParameterJdbcTemplate。
INSERTING DATA USING JDBCTEMPLATE
要使用JdbcTemplate對象,需要為之傳遞DataSource對象。如果使用Java Config配置JdbcTemplatebean,則對應(yīng)代碼如下:
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
這里通過構(gòu)造函數(shù)將DataSource對象注入,而dataSourcebean則來自DataSourceConfiguration文件中定義的javax.sql.DataSource實例。
然后就可以在自己的repository實現(xiàn)中注入jdbcTemplatebean,例如,假設(shè)Spitter的repository使用jdbcTemplatebean,代碼可列舉如下:
package org.test.spittr.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.stereotype.Repository;
import org.test.spittr.data.Spitter;
@Repository
public class JdbcSpitterRepository implements SpitterRepository {
@Autowired
private JdbcOperations jdbcOperations;
.....
}
這里JdbcSpitterRepository被@Repository注解修飾,component-scanning掃描機制起作用時會自動創(chuàng)建對應(yīng)的bean。按照“面向接口編程”的原則,我們定義JdbcOperations接口對應(yīng)的實例,而JdbcTemplate實現(xiàn)了這個接口,從而使得JdbcSpitterRepository與JdbcTemplate解耦合。
使用JdbcTemplate實現(xiàn)的addSpitter()方法非常簡單,代碼如下:
public void addSpitter(Spitter spitter) {
jdbcOperations.update(SQL_INSERT_SPITTER,
spitter.getUsername(),
spitter.getPassword(),
spitter.getFirstName(),
spitter.getLastName());
}
可以看出,這個版本的addSpitter十分簡單,不強制開發(fā)者寫任何管理資源和處理異常的代碼,只有插入語句和對應(yīng)的參數(shù)。
當調(diào)用update()方法時,JdbcTemplate獲取一個連接、創(chuàng)建一個statement,并執(zhí)行插入語句。
JdbcTemplate內(nèi)部捕獲了可能拋出的SQLException異常,然后轉(zhuǎn)為更具體的數(shù)據(jù)庫訪問異常,并重新拋出。由于Spring的數(shù)據(jù)庫訪問異常都是運行時異常,開發(fā)者可以自己決定是否捕獲這些異常。
READING DATA WITH JDBCTEMPLATE
使用JdbcTemplate工具從數(shù)據(jù)庫中讀取數(shù)據(jù)也非常簡單,下列代碼展示了改造過后的findOne()函數(shù):調(diào)用JdbctTemplate的queryForObject函數(shù),用于通過ID查詢Spitter對象。
public Spitter findOne(long id) {
return jdbcOperations.queryForObject(
SQL_SELECT_SPITTER,
new SpitterRowMapper(),
id);
}
private static final class SpitterRowMapper implements RowMapper<Spitter> {
public Spitter mapRow(ResultSet resultSet, int i) throws SQLException {
return new Spitter(
resultSet.getLong("id"),
resultSet.getString("firstName"),
resultSet.getString("lastName"),
resultSet.getString("username"),
resultSet.getString("password"));
}
}
findOne()函數(shù)使用JdbcTemplate的queryForObject()方法從數(shù)據(jù)庫中查詢Spitter記錄。queryForObject()方法包括三個參數(shù):
- SQL字符串,用于從數(shù)據(jù)庫中查詢數(shù)據(jù);
- RowMapper對象,用于從結(jié)果集ResultSet中提取數(shù)據(jù)并構(gòu)造Spitter對象;
- 變量列表,用于指定查詢參數(shù)(這里是通過id查詢)。
這里需要注意SpitterRowMapper類,它實現(xiàn)了RowMapper接口,對于查詢結(jié)果,JdbcTemplate調(diào)用mapRow()方法——一個ResultSet參數(shù)和一個row number參數(shù)。mapRow()方法的主要作用是:從結(jié)果集中取出對應(yīng)屬性的值,并構(gòu)造一個Spitter對象。
和addSpitter()方法相同,findOne()方法也沒有那些JDBC模板樣式代碼,只有純粹的用于查詢Spitter數(shù)據(jù)的代碼。
10.4 總結(jié)
數(shù)據(jù)就像應(yīng)用的血液,在某些以數(shù)據(jù)為中心的業(yè)務(wù)中,數(shù)據(jù)本身就是應(yīng)用。在企業(yè)級應(yīng)用開發(fā)中,編寫穩(wěn)定、簡單、性能良好的數(shù)據(jù)訪問層非常重要。
JDBC是Java處理關(guān)系型數(shù)據(jù)的基本技術(shù)。原生的JDBC技術(shù)并不完美,開發(fā)者不得不寫很多模板樣式代碼,用于管理資源和處理異常。Spring提供了對應(yīng)的模板工具類,用于消除這些模板樣式代碼。
后記:最近在項目開發(fā)中,遇到一次高并發(fā)下數(shù)據(jù)庫成為性能瓶頸的情況,對數(shù)據(jù)訪問層的各個階段有了深入的了解:建立數(shù)據(jù)庫連接、轉(zhuǎn)換SQL語句、執(zhí)行SQL語句、獲取執(zhí)行結(jié)果、釋放資源。我們在項目開發(fā)中使用的數(shù)據(jù)庫連接池是德魯伊(DruidDataSource),它的配置跟DBCP類似,在實際開發(fā)中,我們需要理解每個配置項的含義,用于性能調(diào)優(yōu)。后續(xù)我會寫一篇關(guān)于數(shù)據(jù)庫連接池的文章。
另外,我們現(xiàn)在開發(fā)中最常用的是Mybatis框架,具體內(nèi)容可以參考《Java Persistence With Mybaits 3》一書,也可以參考Mybatis-Spring教程(中文版)
本號專注于后端技術(shù)、JVM問題排查和優(yōu)化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發(fā)者的工作和成長經(jīng)驗,期待你能在這里有所收獲。