本文基于《Spring實戰(zhàn)(第4版)》所寫协怒。
Spring的數(shù)據(jù)訪問哲學
Spittr應用需要從某種類型的數(shù)據(jù)庫讀取和寫入數(shù)據(jù)涝焙。為了避免持久化的邏輯分散到應用的各個組件中,最好將數(shù)據(jù)訪問的功能放到一個或多個專注于此項任務的組件中孕暇。這樣的組件通常稱為數(shù)據(jù)訪問對象(data access object, DAO)或Repository仑撞。
為了避免應用與特定的數(shù)據(jù)訪問策略耦合在一起,編寫良好的Repository應該以接口的方式暴露功能妖滔。下圖展現(xiàn)了設計數(shù)據(jù)訪問層的合理方式隧哮。
如圖所示沮翔,服務對象通過接口來訪問Repository。這樣做會有幾個好處曲秉。第一,它使得服務對象易于測試承二,因為它們不再與特定的數(shù)據(jù)訪問實現(xiàn)綁定在一起亥鸠。實際上妆够,你可以為這些數(shù)據(jù)訪問接口創(chuàng)建mock實現(xiàn)读虏,這樣無需連接數(shù)據(jù)庫就能測試服務對象,而且會顯著提升單元測試的效率并排除因數(shù)據(jù)不一致所造成的測試失敗盖桥。
此外灾螃,數(shù)據(jù)訪問是以持久化技術無關的方式來進行訪問腰鬼。持久化方式的選擇獨立于Repository熄赡,同時只有數(shù)據(jù)訪問相關的方式才通過接口進行暴露彼硫。這可以實現(xiàn)靈活的設計拧篮,并且切花持久化框架對應用程序其他部分所帶來的影響最小串绩。如果將數(shù)據(jù)訪問層的實現(xiàn)細節(jié)滲透到應用程序的其他部分中礁凡,那么整個應用程序將與數(shù)據(jù)訪問層耦合在一起剪芍,從而導致僵化的設計韧掩。
接口與Spring:接口是實現(xiàn)松耦合代碼的關鍵疗锐,并且應將其用于應用程序的各個層滑臊,而不僅僅是持久化層雇卷。還要說明一點关划,盡管Spring鼓勵使用接口贮折,但這并不是強制的—你可以使用Spring將bean(DAO或其他類型)直接裝配到另一個bean的某個屬性中调榄,而不需要一定通過接口注入筐带。
為了將數(shù)據(jù)訪問層與應用程序的其他部分隔離開來伦籍,Spring采取的方式之一就是提供統(tǒng)一的異常體系鸽斟,這個異常體系用在了它支持的所有持久化方案中富蓄。
了解Spring的數(shù)據(jù)訪問異常體系
JDBC中可能導致拋出SQLException的常見問題包括:
- 應用程序無法連接數(shù)據(jù)庫;
- 要執(zhí)行的查詢存在語法錯誤口注;
- 查詢中所使用的表和/或列不存在寝志;
- 試圖插入或更新的數(shù)據(jù)違反了數(shù)據(jù)庫約束;
SQLException的問題在于捕獲到它的時候該如何處理乐导。
Spring JDBC提供的數(shù)據(jù)訪問異常體系具有描述性而且又與特定的持久化框架無關物臂。不同于JDBC棵磷,Spring提供了多個數(shù)據(jù)訪問異常,分別描述了它們拋出時所對應的問題规丽。下表對比了Spring的部分數(shù)據(jù)訪問異常以及JDBC所提供的異常赌莺。
從表中可以看出艘狭,Spring為讀取和寫入數(shù)據(jù)庫的幾乎所有錯誤都提供了異常遵倦。Spring的數(shù)據(jù)訪問異常要比下表所列的還要多梧躺。
JDBC的異常 | Spring的數(shù)據(jù)訪問異常 |
---|---|
BatchUpdateException DataTruncation SQLException SQLWarning SQLWarning | BadSqlGrammarException CannotAcquireLockException CannotSerializeTransactionException CannotGetJdbcConnectionException CleanupFailureDataAccessException ConcurrencyFailureException DataAccessException DataAccessResourceFailureException DataIntegrityViolationException DataRetrievalFailureException DataSourceLookupApiUsageException DeadlockLoserDataAccessException DuplicateKeyException EmptyResultDataAccessException IncorrectResultSizeDataAccessException IncorrectUpdateSemanticsDataAccessException InvalidDataAccessApiUsageException InvalidDataAccessResourceUsageException InvalidResultSetAccessException JdbcUpdateAffectedIncorrectNumberOfRowsException LbRetrievalFailureException |
BatchUpdateException DataTruncation SQLException SQLWarning | NonTransientDataAccessResourceException OptimisticLockingFailureException PermissionDeniedDataAccessException PessimisticLockingFailureException QueryTimeoutException RecoverableDataAccessException SQLWarningException SqlXmlFeatureNotImplementedException TransientDataAccessException TransientDataAccessResourceException TypeMismatchDataAccessException UncategorizedDataAccessException UncategorizedSQLException |
盡管Spring的異常體系比JDBC簡單的SQLException豐富得多,但它并沒有與特定的持久化方式相關聯(lián)。這意味著我們可以使用Spring拋出一致的異常禁舷,而不用擔心所選擇的持久化方案牵咙。這有助于我們將所選擇持久化機制與數(shù)據(jù)訪問層隔離開來。
上表中沒有體現(xiàn)出來一點就是這些異常都繼承自DataAccessException战坤。DataAccessException的特殊之處在于它是一個非檢查型異常途茫。換句話說,沒有必要捕獲Spring所拋出的數(shù)據(jù)訪問異常(當然栅组,如果你想捕獲的話也是完全可以的)玉掸。
DataAccessException只是Spring處理檢查型異常和非檢查型異常哲學的一個范例泊业。Spring認為觸發(fā)異常的很多問題是不能在catch代碼塊中修復的吁伺。Spring使用了非檢查型異常篮奄,而不是強制開發(fā)人員編寫catch代碼塊。這把是否要捕獲異常的權力留給了開發(fā)人員矾克。
為了利用Spring的數(shù)據(jù)訪問異常胁附,我們必須使用Spring所支持的數(shù)據(jù)訪問模板。
數(shù)據(jù)訪問模板化
Spring在數(shù)據(jù)訪問中所使用的模式是模板方法模式郎哭。Spring將數(shù)據(jù)訪問過程中固定的和可變的部分明確劃分為兩個不同的類:模板(template)和回調(callback)。模板管理過程中固定的部分亥至,而回調處理自定義的數(shù)據(jù)訪問代碼。下圖展現(xiàn)了這兩個類的職責:
如圖所示,Spring的模板類處理數(shù)據(jù)訪問的固定部分—事物控制、管理資源以及處理異常。同時,應用程序相關的數(shù)據(jù)訪問—語句洲守、綁定參數(shù)以及整理結果集—在回調的實現(xiàn)中處理。你只需要關心自己的數(shù)據(jù)訪問邏輯即可。
針對不同的持久化平臺,Spring提供了多個可選的模板衡楞。如果直接使用JDBC镰惦,那可以選擇JdbcTemplate凯力。如果希望使用對象關系映射框架祈惶,那HibernateTemplate或JpaTemplate可能會更適合凡涩。下表列出了Spring所提供的所有數(shù)據(jù)訪問模板及其用途。
模板類(org.springframework.*) | 用途 |
---|---|
jca.cci.core.CciTemplate | JCA CCI連接 |
jdbc.core.JdbcTemplate | JDBC連接 |
jdbc.core.namedparam.NamedParameterJdbcTemplate | 支持命名參數(shù)的JDBC連接 |
jdbc.core.simple.SimpleJdbcTemplate | 通過Java 5簡化后的JDBC連接(Spring 3.1中已經廢棄) |
orm.hibernate3.HibernateTemplate | Hibernate 3.x以上的Session |
orm.ibatis.SqlMapClientTemplate | iBATIS SqlMap客戶端 |
orm.jdo.JdoTemplate | Java數(shù)據(jù)對象(Java Data Object)實現(xiàn) |
orm.jpa.JpaTemplate | Java持久化API的實體管理器 |
Spring為多種持久化框架提供了支持。首先要說明的是Spring所支持的大多數(shù)持久化功能都依賴于數(shù)據(jù)源。因此,在聲明模板和Repository之前误墓,我們需要在Spring中配置一個數(shù)據(jù)源用來連接數(shù)據(jù)庫。
配置數(shù)據(jù)源
Spring提供了在Spring上下文中配置數(shù)據(jù)源bean的多種方式欣范,包括:
- 通過JDBC驅動程序定義的數(shù)據(jù)源妨蛹;
- 通過JNDI查找的數(shù)據(jù)源;
- 連接池的數(shù)據(jù)源颤难。
使用JNDI數(shù)據(jù)源
Spring應用程序經常部署在Java EE應用服務器中,如WebSphere昂验、JBoss或甚至像Tomcat這樣的Web容器中。這些服務器允許你配置通過JNDI獲取數(shù)據(jù)源甫恩。這種配置的好處在于數(shù)據(jù)源完全可以在應用程序之外進行管理,這樣應用程序只需在訪問數(shù)據(jù)庫的時候查找數(shù)據(jù)源就可以了松靡。另外,在應用服務器中管理的數(shù)據(jù)源通常以池的方式組織,從而具備更好的性能笛洛,并且還支持系統(tǒng)管理員對其進行熱切換沟蔑。
利用Spring蝌诡,我們可以像使用Spring bean那樣配置JNDI中數(shù)據(jù)源的引用并將其裝配到需要的類中溉贿。位于jee命名空間下的<jee:jndi-lookup>元素可以用于檢索JNDI中的任何對象(包括數(shù)據(jù)源)并將其作為Spring的bean,如下所示:
<jee:jndi-lookup id="dataSource"
jndi-name="/jdbc/SpitterDS"
resource-ref="true" />
其中jndi-name屬性用于指定JNDI中資源的名稱浦旱。如果只設置了jndi-name屬性宇色,那么就會根據(jù)指定的名稱查找數(shù)據(jù)源。但是颁湖,如果應用程序運行在Java應用服務器中,你需要將resource-ref屬性設置為true甥捺,這樣給定的jndi-name將會自動添加“java:comp/env/”前綴抢蚀。
如果想使用Java配置的話,那我們可以借助JndiObjectFactoryBean從JNDI中查詢DataSource:
@Bean
public JndiObjectFactoryBean dataSource(){
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
jndiObjectFB.setJndiName("jdbc/SpitterDS");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
return jndiObjectFB;
}
使用數(shù)據(jù)源連接池
如果不能從JNDI中查找數(shù)據(jù)源镰禾,那么下一個選擇就是直接在Spring中配置數(shù)據(jù)源連接池皿曲。盡管Spring并沒有提供數(shù)據(jù)源連接池實現(xiàn),但是我們有多項可用方案吴侦,包括如下開源的實現(xiàn):
- Apache Commons DBCP(http://jakarta.apache.org/commons/dbcp)
- Druid (阿里巴巴的開源項目屋休,https://github.com/alibaba/druid/wiki)
- c3p0 (http://sourceforge.net/projects/c3p0/)
- BoneCP (http://jolbox.com/)
這些連接池中的大多數(shù)都能配置為Spring的數(shù)據(jù)源,在一定程度上與Spring自帶的DriverManagerDataSource或SingleConnectionDataSource很類似备韧。例如劫樟,如下就是配置
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/spitter"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="initialSize" value="5"/>
<property name="maxActive" value="10"/>
</bean>
如果喜歡Java配置的話,連接池形式的DataSource Bean可以聲明如下:
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spitter");
ds.setUsername("root");
ds.setPassword("123456");
ds.setInitialSize(5);
ds.setMaxActive(10);
return ds;
}
前四個屬性是配置DruidDataSource所必需的织堂。屬性driverClassName指定了JDBC驅動類的全限定類名叠艳。在這里我們配置的是Mysql數(shù)據(jù)庫的數(shù)據(jù)源。屬性url用于設置數(shù)據(jù)庫的JDBC URL易阳。最后附较,username和password用于在連接數(shù)據(jù)庫時進行認證。
以上四個基本屬性定義了DruidDataSource的連接信息潦俺。除此以外翅睛,還有多個配置數(shù)據(jù)源連接池的屬性,如下表所示黑竞。DruidDataSource配置兼容DBCP,但個別配置的語意有所區(qū)別疏旨。
配置 | 缺省值 | 說明 | |
---|---|---|---|
name | 配置這個屬性的意義在于很魂,如果存在多個數(shù)據(jù)源,監(jiān)控的時候可以通過名字來區(qū)分開來檐涝。如果沒有配置遏匆,將會生成一個名字法挨,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此屬性至少在1.0.5版本中是不起作用的,強行設置name會出錯幅聘。 | ||
url | 連接數(shù)據(jù)庫的url凡纳,不同數(shù)據(jù)庫不一樣。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 | ||
username | 連接數(shù)據(jù)庫的用戶名 | ||
password | 連接數(shù)據(jù)庫的密碼帝蒿。如果你不希望密碼直接寫在配置文件中荐糜,可以使用ConfigFilter。 | ||
driverClassName | 根據(jù)url自動識別 | 這一項可配可不配葛超,如果不配置druid會根據(jù)url自動識別dbType暴氏,然后選擇相應的driverClassName | |
initialSize | 0 | 初始化時建立物理連接的個數(shù)。初始化發(fā)生在顯示調用init方法绣张,或者第一次getConnection時 | |
maxActive | 8 | 最大連接池數(shù)量 | |
maxIdle | 8 | 已經不再使用答渔,配置了也沒效果 | |
minIdle | 最小連接池數(shù)量 | ||
maxWait | 獲取連接時最大等待時間,單位毫秒侥涵。配置了maxWait之后沼撕,缺省啟用公平鎖,并發(fā)效率會有所下降芜飘,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖务豺。 | ||
poolPreparedStatements | false | 是否緩存preparedStatement,也就是PSCache燃箭。PSCache對支持游標的數(shù)據(jù)庫性能提升巨大冲呢,比如說oracle。在mysql下建議關閉招狸。 | |
maxPoolPreparedStatementPerConnectionSize | -1 | 要啟用PSCache敬拓,必須配置大于0,當大于0時裙戏,poolPreparedStatements自動觸發(fā)修改為true乘凸。在Druid中,不會存在Oracle下PSCache占用內存過多的問題累榜,可以把這個數(shù)值配置大一些营勤,比如說100 | |
validationQuery | 用來檢測連接是否有效的sql,要求是一個查詢語句壹罚,常用select 'x'葛作。如果validationQuery為null,testOnBorrow猖凛、testOnReturn赂蠢、testWhileIdle都不會起作用。 | ||
validationQueryTimeout | 單位:秒辨泳,檢測連接是否有效的超時時間虱岂。底層調用jdbc Statement對象的void setQueryTimeout(int seconds)方法 | ||
testOnBorrow | true | 申請連接時執(zhí)行validationQuery檢測連接是否有效玖院,做了這個配置會降低性能。 | |
testOnReturn | false | 歸還連接時執(zhí)行validationQuery檢測連接是否有效第岖,做了這個配置會降低性能难菌。 | |
testWhileIdle | false | 建議配置為true,不影響性能蔑滓,并且保證安全性郊酒。申請連接的時候檢測,如果空閑時間大于timeBetweenEvictionRunsMillis烫饼,執(zhí)行validationQuery檢測連接是否有效猎塞。 | |
keepAlive | false(1.0.28) | 連接池中的minIdle數(shù)量以內的連接,空閑時間超過minEvictableIdleTimeMillis杠纵,則會執(zhí)行keepAlive操作荠耽。 | |
timeBetweenEvictionRunsMillis | 1分鐘(1.0.14) | 有兩個含義:1) Destroy線程會檢測連接的間隔時間,如果連接空閑時間大于等于minEvictableIdleTimeMillis則關閉物理連接比藻。2) testWhileIdle的判斷依據(jù)铝量,詳細看testWhileIdle屬性的說明 | |
numTestsPerEvictionRun | 30分鐘(1.0.14) | 不再使用,一個DruidDataSource只支持一個EvictionRun | |
minEvictableIdleTimeMillis | 連接保持空閑而不被驅逐的最小時間 | ||
connectionInitSqls | 物理連接初始化的時候執(zhí)行的sql | ||
exceptionSorter | 根據(jù)dbType自動識別 | 當數(shù)據(jù)庫拋出一些不可恢復的異常時银亲,拋棄連接 | |
filters | 屬性類型是字符串慢叨,通過別名的方式配置擴展插件,常用的插件有:監(jiān)控統(tǒng)計用的filter:stat务蝠;日志用的filter:log4j拍谐;防御sql注入的filter:wall | ||
proxyFilters | 類型是List<com.alibaba.druid.filter.Filter>,如果同時配置了filters和proxyFilters馏段,是組合關系轩拨,并非替換關系 |
在我們的示例中,連接池啟動時會創(chuàng)建5個連接院喜;當需要的時候亡蓉,允許DruidDataSource創(chuàng)建新的連接,但最大活躍連接數(shù)為10喷舀。
基于JDBC驅動的數(shù)據(jù)源
在Spring中砍濒,通過JDBC驅動定義數(shù)據(jù)源是最簡單的配置方式。Spring提供了三個這樣的數(shù)據(jù)源類(均位于org.springframework.jdbc.datasource包中)供選擇:
- DriverManagerDataSource:在每個連接請求時都會返回一個新建的連接硫麻。與DBCP的BasicDataSource不同爸邢,由DriverManagerDataSource提供的連接并沒有進行池化管理。
- SimpleDriverDataSource:
與DriverManagerDataSource的工作方式類似拿愧,但是它直接使用JDBC驅動杠河,來解決在特定環(huán)境下的類加載問題,這樣的環(huán)境包括OSGi容器; - SingleConnectionDataSource:在每個連接請求時都會返回同一個的連接感猛。盡管SingleConnectionDataSource不是嚴格意義上的連接池數(shù)據(jù)源,但是可以將其視為只有一個連接的池奢赂。
如下是配置DriverManagerDataSource的方法:
@Bean
public DataSource dataSource(){
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spitter");
ds.setUsername("root");
ds.setPassword("123456");
return ds;
}
如果使用XML的話陪白,DriverManangerDataSource可以按照如下的方式配置:
<bean id="dataSource" class="com.springframework.jdbc.datasource.DriverManangerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/spitter"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
與具備池功能的數(shù)據(jù)源相比,唯一的區(qū)別在于這些數(shù)據(jù)源bean都沒有提供連接池功能膳灶,所以沒有可配置的池相關的屬性咱士。
盡管這些數(shù)據(jù)源對于小應用或開發(fā)環(huán)境來說是不錯的,但是要將其用于生產環(huán)境轧钓,你還是需求慎重考慮序厉。因為SingleConnectionDataSource有且只有一個數(shù)據(jù)庫連接,所以不適合用于多線程的應用程序毕箍,最好只在測試時候使用弛房。而DriverManagerDataSource和SimpleDriverDataSource盡管支持多線程,但是在每次請求連接的時候都會創(chuàng)建新連接而柑,這是性能為代價的文捶。
使用嵌入式的數(shù)據(jù)源
除此之外,還有一個數(shù)據(jù)源方案:嵌入式數(shù)據(jù)庫(embedded database)媒咳。嵌入式數(shù)據(jù)庫作為應用的一部分運行粹排,而不是應用連接的獨立數(shù)據(jù)庫服務器。對于開發(fā)和測試來講涩澡,嵌入式數(shù)據(jù)庫是很好的可選方案顽耳。
Spring的jdbc命名空間能夠簡化嵌入式數(shù)據(jù)庫的配置。例如妙同,如下的程序清單展現(xiàn)了如何使用jdbc命名空間來配置嵌入式的H2數(shù)據(jù)庫射富,它會預加載一組測試數(shù)據(jù)。
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/aop/spring-jdbc-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
...
<jdbc:embedded-database type="H2">
<jdbc:script location="com/habuma/spitter/db/jdbc/schema.sql" />
<jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql" />
</jdbc:embedded-database>
...
</beans>
我們將<jdbc:embedded-database>的type屬性設置為H2渐溶,表明嵌入式數(shù)據(jù)庫應該是H2數(shù)據(jù)庫(要確保H2位于應用的類路徑下)辉浦。另外,我們還可以將type設置為DERBY茎辐,以使用嵌入式的Apache Derby數(shù)據(jù)庫宪郊。
在<jdbc:embedded-database>中,我們可以不配置也可以配置多個<jdbc:script>元素:第一個引用了schema.sql拖陆,它包含了在數(shù)據(jù)庫中創(chuàng)建表的SQL弛槐;第二個引用了test-data.sql,用來將測試數(shù)據(jù)填充到數(shù)據(jù)庫中依啰。
除了搭建嵌入式數(shù)據(jù)庫以外乎串,<jdbc:embedded-database>元素還會暴露一個數(shù)據(jù)源,我們可以像使用其他的數(shù)據(jù)源那樣來使用它速警。在這里叹誉,id屬性被設置成了dataSource鸯两,這也是所暴露數(shù)據(jù)源的bean ID。因此长豁,當我們需要javax.sql.DataSource的時候钧唐,就可以注入dataSource bean。
如果使用Java來配置嵌入式數(shù)據(jù)庫時匠襟,不會像jdbc命名空間那么簡便钝侠,我們可以使用EmbeddedDatabaseBuilder來構建DataSource:
@Bean
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
使用profile選擇數(shù)據(jù)源
實際上,我們很可能面臨這樣一種需求酸舍,那就是在某種環(huán)境下需要其中一種數(shù)據(jù)源帅韧,而在另外的環(huán)境中需要不同的數(shù)據(jù)源。
例如啃勉,對于開發(fā)期來說忽舟,<jdbc:embedded-database>元素是很適合的,而在QA環(huán)境中璧亮,你可能希望使用阿里的DruidDataSource萧诫,在生產部署環(huán)境下,可能需要使用<jee:jndi-lookup>枝嘶。
Spring的bean profile特性恰好就用在這里帘饶,所需要做的就是將每個數(shù)據(jù)源配置在不同的profile中,如下所示:
package com.myapp;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
@Bean()
@Profile("qa")
public DataSource dataSource(){
DruidDataSource source = new DruidDataSource();
source.setDriverClassName("com.mysql.jdbc.Driver");
source.setUsername("root");
source.setPassword("123456");
source.setUrl("jdbc:mysql://localhost:3306/test");
source.setInitialSize(1);
source.setMaxActive(100);
source.setMinIdle(0);
source.setMaxWait(60000);
return source;
}
}
通過使用profile功能群扶,會在運行時選擇數(shù)據(jù)源及刻,這取決于哪一個profile處于激活狀態(tài),如上面程序清單配置所示竞阐,當且僅當dev profile處于激活狀態(tài)時缴饭,會創(chuàng)建嵌入式數(shù)據(jù)庫,當且僅當qa profile處于激活狀態(tài)時骆莹,會創(chuàng)建DruidDataSource數(shù)據(jù)庫, 當且僅當prod profile處于激活狀態(tài)時颗搂,會從JNDI獲取數(shù)源。
為了內容的完整性幕垦,如下的程序清單展現(xiàn)了如何使用Spring XML代替Java配置丢氢,實現(xiàn)相同的profile配置。
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans profile="dev">
<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 profile="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
<beans profile="qa">
<bean id="dataSource" class="com.springframework.jdbc.datasource.DriverManangerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/spitter"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
</beans>
現(xiàn)在我們已經通過數(shù)據(jù)源建立了與數(shù)據(jù)庫的連接先改,接下來要實際訪問數(shù)據(jù)庫了疚察。Spring 為我們提供了多種使用數(shù)據(jù)庫的方式包括JDBC、Hibernate以及Java持久化API(Java Peristence API, JPA)仇奶。
在Spring中使用JDBC
JDBC不要求我們掌握其他框架的查詢語言貌嫡。它是建立在SQL之上的,而SQL本身就是數(shù)據(jù)訪問語言。此外岛抄,與其他的技術相比别惦,使用JDBC能夠更好地對數(shù)據(jù)訪問的性能進行調優(yōu)。JDBC允許你使用數(shù)據(jù)庫的所有特性夫椭,而這是其他框架不鼓勵甚至禁止的步咪。
應對失控的JDBC代碼
如果使用JDBC所提供的直接操作數(shù)據(jù)庫的API,你需要負責處理與數(shù)據(jù)庫訪問相關的所有事情益楼,其中包含管理數(shù)據(jù)庫資源和處理異常。例如点晴,如下所示
private static final String SQL_INSERT_SPITTER =
"insert into spitter (username, password, firstname) values (?, ?, ?)";
private DataSource dataSource;
public void addSpitter(Spitter spitter) {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection(); // 獲取連接
stmt = conn.prepareStatement(SQL_INSERT_SPITTER); // 創(chuàng)建語句
stmt.setString(1,spitter.getUsername()); // 綁定參數(shù)
stmt.setString(2,spitter.getPassword());
stmt.setString(3,spitter.getFirstName());
stmt.execute(); // 執(zhí)行語句
}catch (SQLException e){
// 處理異常
}
finally {
try {
if (stmt != null) { // 清理資源
stmt.close();
}
if (conn != null) {
conn.close();
}
}
catch (SQLException e) {
}
}
}
僅僅就是往數(shù)據(jù)庫中插入一條數(shù)據(jù)感凤,卻使用了超過20行的代碼。更新的過程與插入類似粒督。下面我們來看看如何從數(shù)據(jù)庫中獲取數(shù)據(jù)陪竿。如下所示
private static final String SQL_SELECT_SPITTER =
"select id, username, firstname 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); // 創(chuàng)建語句
stmt.setLong(1,id); // 綁定參數(shù)
rs = stmt.executeQuery(); // 執(zhí)行語句
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"));
}
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;
}
雖然大量的JDBC代碼都是用于創(chuàng)建連接和語句以及異常處理的樣板代碼,很冗余屠橄。但實際上族跛,這些樣板代碼是非常重要的。資源清理和處理錯誤確保了數(shù)據(jù)訪問的健壯性锐墙。我們不僅需要這些代碼礁哄,而且還要保證它是正確的。
使用JDBC模板
Spring的JDBC框架承擔了資源管理和異常處理的工作溪北,從而簡化了JDBC代碼桐绒,讓我們只需編寫從數(shù)據(jù)庫讀寫數(shù)據(jù)的必須代碼。
正如前面介紹的之拨,Spring將數(shù)據(jù)訪問的樣板代碼抽象到模板類之中茉继。Spring為JDBC提供了三個模板類供選擇:
- JdbcTemplate:最基本的Spring JDBC模板,這個模板支持簡單的JDBC數(shù)據(jù)庫訪問功能以及基于索引參數(shù)的查詢蚀乔;
- NameParameterJdbcTemplate:使用該模板類執(zhí)行查詢時可以將值以命名參數(shù)的形式綁定到SQL中烁竭,而不是使用簡單的索引參數(shù)。
- SimpleJdbcTemplate:該模板類利用Java 5的一些特性自動裝箱吉挣、泛型以及可變參數(shù)列表來簡化JDBC模板的使用派撕。
從Spring 3.1,SimpleJdbcTemplate已經被廢棄了听想,其Java 5的特性被轉移到了JdbcTemplate中腥刹,并且只有在需要使用命名空間參數(shù)的時候,才需要使用NameParameterJdbcTemplate汉买。對于大多數(shù)的JDBC任務來說衔峰,JdbcTemplate就是最好的可選方案。
為了讓JdbcTemplate正常功能,只需要為其設置DataSource就可以了垫卤,這使得在Spring中配置JdbcTemplate非常容易威彰,如下所示:
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
在這里DataSource是通過構造器參數(shù)注入進來的。這里所引用的dataSource bean可以是javax.sql.DataSource的任意實現(xiàn)穴肘,包括了之前文中所創(chuàng)建的歇盼。
現(xiàn)在,我們可以將jdbcTemplate裝配到Repository中并使用它來訪問數(shù)據(jù)庫评抚。例如豹缀,SpitterRepository使用了JdbcTemplate:
@Repository
public class JdbcSpitterRepository implements SpitterRepository{
private JdbcOperations jdbcOperations;
@Autowired
public JdbcSpitterRepository(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
}
...
}
在這里,JdbcSpitterRepository類上使用了@Repository注解慨代,這表明它將會在組件掃描的時候自動創(chuàng)建邢笙。它的構造器上使用了@Autowired(或使用@Inject)注解,因此在創(chuàng)建的時候侍匙,會自動獲得一個jdbcOperations對象氮惯。jdbcOperations是一個接口,定義了JdbcTemplate所實現(xiàn)的操作想暗。通過注入jdbcOperations妇汗,而不是具體的JdbcTemplate,能夠保證JdbcSpitterRepository通過jdbcOperations接口達到與JdbcTemplate保持松耦合说莫。
作為另一種組件掃描和自動裝配的方案杨箭,我們可以將JdbcSpitterRepository顯示聲明為Spring中的bean,如下所示:
@Bean
public SpitterRepository spitterRepository(JdbcTemplate jdbcTemplate){
return new JdbcSpitterRepository(jdbcTemplate);
}
在Repository中具備可用的JdbcTemplate后储狭,我們可以極大地簡化之前程序中的addSpitter() 方法告唆。如下所示:
public void addSpitter(Spitter spitter) {
jdbcOperations.update("insert into Spitter (username, password, firstname) values (?, ?, ?)",
spitter.getUsername(),
spitter.getPassword(),
spitter.getFirstName();
}
不能因為看不到樣板代碼,就意味著他們不存在晶密。樣板代碼被巧妙地隱藏到JDBC模板類中了擒悬。當update() 方法被調用的時候JdbcTemplate將會獲取連接、創(chuàng)建語句并執(zhí)行插入SQL稻艰。
在這里懂牧,也看不到對SQLException處理的代碼。在內部尊勿,JdbcTemplate將會捕獲所有可能拋出的SQLException僧凤,并將通過的SQLException轉換為Spring的哪些更明確的數(shù)據(jù)訪問異常,然后將其重新拋出元扔。因為Spring的數(shù)據(jù)訪問異常都是運行時異常躯保,所以我們不必在addSpring() 方法中進行捕獲。
JdbcTemplate也簡化了數(shù)據(jù)的讀取操作澎语。下面程序清單展現(xiàn)了新版本的findOne() 方法途事,它使用JdbcTemplate的回調验懊,實現(xiàn)根據(jù)ID查詢Spitter,并將結果集映射為Spitter對象尸变。
public Spitter findOne(long id) {
return jdbcOperations.queryForObject(
"select username, password, firstname from Spitter where id=?",
new SpitterRowMapper(), // 將查詢結果映射到對象
id);
}
...
public static final class SpitterRowMapper implements RowMapper<Spitter>{
public Spitter mapRow(ResultSet rs, int rowNum) throw SQLException {
return new Spitter (
rs.getLong("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("firstname"));
}
}
在這個findOne() 方法中使用了JdbcTemplate的queryForObject() 方法來從數(shù)據(jù)庫查詢Spitter义图。queryForObject() 方法有三個參數(shù):
- String對象,包含了要從數(shù)據(jù)庫中查找數(shù)據(jù)的SQL召烂;
- RowMapper對象碱工,用來從ResultSet中提取數(shù)據(jù)并構建域對象(本例中為Spitter);
- 可變參數(shù)列表,列出了要綁定到查詢上的索引參數(shù)值奏夫。
真正奇妙的事情發(fā)生在SpitterRowMapper對象中怕篷,它實現(xiàn)了RowMapper接口。對于查詢返回的每一行數(shù)據(jù)酗昼,JdbcTemplate將會調用RowMapper的mapRow() 方法匙头,并傳入一個ResultSet和包含行號的整數(shù)。在SpitterRowMapper的mapRow()方法中仔雷,我們創(chuàng)建了Spitter對象并將ResultSet中的值填充進去。
因為RowMapper接口只聲明了addRow() 這一個方法舔示,因此它完全符合函數(shù)式接口(functional interface)的標準碟婆。這意味著如果使用Java 8來開發(fā)應用的話,我們可以使用Lambda來表達RowMapper的實現(xiàn)惕稻。如下所示:
public Spitter findOne(long id) {
return jdbcOperations.queryForObject(
"select username, password, firstname from Spitter where id=?",
(rs, rowNum) -> {
return new Spitter(
rs.getLong("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("firstname"));
},id);
}
另外竖共,我們還可以使用Java 8的方法引用,在單獨的方法中定義映射邏輯:
public Spitter findOne(long id) {
return jdbcOperations.queryForObject(
"select username, password, firstname from Spitter where id=?",
this::mapSpitter,
id);
}
private Spitter mapSpitter(ResultSet rs, int row) throws SQLException {
return new Spitter (
rs.getLong("id"),
rs.getString("username"),
rs.getString("password"),
rs.getString("firstname"));
}
不管采用哪種方式俺祠,我們都不必顯示實現(xiàn)RowMapper接口公给,但是與實現(xiàn)RowMapper類似,我們所提供的Lambda表達式和方法必須要接口相同的參數(shù)蜘渣,并返回相同的類型淌铐。
在之前的代碼中,addSpitter() 方法使用了索引參數(shù)蔫缸。這意味著我們需要留意查詢中參數(shù)的順序腿准,在將值傳遞給update()方法的時候要保持正確的順序。如果在修改SQL時更改了參數(shù)的順序拾碌,那我們還需要修改參數(shù)值的順序吐葱。
除了這種方法之外,我們還可以使用命名參數(shù)校翔。命名參數(shù)可以賦予SQL中的每個參數(shù)一個明確的名字弟跑,在綁定值在查詢語句的時候就通過該名字來引用參數(shù)。例如防症,假設SQL_INSERT_SPITTER查詢語句是這樣定義的:
private static final String SQL_INSERT_SPITTER =
"insert into spitter (username, password, firstname) " +
"values (:username, :password, :firstname)";
使用命名參數(shù)查詢孟辑,綁定值的順序就不重要了哎甲,我們可以按照名字來綁定值。如果查詢語句發(fā)生了變化導致參數(shù)的順序與之前不一致扑浸,我們不需要修改綁定的代碼烧给。
NamedParameterJdbcTemplate是一個特殊的JDBC模板類,它支持使用命名參數(shù)喝噪。在Spring中础嫡,NamedParameterJdbcTemplate的聲明方式與常規(guī)的JdbcTemplate幾乎完全相同:
@Bean
public NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
addSpitter() 方法如下所示:
private static final String SQL_INSERT_SPITTER =
"insert into spitter (username, password, firstname) " +
"values (:username, :password, :firstname)";
public void addSpitter(Spitter spitter) {
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("username", spitter.getUsername());
paramMap.put("password", spitter.getPassword());
paramMap.put("firstname", spitter.getFirstName());
jdbcOperations.update(SQL_INSERT_SPITTER, paramMap);
}