Spring的JDBCTemplate

Spring的JDBCTemplate

當hql等查詢方式不能滿足性能或靈活性的要求,必須使用SQL時,大家有三種選擇:

第一、使用Hibernate的sql查詢函數,將查詢結果對象轉爲Entity對象。

第二、使用HibernateSession的getConnection獲得JDBCConnection,然後進行純JDBCAPI操作;

第三、選擇把Spring的JDBCTemplate作爲一種很不錯的JDBCUtils來使用。

JDBCTemplate的使用很簡單,只要在ApplicationContext文件裏定義一個jdbcTemplate節點,POJO獲得注入後可以直接執行操作,不需要繼承什麼基類,詳見JDBCTemplate參考文檔linkext7.gif

AplicationContext定義:

<beanid="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate">
<propertyname="dataSource"ref="dataSource"/>
</bean>

實際使用:

SqlRowSet rs =jdbcTemplate.queryForRowSet(sql, params);

Tips1jdbcTemplate有很多的ORM化回調操作將返回結果轉爲對象列表,但很多時候還是需要返回ResultSet,Spring有提供一個類似ResultSet的SpringSqlRowSet對象。


Tips2.注意jdbcTemplate儘量只執行查詢操作,莫要進行更新,否則很容易破壞Hibernate的二級緩存體系。


Chapter11.使用JDBC進行數據訪問

11.1.簡介

SpringJDBC抽象框架所帶來的價值將在以下幾個方面得以體現:(注:使用了SpringJDBC抽象框架之後,應用開發人員只需要完成斜體字部分的編碼工作。)

1.指定數據庫連接參數

2.打開數據庫連接

3.聲明SQL語句

4.預編譯並執行SQL語句

5.遍歷查詢結果(如果需要的話)

6.處理每一次遍歷操作

7.處理拋出的任何異常

8.處理事務

9.關閉數據庫連接

Spring將替我們完成所有單調乏味的JDBC底層細節處理工作。

11.1.1.SpringJDBC包結構

SpringJDBC抽象框架由四個包構成:coredataSourceobject以及support

org.springframework.jdbc.core包由JdbcTemplate類以及相關的回調接口(callbackinterface)和類組成。

org.springframework.jdbc.datasource包由一些用來簡化DataSource訪問的工具類,以及各種DataSource接口的簡單實現(主要用於單元測試以及在J2EE容器之外使用JDBC)組成。工具類提供了一些靜態方法,諸如通過JNDI獲取數據連接以及在必要的情況下關閉這些連接。它支持綁定線程的連接,比如被用於DataSourceTransactionManager的連接。

接下來,org.springframework.jdbc.object包由封裝了查詢、更新以及存儲過程的類組成,這些類的對象都是線程安全並且可重複使用的。它們類似於JDO,與JDO的不同之處在於查詢結果與數據庫是“斷開連接”的。它們是在org.springframework.jdbc.core包的基礎上對JDBC更高層次的抽象。

最後,org.springframework.jdbc.support包提供了一些SQLException的轉換類以及相關的工具類。

在JDBC處理過程中拋出的異常將被轉換成org.springframework.dao包中定義的異常。因此使用SpringJDBC進行開發將不需要處理JDBC或者特定的RDBMS纔會拋出的異常。所有的異常都是uncheckedexception,這樣我們就可以對傳遞到調用者的異常進行有選擇的捕獲。

11.2.利用JDBC核心類實現JDBC的基本操作和錯誤處理

11.2.1.JdbcTemplate

JdbcTemplate是core包的核心類。它替我們完成了資源的創建以及釋放工作,從而簡化了我們對JDBC的使用。它還可以幫助我們避免一些常見的錯誤,比如忘記關閉數據庫連接。JdbcTemplate將完成JDBC核心處理流程,比如SQL語句的創建、執行,而把SQL語句的生成以及查詢結果的提取工作留給我們的應用代碼。它可以完成SQL查詢、更新以及調用存儲過程,可以對ResultSet進行遍歷並加以提取。它還可以捕獲JDBC異常並將其轉換成org.springframework.dao包中定義的,通用的,信息更豐富的異常。

使用JdbcTemplate進行編碼只需要根據明確定義的一組契約來實現回調接口。PreparedStatementCreator回調接口通過給定的Connection創建一個PreparedStatement,包含SQL和任何相關的參數。CallableStatementCreateor實現同樣的處理,只不過它創建的是CallableStatement。RowCallbackHandler接口則從數據集的每一行中提取值。

