Spring實戰(zhàn)(十)-通過Spring和JDBC征服數(shù)據(jù)庫

本文基于《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ù)訪問層的合理方式隧哮。

服務對象本身并不會處理數(shù)據(jù)訪問,而是將數(shù)據(jù)訪問委托給Repository座舍。Repository接口確保其與服務對象松耦合

如圖所示沮翔,服務對象通過接口來訪問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ù)訪問功能。對于應用程序特定的任務,則會調用自定義的回調對象

如圖所示,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):

這些連接池中的大多數(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);
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市酝惧,隨后出現(xiàn)的幾起案子榴鼎,更是在濱河造成了極大的恐慌,老刑警劉巖晚唇,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巫财,死亡現(xiàn)場離奇詭異,居然都是意外死亡哩陕,警方通過查閱死者的電腦和手機平项,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悍及,“玉大人闽瓢,你說我怎么就攤上這事⌒母希” “怎么了扣讼?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缨叫。 經常有香客問我椭符,道長,這世上最難降的妖魔是什么耻姥? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任销钝,我火速辦了婚禮,結果婚禮上琐簇,老公的妹妹穿的比我還像新娘曙搬。我一直安慰自己,他們只是感情好鸽嫂,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布纵装。 她就那樣靜靜地躺著,像睡著了一般据某。 火紅的嫁衣襯著肌膚如雪橡娄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天癣籽,我揣著相機與錄音挽唉,去河邊找鬼滤祖。 笑死,一個胖子當著我的面吹牛瓶籽,可吹牛的內容都是我干的匠童。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼塑顺,長吁一口氣:“原來是場噩夢啊……” “哼汤求!你這毒婦竟也來了?” 一聲冷哼從身側響起严拒,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤扬绪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后裤唠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挤牛,經...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年种蘸,在試婚紗的時候發(fā)現(xiàn)自己被綠了墓赴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡航瞭,死狀恐怖诫硕,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情沧奴,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布长窄,位于F島的核電站滔吠,受9級特大地震影響,放射性物質發(fā)生泄漏彻舰。R本人自食惡果不足惜唱捣,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一嘱根、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冬骚,春花似錦、人聲如沸懂算。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽计技。三九已至喜德,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垮媒,已是汗流浹背舍悯。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工航棱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人萌衬。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓饮醇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親秕豫。 傳聞我的和親對象是個殘疾皇子朴艰,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容