主要內(nèi)容
- 定義Spring的數(shù)據(jù)訪問支持
- 配置數(shù)據(jù)庫資源
- 使用Spring提供的JDBC模板
寫在前面:經(jīng)過上一篇文章的學(xué)習(xí),我們掌握了如何寫web應(yīng)用的控制器層浦箱,不過由于只定義了SpitterRepository和SpittleRepository接口,在本地啟動(dòng)該web服務(wù)的時(shí)候會(huì)遇到控制器無法注入對(duì)應(yīng)的bean的錯(cuò)誤构灸,因此我決定跳過6~9章泽裳,先搞定數(shù)據(jù)庫訪問者一章。
在企業(yè)級(jí)應(yīng)用開發(fā)中不可避免得會(huì)涉及到數(shù)據(jù)持久化層淋纲,在數(shù)據(jù)持久化層的開發(fā)過程中,可能遇到很多陷阱触机。你需要初始化數(shù)據(jù)庫訪問框架帚戳、打開數(shù)據(jù)庫連接、處理各種異常儡首,最后還要記得關(guān)閉連接片任。如果在這些步驟中你有一步做錯(cuò)了,那就又丟失公司數(shù)據(jù)的風(fēng)險(xiǎn)蔬胯。妥當(dāng)?shù)锰幚磉@些并不容易对供,Spring提供了一套完整的數(shù)據(jù)庫訪問框架,用于簡化各種數(shù)據(jù)庫訪問技術(shù)的使用氛濒。
在開發(fā)Spttr應(yīng)用的持久層時(shí)产场,你需要在JDBC、Hibernate舞竿、Java Perssitence或者其他ORM框架等技術(shù)中進(jìn)行選擇京景。Spring扮演的角色是盡量消除你在使用這些技術(shù)時(shí)需要寫的重復(fù)代碼,以便開發(fā)人員專注于業(yè)務(wù)邏輯骗奖。
10.1 學(xué)習(xí)Spring的數(shù)據(jù)庫訪問哲學(xué)
Spring框架的目標(biāo)之一就是讓開發(fā)者面向接口編程确徙,Spring的數(shù)據(jù)訪問支持也不例外醒串。
和很多其他應(yīng)用一樣,Spittr應(yīng)用也需要從數(shù)據(jù)庫中讀取信息或者寫入信息到數(shù)據(jù)庫鄙皇。為了避免持久化相關(guān)的代碼遍布應(yīng)用的各個(gè)地方芜赌,一般我們會(huì)將這些任務(wù)整合到一個(gè)模塊中完成,這類模塊通常被稱之為數(shù)據(jù)訪問對(duì)象(DAOs)或者repositories伴逸。
為了避免業(yè)務(wù)層模塊強(qiáng)依賴于某種類型的數(shù)據(jù)庫(關(guān)系型orNoSQL)缠沈,數(shù)據(jù)庫訪問層應(yīng)以接口形式對(duì)外提供服務(wù)。下圖展示了這個(gè)思路:
如你所見,service對(duì)象通過接口訪問repository對(duì)象漱竖,這有很多好處:(1)因?yàn)閟ervice對(duì)象并不限制于某個(gè)特定的數(shù)據(jù)訪問實(shí)現(xiàn)禽篱,這使得service對(duì)象便于測(cè)試畜伐;(2)你可以創(chuàng)建這些數(shù)據(jù)庫訪問接口的mock實(shí)現(xiàn)馍惹,這樣即使沒有建立數(shù)據(jù)庫連接你也可以測(cè)試service對(duì)象;(3)可以顯著加速單元測(cè)試的執(zhí)行速度玛界;(4)可以避免某個(gè)測(cè)試用例因數(shù)據(jù)不一致而失敗万矾。
數(shù)據(jù)訪問層通過repository接口中的幾個(gè)方法與service層溝通,這使得應(yīng)用設(shè)計(jì)非常靈活慎框,即使將來要更換數(shù)據(jù)庫持久層框架良狈,對(duì)應(yīng)用的其他部分的影響也非常小。如果數(shù)據(jù)訪問層的實(shí)現(xiàn)細(xì)節(jié)散步到應(yīng)用的其他部分笨枯,則整個(gè)應(yīng)用跟數(shù)據(jù)訪問層緊密耦合在一起薪丁。
INTERFACES AND SPRING 如果你讀完上面兩段話之后能夠感覺到我有很強(qiáng)的意愿將持久化層隱藏在接口之后,那說明我正確得表達(dá)了自己的想法馅精。我相信接口是書寫松耦合的代碼的關(guān)鍵严嗜,不僅是數(shù)據(jù)庫訪問層,應(yīng)該在應(yīng)用的所有模塊之間使用接口進(jìn)行交互洲敢。雖然Spring并沒有強(qiáng)制要求面向接口編程漫玄,但是Spring的設(shè)計(jì)思想鼓勵(lì)面向接口編程——最好通過接口將一個(gè)bean裝配到另一個(gè)bean的屬性中。
Spring提供了方便的異常體系压彭,也可以幫助開發(fā)者隔離數(shù)據(jù)庫訪問層與應(yīng)用的其他模塊睦优。
10.1.1 了解Spring的數(shù)據(jù)訪問的異常體系
在使用原始的JDBC接口時(shí),如果你不捕獲SQLException壮不,就不能做任何事情汗盘。SQLException的意思是在嘗試訪問數(shù)據(jù)庫過程中發(fā)生了某些錯(cuò)誤,但是并沒有提供足夠的信息告訴開發(fā)人員具體的錯(cuò)誤原因以及如何修正錯(cuò)誤询一。
下列這些情況都可能引發(fā)SQLException:
- 連接數(shù)據(jù)庫失斠酢尸执;
- 查詢語句中存在語法錯(cuò)誤;
- 查詢中提到的表或者列不存在缓醋;
- 插入或者更新操作違背了數(shù)據(jù)庫一致性如失;
關(guān)于SQLException最大的問題在于:當(dāng)捕獲它的時(shí)候應(yīng)該如何處理。調(diào)查顯示送粱,很多引起SQLException的故障不能在catch代碼塊中修復(fù)褪贵。大部分被拋出的SQLException表示應(yīng)用發(fā)生了致命故障。如果應(yīng)用不能連接數(shù)據(jù)庫抗俄,通常意味著應(yīng)用不能繼續(xù)執(zhí)行脆丁;同樣地,如果在查詢語句中有錯(cuò)誤动雹,在運(yùn)行時(shí)能做的工作也很少槽卫。
既然我們并不能做些操作來恢復(fù)SQLException,為什么必須捕獲它胰蝠?
即使你計(jì)劃處理一些SQLException歼培,你也必須捕獲SQLException對(duì)象然后查看它的屬性才能發(fā)掘出問題的本質(zhì)。這是因?yàn)?em>SQLException是一個(gè)代之所有數(shù)據(jù)庫訪問相關(guān)問題的異常茸塞,而不是針對(duì)每個(gè)可能的問題定義一個(gè)異常類型躲庄。
一些持久化框架提供了豐富的異常體系。例如钾虐,Hibernate提供了幾乎兩打不通的異常噪窘,每種代表一個(gè)特定的數(shù)據(jù)庫訪問問題。這令使用Hibernate的開發(fā)者可以為自己想處理的異常書寫catch塊效扫。
即使這樣倔监,Hibernate的異常也只對(duì)Hibernate框架有用,如果你使用Hibernate自己的異常體系菌仁,就可能使程序的剩余部分強(qiáng)依賴于Hibernate浩习,將來如果想升級(jí)為其他的持久化框架會(huì)非常麻煩。在這節(jié)開頭的時(shí)候說過掘托,我們希望隔離數(shù)據(jù)訪問層和持久化機(jī)制的特性瘦锹。如果在數(shù)據(jù)訪問層處理Hibernate框架拋出的專屬異常,則會(huì)影響到應(yīng)用中的其余模塊闪盔;如果不這么做弯院,你必須捕獲該持久化的專屬異常,然后重新拋出一個(gè)平臺(tái)無關(guān)的異常泪掀。
SPRING'S PERSISTENCE PLATFORM-AGNOSTIC EXCEPTION
一方面听绳,JDBC提供的異常體系過于普遍——根本沒有異常體系可言;另一方面异赫,Hibernate的異常體系是針對(duì)這個(gè)框架自己的椅挣,因此我們需要一套數(shù)據(jù)庫訪問的異常體系头岔,既具備足夠強(qiáng)的描述能力,又不要跟具體的持久化框架直接關(guān)聯(lián)鼠证。
Spring JDBC提供的異常體系同時(shí)滿足上述兩個(gè)條件峡竣。不同于傳統(tǒng)的JDBC,Spring JDBC針對(duì)某些具體的問題定義了對(duì)應(yīng)的數(shù)據(jù)庫訪問異常量九。下表展示了Spring 數(shù)據(jù)訪問異常和JDBC的異常之間的對(duì)應(yīng)關(guān)系适掰。
如你所見,Spring為在讀取或者寫入數(shù)據(jù)庫時(shí)可能出錯(cuò)的原因設(shè)置了對(duì)應(yīng)的異常類型荠列,Spring 實(shí)際提供的數(shù)據(jù)庫訪問異常要遠(yuǎn)多于表10.1所列出的那些类浪。
Spring在提供如此豐富的異常前提下,還保證這些異常類型跟具體的持久化機(jī)制隔離肌似。這意味著無論你使用什么持久化框架费就,你都可以使用同一套異常定義——持久化機(jī)制的選擇與數(shù)據(jù)訪問層實(shí)現(xiàn)解耦合。
LOOK, MA! NO CATCH BLOCKS!
表10.1中沒有說明的是:所有這些異常的根對(duì)象是DataAccessException川队,這是一個(gè)unchecked exception力细。換句話說,Spring不會(huì)強(qiáng)制你捕獲這些數(shù)據(jù)庫訪問異常呼寸。
Spring通過提供unchecked exception艳汽,讓開發(fā)者決定是否需要捕獲并處理某個(gè)異常猴贰。為了充分發(fā)揮Spring的數(shù)據(jù)訪問異常对雪,你最好使用Spring提供的數(shù)據(jù)訪問模板。
10.1.2 模式化數(shù)據(jù)訪問
如果你之前通過飛機(jī)出行過米绕,你一定明白在行程過程中最重要的事情是將行李從A地托運(yùn)到B地瑟捣。要妥當(dāng)?shù)猛瓿蛇@個(gè)事情需要很多步驟:當(dāng)你到達(dá)機(jī)場(chǎng)時(shí),你首先需要檢查行李栅干;然后需要通過機(jī)場(chǎng)的安全掃描迈套,以免不小心將可能危害飛行安全的東西帶上飛機(jī);然后行李需要通過長長的傳送帶被運(yùn)上飛機(jī)碱鳞。如果你需要轉(zhuǎn)乘航線桑李,行李也需要跟著你一起運(yùn)輸。當(dāng)你到達(dá)最終目的地時(shí)窿给,行李會(huì)被運(yùn)下飛機(jī)然后放置在傳送帶上贵白,最后,你需要去目的地機(jī)場(chǎng)的指定地點(diǎn)領(lǐng)取自己的行李崩泡。
雖然在這個(gè)過程中有需要步驟禁荒,但是你僅僅需要參與其中的一部分。在這個(gè)例子中角撞,整個(gè)過程就是將行李從出發(fā)城市運(yùn)輸?shù)侥康某鞘星喊椋@個(gè)過程是固定的不會(huì)改變勃痴。在運(yùn)輸過程可以分成明確的幾步:檢查行李、裝載行李热康、卸載行李等沛申。在這其中一些步驟也是固定的,每次都一樣:當(dāng)飛機(jī)到達(dá)目的地之后姐军,所有行李都需要卸載并放在機(jī)場(chǎng)的指定地點(diǎn)污它。
在指定的節(jié)點(diǎn),總程序會(huì)將一部分工作委托給一個(gè)子程序庶弃,用于完成更加細(xì)節(jié)的任務(wù)衫贬,這就是總程序中的變量部分。例如歇攻,行李的托運(yùn)開始于乘客自己檢查行李固惯,因?yàn)槊總€(gè)乘客的動(dòng)作都不相同——各自檢查自己的行李,因此總程序中的這個(gè)步驟如何執(zhí)行具體取決于每個(gè)乘客缴守。用軟件開發(fā)中的術(shù)語描述葬毫,上述過程就是模板模式:模板方法規(guī)定整個(gè)算法的執(zhí)行過程,將每個(gè)步驟的具體細(xì)節(jié)通過接口委托給子類完成屡穗。
Spring提供的數(shù)據(jù)訪問支持也使用了模板模式贴捡。無論你選擇使用什么技術(shù),數(shù)據(jù)訪問的步驟就是固定的幾步(例如村砂,在開始時(shí)烂斋,你一定需要獲取一個(gè)數(shù)據(jù)庫連接;在操作完成后础废,你一定需要釋放之前獲取的資源)汛骂,但是每一步具體怎么實(shí)現(xiàn)有所不同。你用不同的方法查詢或者更新不同的數(shù)據(jù)评腺,這些屬于數(shù)據(jù)庫訪問過程中的變量帘瞭。
Spring將數(shù)據(jù)訪問過程中的固定步驟和變量部分分為兩類:模板(templates)和回調(diào)函數(shù)(callbacks)。模板負(fù)責(zé)管理數(shù)據(jù)訪問過程中的固定步驟蒿讥,而由你定制的業(yè)務(wù)邏輯則寫在回調(diào)函數(shù)中蝶念。下圖顯示了這兩類對(duì)象的責(zé)任和角色:
如你所見芋绸,Spring的模板類處理數(shù)據(jù)訪問的固定步驟——事務(wù)管理媒殉、資源管理和異常處理;與此同時(shí)侥钳,跟應(yīng)用相關(guān)的數(shù)據(jù)訪問任務(wù)——?jiǎng)?chuàng)建語句适袜、綁定參數(shù)和處理結(jié)果集等,則需要在回調(diào)函數(shù)中完成舷夺。這種框架十分優(yōu)雅苦酱,作為開發(fā)人員你只需要關(guān)注具體的數(shù)據(jù)訪問邏輯售貌。
Spring提供了集中不同的模板,開發(fā)者根據(jù)項(xiàng)目中使用的持久化框架選擇對(duì)應(yīng)的模板工具類疫萤。如果你使用原始的JDBC方式颂跨,則可以使用JdbcTemplate;如果你更傾向于使用ORM框架扯饶,則可以使用HibernateTemplate和JpaTemplate恒削。表10.2列出了Spring提供的數(shù)據(jù)訪問模板。
Spring為不同的持久化技術(shù)提供了對(duì)應(yīng)的數(shù)據(jù)訪問模板尾序,在這一章中并不能一一講述钓丰。因此我們將選擇最有效和你最可能使用的進(jìn)行講解。
這一章首先介紹JDBC技術(shù)每币,因?yàn)樗詈唵涡。辉诤竺孢€會(huì)介紹Hibernate和JPA——兩種最流行的基于POJO的ORM框架。PS:除了《Spring in Action》中的這幾種持久化技術(shù)兰怠,現(xiàn)在更加流行的是Mybatis框架梦鉴,后續(xù)我會(huì)專門寫對(duì)應(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ū)動(dòng)定義數(shù)據(jù)源;
- 從JNDI中查詢數(shù)據(jù)源塔次;
- 從連接池中獲取數(shù)據(jù)源方篮;
對(duì)于生產(chǎn)級(jí)別的應(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)用一般部署在某個(gè)J2EE容器中,例如WebSphere汁掠、JBoss或者Tomcat略吨。開發(fā)者可以在這些服務(wù)器中配置數(shù)據(jù)源,一遍Spring應(yīng)用通過JNDI獲取考阱。按照這種方式配置數(shù)據(jù)源的好處在于:數(shù)據(jù)源配置在應(yīng)用外部翠忠,允許應(yīng)用在啟動(dòng)完成時(shí)再請(qǐng)求數(shù)據(jù)源進(jìn)行數(shù)據(jù)訪問;而且乞榨,數(shù)據(jù)源配置在應(yīng)用服務(wù)器中有助于提高性能秽之,且系統(tǒng)管理員可以進(jìn)行熱切換当娱。
首先,需要在tomcat中配置數(shù)據(jù)源考榨,方法參見stackoverflowHow to use JNDI DataSource provided by Tomcat in Spring?
在SpringXML配置文件中使用<jee:jndi-lookup>元素定義數(shù)據(jù)源對(duì)應(yīng)的Spring bean跨细。Spring應(yīng)用根據(jù)jndi-name從Tomcat容器中查找數(shù)據(jù)源;如果應(yīng)用是運(yùn)行Java應(yīng)用服務(wù)器中河质,則需要設(shè)置resource-ref為true冀惭,這樣在查詢的時(shí)候會(huì)在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配置文件更簡單乐尊,這是個(gè)例外溃槐。
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文件中添加對(duì)應(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個(gè)大版本对人,不同的版本要求的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;
}
前四個(gè)屬性屬于配置BasicDataSource的必備屬性姻几,driverClassName指定JDBC驅(qū)動(dòng)類的全稱,這里我們配置了H2數(shù)據(jù)庫的驅(qū)動(dòng)势告;url屬性用于設(shè)置完整的數(shù)據(jù)庫地址蛇捌;username和password分別指定用戶名和密碼。BasicDataSource中還有其他的屬性咱台,可以設(shè)置數(shù)據(jù)連接池的屬性络拌,例如,initialSize屬性用于指定連接池初始化時(shí)建立幾個(gè)數(shù)據(jù)庫連接回溺。對(duì)于dbcp1.4系列春贸,BasicDataSource的屬性可列舉如下表10.3所示:
對(duì)于dbcp2.x系列,如果你希望了解更多BasicDataSource的屬性遗遵,可參照官方文檔:dbcp2配置萍恕。
10.2.3 使用基于JDBC驅(qū)動(dòng)的數(shù)據(jù)源
在Spring中最簡單的數(shù)據(jù)源就是通過JDBC驅(qū)動(dòng)配置的數(shù)據(jù)源。Spring提供了三個(gè)相關(guān)的類供開發(fā)者選擇(都在org.springframework.jdbc.datasource包中):
- DriverManagerDataSource——每次請(qǐng)求連接時(shí)都返回新的連接车要,用過的連接會(huì)馬上關(guān)閉并釋放資源允粤;
- SimpleDriverDataSource——功能和DriverManagerDataSource相同,不同之處在于該類直接和JDBC驅(qū)動(dòng)交互,免去了類在特定環(huán)境(如OSGi容器)中可能遇到的類加載問題类垫。
- SingleConnectionDataSource——每次都返回同一個(gè)連接對(duì)象司光,可以理解為只有1個(gè)連接的數(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="" />
由于上述這三個(gè)數(shù)據(jù)源對(duì)象對(duì)多線程應(yīng)用的支持都不好,因此強(qiáng)烈建議直接使用數(shù)據(jù)庫連接池购撼。
10.2.4 使用嵌入式數(shù)據(jù)源
嵌入式數(shù)據(jù)源作為應(yīng)用的一部分運(yùn)行跪削,非常適合在開發(fā)和測(cè)試環(huán)境中使用,但是不適合用于生產(chǎn)環(huán)境迂求。因?yàn)樵谑褂们度胧綌?shù)據(jù)源的情況下碾盐,你可以在每次應(yīng)用啟動(dòng)或者每次運(yùn)行單元測(cè)試之前初始化測(cè)試數(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>配置中付枫,可以配置多個(gè)<jdbc:script>元素,用于設(shè)置和初始化數(shù)據(jù)庫:在這個(gè)例子中驰怎,schema.sql文件中包含用于創(chuàng)建數(shù)據(jù)表的關(guān)系阐滩;test-data.sql文件中用于插入測(cè)試數(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)境装获、性能測(cè)試環(huán)境、預(yù)發(fā)環(huán)境和生產(chǎn)環(huán)境等等)中配置不同的數(shù)據(jù)源厉颤,例如穴豫,在開發(fā)時(shí)非常適合使用嵌入式數(shù)據(jù)源、在QA環(huán)境中比較適合使用DBCP的BasicDataSource走芋、在生產(chǎn)環(huán)境中則適合使用<jee:jndi-lookup>元素绩郎,即使用JNDI查詢數(shù)據(jù)源。
在Spring實(shí)戰(zhàn)3:裝配bean的進(jìn)階知識(shí)一文中我們探討過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)用可以運(yùn)行時(shí)再根據(jù)激活的profile選擇指定的數(shù)據(jù)源局服。在上述代碼中,當(dāng)development對(duì)應(yīng)的profile被激活時(shí),應(yīng)用會(huì)使用嵌入式數(shù)據(jù)源怨喘;當(dāng)qa對(duì)應(yīng)的profile被激活時(shí)津畸,應(yīng)用會(huì)使用DBCP的BasicDataSource;當(dāng)production對(duì)應(yīng)的profile被激活時(shí)必怜,應(yīng)用會(huì)使用從JNDI中獲取的數(shù)據(jù)源肉拓。
上述代碼對(duì)應(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對(duì)很多持久化技術(shù)提供了支持,包括JDBC膏执、Hibernate和Java Persistence API(API)驻售。在下一小節(jié)中,我們首先介紹如何在Spring應(yīng)用中使用JDBC書寫持久層更米。
10.3 在Spring應(yīng)用中使用JDBC
在實(shí)際開發(fā)過程中有很多持久化技術(shù)可供選擇:Hibernate欺栗、iBATIS和JPA等。盡管如此征峦,還是有很多應(yīng)用使用古老的方法即JDBC技術(shù)迟几,來訪問數(shù)據(jù)庫。
使用JDBC技術(shù)不需要開發(fā)人員學(xué)習(xí)新的框架栏笆,因?yàn)樗褪腔赟QL語言運(yùn)行的类腮。JDBC技術(shù)更加靈活,開發(fā)人員可以調(diào)整的余地很大竖伯,JDBC技術(shù)允許開發(fā)人員充分利用數(shù)據(jù)庫的本地特性存哲,而在其他ORM框架中可能做不到如此靈活和可定制。
除了上述提到的靈活性七婴、可定制能力祟偷,JDBC技術(shù)也有一些缺點(diǎn)。
10.3.1 分析JDBC代碼
開發(fā)者使用JDBC技術(shù)提供的API可以非常底層得操作數(shù)據(jù)庫打厘,同時(shí)也意味著修肠,開發(fā)者需要負(fù)責(zé)處理數(shù)據(jù)訪問過程中的各個(gè)具體步驟:管理數(shù)據(jù)庫資源和處理數(shù)據(jù)庫訪問異常。如果你使用JDBC插入數(shù)據(jù)庫户盯,在這個(gè)例子中嵌施,假設(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語句足淆,以及自己處理可能拋出的異常。
對(duì)于SQLException,開發(fā)者并不清楚具體該如何處理該異常(該異常并未指明具體的錯(cuò)誤原因)巧号,卻被迫需要捕獲該異常族奢。如果在執(zhí)行插入語句時(shí)發(fā)生錯(cuò)誤,你需要捕獲該異常丹鸿;如果在關(guān)閉statement和connection資源時(shí)發(fā)生錯(cuò)誤越走,你也需要捕獲該異常,但是捕獲后你并不能做實(shí)際的有意義的操作靠欢。
同樣廊敌,如果你需要更新一條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)的代碼秧廉。
為了補(bǔ)足JDBC體驗(yàn)之旅,我們?cè)倏纯慈绾问褂肑DBC從數(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;
}
這個(gè)函數(shù)跟之前的insert和update例子一樣啰嗦冗長:幾乎只有20%的代碼是真正有用的業(yè)務(wù)邏輯疼电,而80%的代碼則是模板樣式代碼。
可以看出减拭,使用JDBC持久化技術(shù)蔽豺,就需要編寫大量的模板樣式代碼,用于創(chuàng)建連接拧粪、創(chuàng)建statements和處理異常修陡。另外,上述提到的模板樣式代碼在數(shù)據(jù)庫訪問過程中又非常重要:釋放資源和處理異常等可霎,這都能提高數(shù)據(jù)訪問的穩(wěn)定性魄鸦。如果沒有這些操作,應(yīng)用就無法及時(shí)處理錯(cuò)誤癣朗、資源始終被占用拾因,會(huì)導(dǎo)致內(nèi)存泄露。因此旷余,開發(fā)者需要一個(gè)數(shù)據(jù)庫訪問框架绢记,用于處理這些模板樣式代碼。
10.3.2 使用Spring提供的JDBC模板
Spring提供的JDBC框架負(fù)責(zé)管理資源和異常處理正卧,從而可以簡化開發(fā)者的JDBC代碼蠢熄。開發(fā)者只需要編寫寫入和讀取數(shù)據(jù)庫相關(guān)的代碼即可。
正如在之前的小節(jié)中論述過的炉旷,Spring將數(shù)據(jù)庫訪問過程中的模板樣式代碼封裝到各個(gè)模板類中了护赊,對(duì)于JDBC惠遏,Spring提供了下列三個(gè)模板類:
- JdbcTemplate——最基本的JDBC模板砾跃,這個(gè)類提供了簡單的接口骏啰,通過JDBC和索引參數(shù)訪問數(shù)據(jù)庫;
- NameParameterJdbcTemplate——這個(gè)JDBC模板類是的開發(fā)者可以執(zhí)行綁定了指定參數(shù)名稱的SQL抽高,而不是索引參數(shù)判耕;
- SimpleJdbcTemplate——這個(gè)版本的JDBC模板利用了Java 5的一些特性,例如自動(dòng)裝箱/拆箱翘骂、接口和變參列表等壁熄,用于簡化JDBC模板的使用。
從Spring 3.1開始已經(jīng)將SimpleJdbcTemplate廢棄碳竟,它所擁有的Java 5那些特性被添加到原來的JdbcTemplate中了草丧,因此你可以直接使用JdbcTemplate;當(dāng)你希望在查詢中使用命名參數(shù)時(shí)莹桅,則可以選擇使用NamedParameterJdbcTemplate昌执。
INSERTING DATA USING JDBCTEMPLATE
要使用JdbcTemplate對(duì)象,需要為之傳遞DataSource對(duì)象诈泼。如果使用Java Config配置JdbcTemplatebean懂拾,則對(duì)應(yīng)代碼如下:
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
這里通過構(gòu)造函數(shù)將DataSource對(duì)象注入,而dataSourcebean則來自DataSourceConfiguration文件中定義的javax.sql.DataSource實(shí)例铐达。
然后就可以在自己的repository實(shí)現(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掃描機(jī)制起作用時(shí)會(huì)自動(dòng)創(chuàng)建對(duì)應(yīng)的bean。按照“面向接口編程”的原則杭抠,我們定義JdbcOperations接口對(duì)應(yīng)的實(shí)例脸甘,而JdbcTemplate實(shí)現(xiàn)了這個(gè)接口,從而使得JdbcSpitterRepository與JdbcTemplate解耦合祈争。
使用JdbcTemplate實(shí)現(xiàn)的addSpitter()方法非常簡單斤程,代碼如下:
public void addSpitter(Spitter spitter) {
jdbcOperations.update(SQL_INSERT_SPITTER,
spitter.getUsername(),
spitter.getPassword(),
spitter.getFirstName(),
spitter.getLastName());
}
可以看出,這個(gè)版本的addSpitter十分簡單菩混,不強(qiáng)制開發(fā)者寫任何管理資源和處理異常的代碼忿墅,只有插入語句和對(duì)應(yīng)的參數(shù)。
當(dāng)調(diào)用update()方法時(shí)沮峡,JdbcTemplate獲取一個(gè)連接疚脐、創(chuàng)建一個(gè)statement,并執(zhí)行插入語句邢疙。
JdbcTemplate內(nèi)部捕獲了可能拋出的SQLException異常棍弄,然后轉(zhuǎn)為更具體的數(shù)據(jù)庫訪問異常望薄,并重新拋出。由于Spring的數(shù)據(jù)庫訪問異常都是運(yùn)行時(shí)異常呼畸,開發(fā)者可以自己決定是否捕獲這些異常痕支。
READING DATA WITH JDBCTEMPLATE
使用JdbcTemplate工具從數(shù)據(jù)庫中讀取數(shù)據(jù)也非常簡單,下列代碼展示了改造過后的findOne()函數(shù):調(diào)用JdbctTemplate的queryForObject函數(shù)蛮原,用于通過ID查詢Spitter對(duì)象卧须。
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()方法包括三個(gè)參數(shù):
- SQL字符串儒陨,用于從數(shù)據(jù)庫中查詢數(shù)據(jù)花嘶;
- RowMapper對(duì)象,用于從結(jié)果集ResultSet中提取數(shù)據(jù)并構(gòu)造Spitter對(duì)象蹦漠;
- 變量列表椭员,用于指定查詢參數(shù)(這里是通過id查詢)。
這里需要注意SpitterRowMapper類笛园,它實(shí)現(xiàn)了RowMapper接口隘击,對(duì)于查詢結(jié)果,JdbcTemplate調(diào)用mapRow()方法——一個(gè)ResultSet參數(shù)和一個(gè)row number參數(shù)喘沿。mapRow()方法的主要作用是:從結(jié)果集中取出對(duì)應(yīng)屬性的值闸度,并構(gòu)造一個(gè)Spitter對(duì)象。
和addSpitter()方法相同蚜印,findOne()方法也沒有那些JDBC模板樣式代碼莺禁,只有純粹的用于查詢Spitter數(shù)據(jù)的代碼。
10.4 總結(jié)
數(shù)據(jù)就像應(yīng)用的血液窄赋,在某些以數(shù)據(jù)為中心的業(yè)務(wù)中哟冬,數(shù)據(jù)本身就是應(yīng)用。在企業(yè)級(jí)應(yīng)用開發(fā)中忆绰,編寫穩(wěn)定浩峡、簡單、性能良好的數(shù)據(jù)訪問層非常重要错敢。
JDBC是Java處理關(guān)系型數(shù)據(jù)的基本技術(shù)翰灾。原生的JDBC技術(shù)并不完美,開發(fā)者不得不寫很多模板樣式代碼稚茅,用于管理資源和處理異常纸淮。Spring提供了對(duì)應(yīng)的模板工具類,用于消除這些模板樣式代碼亚享。
后記:最近在項(xiàng)目開發(fā)中咽块,遇到一次高并發(fā)下數(shù)據(jù)庫成為性能瓶頸的情況,對(duì)數(shù)據(jù)訪問層的各個(gè)階段有了深入的了解:建立數(shù)據(jù)庫連接欺税、轉(zhuǎn)換SQL語句侈沪、執(zhí)行SQL語句揭璃、獲取執(zhí)行結(jié)果、釋放資源亭罪。我們?cè)陧?xiàng)目開發(fā)中使用的數(shù)據(jù)庫連接池是德魯伊(DruidDataSource)瘦馍,它的配置跟DBCP類似,在實(shí)際開發(fā)中皆撩,我們需要理解每個(gè)配置項(xiàng)的含義扣墩,用于性能調(diào)優(yōu)。后續(xù)我會(huì)寫一篇關(guān)于數(shù)據(jù)庫連接池的文章扛吞。
另外,我們現(xiàn)在開發(fā)中最常用的是Mybatis框架荆责,具體內(nèi)容可以參考《Java Persistence With Mybaits 3》一書滥比,也可以參考Mybatis-Spring教程(中文版)
本號(hào)專注于后端技術(shù)、JVM問題排查和優(yōu)化做院、Java面試題盲泛、個(gè)人成長和自我管理等主題,為讀者提供一線開發(fā)者的工作和成長經(jīng)驗(yàn)键耕,期待你能在這里有所收獲寺滚。