我們可以在一個service實現類中通過傳遞一個DataSource引用來完成JdbcTemplate的實例化,也可以在applicationcontext中配置一個JdbcTemplatebean,來供service使用。需要注意的是DataSource在applicationcontext總是配製成一個bean,第一種情況下,DataSourcebean將傳遞給service,第二種情況下DataSourcebean傳遞給JdbcTemplatebean。因爲JdbcTemplate使用回調接口和SQLExceptionTranslator接口作爲參數,所以一般情況下沒有必要通過繼承JdbcTemplate來定義其子類。

JdbcTemplate中使用的所有SQL將會以“DEBUG”級別記入日誌(一般情況下日誌的category是JdbcTemplate相應的全限定類名,不過如果需要對JdbcTemplate進行定製的話,可能是它的子類名)。

11.2.2.NamedParameterJdbcTemplate

NamedParameterJdbcTemplate類增加了在SQL語句中使用命名參數的支持。在此之前,在傳統的SQL語句中,參數都是用'?'佔位符來表示的。NamedParameterJdbcTemplate類內部封裝了一個普通的JdbcTemplate,並作爲其代理來完成大部分工作。下面的內容主要針對NamedParameterJdbcTemplateJdbcTemplate的不同之處來加以說明,即如何在SQL語句中使用命名參數。

通過下面的例子我們可以更好地瞭解NamedParameterJdbcTemplate的使用模式(在後面我們還有更好的使用方式)。

//someJDBC-backedDAOclass...

public int countOfActorsByFirstName(String firstName) {

String sql = "select count(0) from T_ACTOR where first_name =:first_name";

NamedParameterJdbcTemplate template = newNamedParameterJdbcTemplate(this.getDataSource());

SqlParameterSource namedParameters = newMapSqlParameterSource("first_name", firstName);

return template.queryForInt(sql, namedParameters);

}

在上面例子中,sql變量使用了命名參數佔位符“first_name”,與其對應的值存在namedParameters變量中(類型爲MapSqlParameterSource)。

如果你喜歡的話,也可以使用基於Map風格的名值對將命名參數傳遞給NamedParameterJdbcTemplateNamedParameterJdbcTemplate實現了NamedParameterJdbcOperations接口,剩下的工作將由調用該接口的相應方法來完成,這裏我們就不再贅述):

//someJDBC-backedDAOclass...

public int countOfActorsByFirstName(String firstName) {

String sql = "select count(0) from T_ACTOR where first_name =:first_name";

NamedParameterJdbcTemplate template = newNamedParameterJdbcTemplate(this.getDataSource());

Map namedParameters = new HashMap();

namedParameters.put("first_name", firstName);

return template.queryForInt(sql, namedParameters);

}

另外一個值得一提的特性是與NamedParameterJdbcTemplate位於同一個包中的SqlParameterSource接口。在前面的代碼片斷中我們已經看到了該接口的實現(即MapSqlParameterSource類),SqlParameterSource可以用來作爲NamedParameterJdbcTemplate命名參數的來源。MapSqlParameterSource類是一個非常簡單的實現,它僅僅是一個java.util.Map適配器,當然其用法也就不言自明瞭(如果還有不明瞭的,可以在Spring的JIRA系統中要求提供更多的相關資料)。

SqlParameterSource接口的另一個實現--BeanPropertySqlParameterSource爲我們提供了更有趣的功能。該類包裝一個類似JavaBean的對象,所需要的命名參數值將由包裝對象提供,下面我們使用一個例子來更清楚地說明它的用法。

//someJavaBean-likeclass...

public class Actor {

private Long id;

private String firstName;

private String lastName;

public String getFirstName() {

return this.firstName;

}

public String getLastName() {

return this.lastName;

}

public Long getId() {

return this.id;

}

//settersomitted...

}

//someJDBC-backedDAOclass...

public int countOfActors(Actor exampleActor) {

//noticehowthenamedparametersmatchthepropertiesoftheabove'Actor'class

String sql = "select count(0) from T_ACTOR where first_name =:firstName and last_name = :lastName";

NamedParameterJdbcTemplate template = newNamedParameterJdbcTemplate(this.getDataSource());

SqlParameterSource namedParameters = newBeanPropertySqlParameterSource(exampleActor);

return template.queryForInt(sql, namedParameters);

}

大家必須牢記一點:NamedParameterJdbcTemplate類內部包裝了一個標準的JdbcTemplate類。如果你需要訪問其內部的JdbcTemplate實例(比如訪問JdbcTemplate的一些方法)那麼你需要使用getJdbcOperations()方法返回的JdbcOperations接口。(JdbcTemplate實現了JdbcOperations接口)。

NamedParameterJdbcTemplate類是線程安全的,該類的最佳使用方式不是每次操作的時候實例化一個新的NamedParameterJdbcTemplate,而是針對每個DataSource只配置一個NamedParameterJdbcTemplate實例(比如在SpringIoC容器中使用SpringIoC來進行配置),然後在那些使用該類的DAO中共享該實例。

11.2.3.SimpleJdbcTemplate


Note


請注意該類所提供的功能僅適用於Java5(Tiger)。

SimpleJdbcTemplate類是JdbcTemplate類的一個包裝器(wrapper),它利用了Java5的一些語言特性,比如Varargs和Autoboxing。對那些用慣了Java5的程序員,這些新的語言特性還是很好用的。

SimpleJdbcTemplate類利用Java5的語法特性帶來的好處可以通過一個例子來說明。在下面的代碼片斷中我們首先使用標準的JdbcTemplate進行數據訪問,接下來使用SimpleJdbcTemplate做同樣的事情。

//classicJdbcTemplate-style...

public Actor findActor(long id) {

String sql = "select id, first_name, last_name from T_ACTOR where id =?";

RowMapper mapper = new RowMapper() {

public Object mapRow(ResultSet rs, int rowNum) throws SQLException {

Actor actor = new Actor();

actor.setId(rs.getLong(Long.valueOf(rs.getLong("id"))));

actor.setFirstName(rs.getString("first_name"));

actor.setLastName(rs.getString("last_name"));

return actor;

}

};

//normallythiswouldbedependencyinjectedofcourse...

JdbcTemplate jdbcTemplate = new JdbcTemplate(this.getDataSource());

//noticethecast,andthewrappingupofthe'id'argument//inanarray,andtheboxingofthe'id'argumentasareferencetype

return (Actor) jdbcTemplate.queryForObject(sql, mapper, new Object[]{Long.valueOf(id)});

}

下面是同一方法的另一種實現,惟一不同之處是我們使用了SimpleJdbcTemplate,這樣代碼顯得更加清晰。

//SimpleJdbcTemplate-style...

public Actor findActor(long id) {

String sql = "select id, first_name, last_name from T_ACTOR where id =?";

ParameterizedRowMapper<Actor> mapper = newParameterizedRowMapper<Actor>() {

//noticethereturntypewithrespecttoJava5covariantreturntypes

public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {

Actor actor = new Actor();

actor.setId(rs.getLong("id"));

actor.setFirstName(rs.getString("first_name"));

actor.setLastName(rs.getString("last_name"));

return actor;

}

};

//again,normallythiswouldbedependencyinjectedofcourse...

SimpleJdbcTemplate simpleJdbcTemplate = newSimpleJdbcTemplate(this.getDataSource());

return simpleJdbcTemplate.queryForObject(sql, mapper, id);

}

11.2.4.DataSource接口

爲了從數據庫中取得數據,我們首先需要獲取一個數據庫連接。Spring通過DataSource對象來完成這個工作。DataSource是JDBC規範的一部分,它被視爲一個通用的數據庫連接工廠。通過使用DataSource,Container或Framework可以將連接池以及事務管理的細節從應用代碼中分離出來。作爲一個開發人員,在開發和測試產品的過程中,你可能需要知道連接數據庫的細節。但在產品實施時,你不需要知道這些細節。通常數據庫管理員會幫你設置好數據源。

在使用SpringJDBC時,你既可以通過JNDI獲得數據源,也可以自行配置數據源(使用Spring提供的DataSource實現類)。使用後者可以更方便的脫離Web容器來進行單元測試。這裏我們將使用DriverManagerDataSource,不過DataSource有多種實現,後面我們會講到。使用DriverManagerDataSource和你以前獲取一個JDBC連接的做法沒什麼兩樣。你首先必須指定JDBC驅動程序的全限定名,這樣DriverManager才能加載JDBC驅動類,接着你必須提供一個url(因JDBC驅動而異,爲了保證設置正確請參考相關JDBC驅動的文檔),最後你必須提供一個用戶連接數據庫的用戶名和密碼。下面我們將通過一個例子來說明如何配置一個DriverManagerDataSource

DriverManagerDataSource dataSource = new DriverManagerDataSource();

dataSource.setDriverClassName("org.hsqldb.jdbcDriver");

dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");

dataSource.setUsername("sa");

dataSource.setPassword("");

11.2.5.SQLExceptionTranslator接口

SQLExceptionTranslator是一個接口,如果你需要在SQLExceptionorg.springframework.dao.DataAccessException之間作轉換,那麼必須實現該接口。

轉換器類的實現可以採用一般通用的做法(比如使用JDBC的SQLStatecode),如果爲了使轉換更準確,也可以進行定製(比如使用Oracle的errorcode)。

SQLErrorCodeSQLExceptionTranslator是SQLExceptionTranslator的默認實現。該實現使用指定數據庫廠商的errorcode,比採用SQLState更精確。轉換過程基於一個JavaBean(類型爲SQLErrorCodes)中的errorcode。這個JavaBean由SQLErrorCodesFactory工廠類創建,其中的內容來自於"sql-error-codes.xml"配置文件。該文件中的數據庫廠商代碼基於DatabaseMetaData信息中的DatabaseProductName,從而配合當前數據庫的使用。


SQLErrorCodeSQLExceptionTranslator使用以下的匹配規則:


·首先檢查是否存在完成定製轉換的子類實現。通常SQLErrorCodeSQLExceptionTranslator這個類可以作爲一個具體類使用,不需要進行定製,那麼這個規則將不適用。

·接着將SQLException的errorcode與錯誤代碼集中的errorcode進行匹配。默認情況下錯誤代碼集將從SQLErrorCodesFactory取得。錯誤代碼集來自classpath下的sql-error-codes.xml文件,它們將與數據庫metadata信息中的databasename進行映射。

·如果仍然無法匹配,最後將調用fallbackTranslator屬性的translate方法,SQLStateSQLExceptionTranslator類實例是默認的fallbackTranslator。


SQLErrorCodeSQLExceptionTranslator可以採用下面的方式進行擴展:

public class MySQLErrorCodesTranslator extendsSQLErrorCodeSQLExceptionTranslator {

protected DataAccessException customTranslate(String task, String sql,SQLException sqlex) {

if (sqlex.getErrorCode() == -12345) {

return new DeadlockLoserDataAccessException(task, sqlex);

}

return null;

}

}

在上面的這個例子中,errorcode爲'-12345'的SQLException將採用該轉換器進行轉換,而其他的errorcode將由默認的轉換器進行轉換。爲了使用該轉換器,必須將其作爲參數傳遞給JdbcTemplate類的setExceptionTranslator方法,並在需要使用這個轉換器器的數據存取操作中使用該JdbcTemplate。下面的例子演示瞭如何使用該定製轉換器:

// create a JdbcTemplate and set data source

JdbcTemplate jt = new JdbcTemplate();

jt.setDataSource(dataSource);

// create a custom translator and set the DataSource for the defaulttranslation lookup

MySQLErrorCodesTransalator tr = new MySQLErrorCodesTransalator();

tr.setDataSource(dataSource);

jt.setExceptionTranslator(tr);

// use the JdbcTemplate for this SqlUpdate

SqlUpdate su = new SqlUpdate();

su.setJdbcTemplate(jt);

su.setSql("update orders set shipping_charge = shipping_charge *1.05");

su.compile();

su.update();

在上面的定製轉換器中,我們給它注入了一個數據源,因爲我們仍然需要使用默認的轉換器從sql-error-codes.xml中獲取錯誤代碼集。

11.2.6.執行SQL語句

我們僅需要非常少的代碼就可以達到執行SQL語句的目的,一旦獲得一個DataSource和一個JdbcTemplate,我們就可以使用JdbcTemplate提供的豐富功能實現我們的操作。下面的例子使用了極少的代碼完成創建一張表的工作。

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAStatement {

private JdbcTemplate jt;

private DataSource dataSource;

public void doExecute() {

jt = new JdbcTemplate(dataSource);

jt.execute("create table mytable (id integer, namevarchar(100))");

}

public void setDataSource(DataSource dataSource) {

this.dataSource = dataSource;

}

}

11.2.7.執行查詢

除了execute方法之外,JdbcTemplate還提供了大量的查詢方法。在這些查詢方法中,有很大一部分是用來查詢單值的。比如返回一個彙總(count)結果或者從返回行結果中取得指定列的值。這時我們可以使用queryForInt(..)queryForLong(..)或者queryForObject(..)方法。queryForObject方法用來將返回的JDBC類型對象轉換成指定的Java對象,如果類型轉換失敗將拋出InvalidDataAccessApiUsageException異常。下面的例子演示了兩個查詢的用法,一個返回int值,另一個返回String

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {

private JdbcTemplate jt;

private DataSource dataSource;

public int getCount() {

jt = new JdbcTemplate(dataSource);

int count = jt.queryForInt("select count(*) from mytable");

return count;

}

public String getName() {

jt = new JdbcTemplate(dataSource);

String name = (String) jt.queryForObject("select name frommytable", String.class);

return name;

}

public void setDataSource(DataSource dataSource) {

this.dataSource = dataSource;

}

}

除了返回單值的查詢方法,JdbcTemplate還提供了一組返回List結果的方法。List中的每一項對應查詢返回結果中的一行。其中最簡單的是queryForList方法,該方法將返回一個List,該List中的每一條記錄是一個Map對象,對應應數據庫中某一行;而該Map中的每一項對應該數據庫行中的某一列值。下面的代碼片斷接着上面的例子演示瞭如何用該方法返回表中所有記錄:

public List getList() {

jt = new JdbcTemplate(dataSource);

List rows = jt.queryForList("select * from mytable");

return rows;

}

返回的結果集類似下面這種形式:

[{name=Bob, id=1}, {name=Mary, id=2}]

11.2.8.更新數據庫

JdbcTemplate還提供了一些更新數據庫的方法。在下面的例子中,我們根據給定的主鍵值對指定的列進行更新。例子中的SQL語句中使用了“?”佔位符來接受參數(這種做法在更新和查詢SQL語句中很常見)。傳遞的參數值位於一個對象數組中(基本類型需要被包裝成其對應的對象類型)。

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAnUpdate {

private JdbcTemplate jt;

private DataSource dataSource;

public void setName(int id, String name) {

jt = new JdbcTemplate(dataSource);

jt.update("update mytable set name = ? where id = ?", newObject[] {name, new Integer(id)});

}

public void setDataSource(DataSource dataSource) {

this.dataSource = dataSource;

}

}

11.3.控制數據庫連接

11.3.1.DataSourceUtils

DataSourceUtils作爲一個幫助類提供易用且強大的數據庫訪問能力,我們可以使用該類提供的靜態方法從JNDI獲取數據庫連接以及在必要的時候關閉之。它提供支持線程綁定的數據庫連接(比如使用DataSourceTransactionManager的時候,將把數據庫連接綁定到當前的線程上)。

注:getDataSourceFromJndi(..)方法主要用於那些沒有使用beanfactory或者applicationcontext的場合。如果使用applicationcontext,那麼最好是在JndiObjectFactoryBean中配置bean或者直接使用JdbcTemplate實例。JndiObjectFactoryBean能夠通過JNDI獲取DataSource並將DataSource作爲引用參數傳遞給其他bean。這樣,在不同的DataSource之間切換只需要修改配置文件即可,甚至我們可以用一個非JNDI的DataSource來替換FactoryBean定義!

11.3.2.SmartDataSource接口

SmartDataSourceDataSource接口的一個擴展,用來提供數據庫連接。使用該接口的類在指定的操作之後可以檢查是否需要關閉連接。該接口在某些情況下非常有用,比如有些情況需要重用數據庫連接。

11.3.3.AbstractDataSource

AbstractDataSource是一個實現了DataSource接口的abstract基類。它實現了DataSource接口的一些無關痛癢的方法,如果你需要實現自己的DataSource,那麼繼承該類是個好主意。

11.3.4.SingleConnectionDataSource

SingleConnectionDataSourceSmartDataSource接口的一個實現,其內部包裝了一個單連接。該連接在使用之後將不會關閉,很顯然它不能在多線程的環境下使用。

當客戶端代碼調用close方法的時候,如果它總是假設數據庫連接來自連接池(就像使用持久化工具時一樣),你應該將suppressClose設置爲true。這樣,通過該類獲取的將是代理連接(禁止關閉)而不是原有的物理連接。需要注意的是,我們不能把使用該類獲取的數據庫連接造型(cast)爲OracleConnection之類的本地數據庫連接。

SingleConnectionDataSource主要在測試的時候使用。它使得測試代碼很容易脫離應用服務器而在一個簡單的JNDI環境下運行。與DriverManagerDataSource不同的是,它始終只會使用同一個數據庫連接,從而避免每次建立物理連接的開銷。

11.3.5.DriverManagerDataSource

DriverManagerDataSource類實現了SmartDataSource接口。在applicationContext.xml中可以使用beanproperties來設置JDBCDriver屬性,該類每次返回的都是一個新的連接。

該類主要在測試以及脫離J2EE容器的獨立環境中使用。它既可以用來在applicationcontext中作爲一個DataSourcebean,也可以在簡單的JNDI環境下使用。由於Connection.close()僅僅只是簡單的關閉數據庫連接,因此任何能夠獲取DataSource的持久化代碼都能很好的工作。不過使用JavaBean風格的連接池(比如commons-dbcp)也並非難事。即使是在測試環境下,使用連接池也是一種比使用DriverManagerDataSource更好的做法。

11.3.6.TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy作爲目標DataSource的一個代理,在對目標DataSource包裝的同時,還增加了Spring的事務管理能力,在這一點上,這個類的功能非常像J2EE服務器所提供的事務化的JNDIDataSource


Note


該類幾乎很少被用到,除非現有代碼在被調用的時候需要一個標準的JDBCDataSource接口實現作爲參數。這種情況下,這個類可以使現有代碼參與Spring的事務管理。通常最好的做法是使用更高層的抽象來對數據源進行管理,比如JdbcTemplate和DataSourceUtils等等。

如果需要更詳細的資料,請參考TransactionAwareDataSourceProxyJavaDoc

11.3.7.DataSourceTransactionManager

DataSourceTransactionManager類是PlatformTransactionManager接口的一個實現,用於處理單JDBC數據源。它將從指定DataSource取得的JDBC連接綁定到當前線程,因此它也支持了每個數據源對應到一個線程。

我們推薦在應用代碼中使用DataSourceUtils.getConnection(DataSource)來獲取JDBC連接,而不是使用J2EE標準的DataSource.getConnection。因爲前者將拋出unchecked的org.springframework.dao異常,而不是checked的SQLException異常。SpringFramework中所有的類(比如JdbcTemplate)都採用這種做法。如果不需要和這個DataSourceTransactionManager類一起使用,DataSourceUtils提供的功能跟一般的數據庫連接策略沒有什麼兩樣,因此它可以在任何場景下使用。

DataSourceTransactionManager類支持定製隔離級別,以及對SQL語句查詢超時的設定。爲了支持後者,應用代碼必須使用JdbcTemplate或者在每次創建SQL語句時調用DataSourceUtils.applyTransactionTimeout方法。

在使用單個數據源的情形下,你可以用DataSourceTransactionManager來替代JtaTransactionManager,因爲DataSourceTransactionManager不需要容器支持JTA。如果你使用DataSourceUtils.getConnection(DataSource)來獲取JDBC連接,二者之間的切換只需要更改一些配置。最後需要注意的一點就是JtaTransactionManager不支持隔離級別的定製!

11.4.用Java對象來表達JDBC操作

org.springframework.jdbc.object包下的類允許用戶以更加面向對象的方式去訪問數據庫。比如說,用戶可以執行查詢並返回一個list,該list作爲一個結果集將把從數據庫中取出的列數據映射到業務對象的屬性上。用戶也可以執行存儲過程,以及運行更新、刪除以及插入SQL語句。


Note


在許多Spring開發人員中間存在有一種觀點,那就是下面將要提到的各種RDBMS操作類(StoredProcedure類除外)通常也可以直接使用JdbcTemplate相關的方法來替換。相對於把一個查詢操作封裝成一個類而言,直接調用JdbcTemplate方法將更簡單而且更容易理解。

必須說明的一點就是,這僅僅只是一種觀點而已,如果你認爲你可以從直接使用RDBMS操作類中獲取一些額外的好處,你不妨根據自己的需要和喜好進行不同的選擇。

11.4.1.SqlQuery

SqlQuery是一個可重用、線程安全的類,它封裝了一個SQL查詢。其子類必須實現newResultReader()方法,該方法用來在遍歷ResultSet的時候能使用一個類來保存結果。我們很少需要直接使用SqlQuery,因爲其子類MappingSqlQuery作爲一個更加易用的實現能夠將結果集中的行映射爲Java對象。SqlQuery還有另外兩個擴展分別是MappingSqlQueryWithParametersUpdatableSqlQuery

11.4.2.MappingSqlQuery

MappingSqlQuery是一個可重用的查詢抽象類,其具體類必須實現mapRow(ResultSet,int)抽象方法來將結果集中的每一行轉換成Java對象。

SqlQuery的各種實現中,MappingSqlQuery是最常用也是最容易使用的一個。

下面這個例子演示了一個定製查詢,它將從客戶表中取得的數據映射到一個Customer類實例。

private class CustomerMappingQuery extends MappingSqlQuery {

public CustomerMappingQuery(DataSource ds) {

super(ds, "SELECT id, name FROM customer WHERE id = ?");

super.declareParameter(new SqlParameter("id", Types.INTEGER));

compile();

}

public Object mapRow(ResultSet rs, int rowNumber) throws SQLException {

Customer cust = new Customer();

cust.setId((Integer) rs.getObject("id"));

cust.setName(rs.getString("name"));

return cust;

}

}

在上面的例子中,我們爲用戶查詢提供了一個構造函數併爲構造函數傳遞了一個DataSource參數。在構造函數裏面我們把DataSource和一個用來返回查詢結果的SQL語句作爲參數調用父類的構造函數。SQL語句將被用於生成一個PreparedStatement對象,因此它可以包含佔位符來傳遞參數。而每一個SQL語句的參數必須通過調用declareParameter方法來進行聲明,該方法需要一個SqlParameter(封裝了一個字段名字和一個java.sql.Types中定義的JDBC類型)對象作爲參數。所有參數定義完之後,我們調用compile()方法來對SQL語句進行預編譯。

下面讓我們看看該定製查詢初始化並執行的代碼:

public Customer getCustomer(Integer id) {

CustomerMappingQuery custQry = new CustomerMappingQuery(dataSource);

Object[] parms = new Object[1];

parms[0] = id;

List customers = custQry.execute(parms);

if (customers.size() > 0) {

return (Customer) customers.get(0);

}

else {

return null;

}

}

在上面的例子中,getCustomer方法通過傳遞惟一參數id來返回一個客戶對象。該方法內部在創建CustomerMappingQuery實例之後,我們創建了一個對象數組用來包含要傳遞的查詢參數。這裏我們只有唯一的一個Integer參數。執行CustomerMappingQuery的execute方法之後,我們得到了一個List,該List中包含一個Customer對象,如果有對象滿足查詢條件的話。

11.4.3.SqlUpdate

SqlUpdate類封裝了一個可重複使用的SQL更新操作。跟所有RdbmsOperation類一樣,SqlUpdate可以在SQL中定義參數。

該類提供了一系列update()方法,就像SqlQuery提供的一系列execute()方法一樣。

SqlUpdate是一個具體的類。通過在SQL語句中定義參數,這個類可以支持不同的更新方法,我們一般不需要通過繼承來實現定製。

import java.sql.Types;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlParameter;

import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

public UpdateCreditRating(DataSource ds) {

setDataSource(ds);

setSql("update customer set credit_rating = ? where id = ?");

declareParameter(new SqlParameter(Types.NUMERIC));

declareParameter(new SqlParameter(Types.NUMERIC));

compile();

}

/**

* @param id for the Customer to be updated

* @param rating the new value for credit rating

* @return number of rows updated

*/

public int run(int id, int rating) {

Object[] params =

new Object[] {

new Integer(rating),

new Integer(id)};

return update(params);

}

}

11.4.4.StoredProcedure

StoredProcedure類是一個抽象基類,它是對RDBMS存儲過程的一種抽象。該類提供了多種execute(..)方法,不過這些方法的訪問類型都是protected的。

從父類繼承的sql屬性用來指定RDBMS存儲過程的名字。儘管該類提供了許多必須在JDBC3.0下使用的功能,但是我們更關注的是JDBC3.0中引入的命名參數特性。

下面的程序演示瞭如何調用Oracle中的sysdate()函數。這裏我們創建了一個繼承StoredProcedure的子類,雖然它沒有輸入參數,但是我必須通過使用SqlOutParameter來聲明一個日期類型的輸出參數。execute()方法將返回一個map,map中的每個entry是一個用參數名作key,以輸出參數爲value的名值對。

import java.sql.Types;

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlOutParameter;

import org.springframework.jdbc.datasource.*;

import org.springframework.jdbc.object.StoredProcedure;

public class TestStoredProcedure {

public static void main(String[] args) {

TestStoredProcedure t = new TestStoredProcedure();

t.test();

System.out.println("Done!");

}

void test() {

DriverManagerDataSource ds = new DriverManagerDataSource();

ds.setDriverClassName("oracle.jdbc.OracleDriver");

ds.setUrl("jdbc:oracle:thin:@localhost:1521:mydb");

ds.setUsername("scott");

ds.setPassword("tiger");

MyStoredProcedure sproc = new MyStoredProcedure(ds);

Map results = sproc.execute();

printMap(results);

}

private class MyStoredProcedure extends StoredProcedure {

private static final String SQL = "sysdate";

public MyStoredProcedure(DataSource ds) {

setDataSource(ds);

setFunction(true);

setSql(SQL);

declareParameter(new SqlOutParameter("date", Types.DATE));

compile();

}

public Map execute() {

//the'sysdate'sprochasnoinputparameters,soanemptyMapissupplied...

return execute(new HashMap());

}

}

private static void printMap(Map results) {

for (Iterator it = results.entrySet().iterator(); it.hasNext(); ) {

System.out.println(it.next());

}

}

}

下面是StoredProcedure的另一個例子,它使用了兩個Oracle遊標類型的輸出參數。

import oracle.jdbc.driver.OracleTypes;

import org.springframework.jdbc.core.SqlOutParameter;

import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;

import java.util.HashMap;

import java.util.Map;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

private static final String SPROC_NAME = "AllTitlesAndGenres";

public TitlesAndGenresStoredProcedure(DataSource dataSource) {

super(dataSource, SPROC_NAME);

declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR,new TitleMapper()));

declareParameter(new SqlOutParameter("genres",OracleTypes.CURSOR, new GenreMapper()));

compile();

}

public Map execute() {

//again,thissprochasnoinputparameters,soanemptyMapissupplied...

return super.execute(new HashMap());

}

}

值得注意的是TitlesAndGenresStoredProcedure構造函數中declareParameter(..)SqlOutParameter參數,該參數使用了RowMapper接口的實現。這是一種非常方便而強大的重用方式。下面我們來看一下RowMapper的兩個具體實現。

首先是TitleMapper類,它簡單的把ResultSet中的每一行映射爲一個TitleDomainObject

import com.foo.sprocs.domain.Title;

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;

import java.sql.SQLException;

public final class TitleMapper implements RowMapper {

public Object mapRow(ResultSet rs, int rowNum) throws SQLException {

Title title = new Title();

title.setId(rs.getLong("id"));

title.setName(rs.getString("name"));

return title;

}

}

另一個是GenreMapper類,也是非常簡單的將ResultSet中的每一行映射爲一個GenreDomainObject

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;

import java.sql.SQLException;

import com.foo.domain.Genre;

public final class GenreMapper implements RowMapper {

public Object mapRow(ResultSet rs, int rowNum) throws SQLException {

return new Genre(rs.getString("name"));

}

}

如果你需要給存儲過程傳輸入參數(這些輸入參數是在RDBMS存儲過程中定義好了的),則需要提供一個指定類型的execute(..)方法,該方法將調用基類的protectedexecute(Mapparameters)方法。例如:

import oracle.jdbc.driver.OracleTypes;

import org.springframework.jdbc.core.SqlOutParameter;

import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;

import java.util.HashMap;

import java.util.Map;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

private static final String SPROC_NAME = "TitlesAfterDate";

private static final String CUTOFF_DATE_PARAM = "cutoffDate";

public TitlesAfterDateStoredProcedure(DataSource dataSource) {

super(dataSource, SPROC_NAME);

declaraParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);

declareParameter(new SqlOutParameter("titles",OracleTypes.CURSOR, new TitleMapper()));

compile();

}

public Map execute(Date cutoffDate) {

Map inputs = new HashMap();

inputs.put(CUTOFF_DATE_PARAM, cutoffDate);

return super.execute(inputs);

}

}

11.4.5.SqlFunction

SqlFunctionRDBMS操作類封裝了一個SQL“函數”包裝器(wrapper),該包裝器適用於查詢並返回一個單行結果集。默認返回的是一個int值,不過我們可以採用類似JdbcTemplate中的queryForXxx做法自己實現來返回其它類型。SqlFunction優勢在於我們不必創建JdbcTemplate,這些它都在內部替我們做了。

該類的主要用途是調用SQL函數來返回一個單值的結果集,比如類似“selectuser()”、“selectsysdatefromdual”的查詢。如果需要調用更復雜的存儲函數,可以使用StoredProcedureSqlCall

SqlFunction是一個具體類,通常我們不需要它的子類。其用法是創建該類的實例,然後聲明SQL語句以及參數就可以調用相關的run方法去多次執行函數。下面的例子用來返回指定表的記錄行數:

public int countRows() {

SqlFunction sf = new SqlFunction(dataSource, "select count(*) frommytable");

sf.compile();

return sf.run();

}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章