Spring提供的JDBC抽象框架由core, datasource,object和 support四個不同的包組成。
就和它名字的暗示一樣,org.springframework.jdbc.core包裡定義了提供核心功能的類。 其中有各種SQLExceptionTranslator和DataFieldMaxValueIncrementer的實現以及一個用於JdbcTemplate的DAO基礎類。
org.springframework.jdbc.datasource包裡有一個用以簡化數據源訪問的工具類, 以及各種數據源的簡單實現,以被用來在J2EE容器之外不經修改地測試JDBC代碼。 這個工具類提供了從JNDI獲得連接和可能用到的關閉連接的靜態方法。 它支持綁定線程的連接,比如被用於DataSourceTransactionManager。
接著,org.springframework.jdbc.object包裡是把關系數據庫的查詢, 更新和存儲過程封裝為線程安全並可重用對象的類。 這中方式模擬了JDO,盡管查詢返回的對象理所當然的“脫離”了數據庫連接。 這個JDBC的高層抽象依賴於org.springframework.jdbc.core包中所實現的底層抽象。
最後在org.springframework.jdbc.support包中你可以找到 SQLException的轉換功能和一些工具類。
在JDBC調用中被拋出的異常會被轉換成在定義org.springframework.dao包中的異常。 這意味著使用Spring JDBC抽象層的代碼不需要實現JDBC或者RDBMS特定的錯誤處理。 所有的轉換後的異常都是unchecked,它允許你捕捉那些你可以恢復的異常, 並將其余的異常傳遞給調用者。
JdbcTemplate
這是在JDBC核心包中最重要的類。它簡化了JDBC的使用, 因為它處理了資源的建立和釋放。 它幫助你避免一些常見的錯誤,比如忘了總要關閉連接。它運行核心的JDBC工作流, 如Statement的建立和執行,而只需要應用程序代碼提供SQL和提取結果。這個類執行SQL查詢, 更新或者調用存儲過程,模擬結果集的迭代以及提取返回參數值。它還捕捉JDBC的異常並將它們轉換成 org.springframework.dao包中定義的通用的,能夠提供更多信息的異常體系。
使用這個類的代碼只需要根據明確定義的一組契約來實現回調接口。 PreparedStatementCreator回調接口創建一個由該類提供的連接所建立的PreparedStatement, 並提供SQL和任何必要的參數。CallableStatementCreateor實現同樣的處理, 只是它創建了CallableStatement。RowCallbackHandler接口從數據集的每一行中提取值。
這個類可以直接通過數據源的引用實例化,然後在服務中使用, 也可以在ApplicationContext中產生並作為bean的引用給服務使用。 注意:數據源應當總是作為一個bean在ApplicationContext中配置, 第一種情況它被直接傳遞給服務,第二種情況給JdbcTemplate。 因為這個類把回調接口和SQLExceptionTranslator接口作為參數表示,所以沒有必要為它定義子類。 這個類執行的所有SQL都會被記入日志。
數據源
為了從數據庫獲得數據,我們需要得到數據庫的連接。 Spring采用的方法是通過一個數據源。 數據源是JDBC規范的一部分,它可以被認為是一個通用的連接工廠。 它允許容器或者框架將在應用程序代碼中隱藏連接池和事務的管理操作。 開發者將不需要知道連接數據庫的任何細節,那是設置數據源的管理員的責任。 雖然你很可能在開發或者測試的時候需要兼任兩種角色,但是你並不需要知道實際產品中的數據源是如何配置的。
使用Spring的JDBC層,你可以從JNDI得到一個數據源,也可以通過使用Spring發行版提供的實現自己配置它。 後者對於脫離Web容器的單元測試是十分便利的。 我們將在本節中使用DriverManagerDataSource實現,當然以後還會提到其他的一些的實現。 DriverManagerDataSource和傳統的方式一樣獲取JDBC連接。 為了讓DriverManager可以裝載驅動類,你必須指定JDBC驅動完整的類名。 然後你必須提供相對於各種JDBC驅動的不同的URL。你必須參考你所用的驅動的文檔,以獲得需要使用的正確參數。 最後,你還必須提供用來連接數據庫的用戶名和密碼 下面這個例子說明如何配置DriverManagerDataSource:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName( "org.hsqldb.jdbcDriver");
dataSource.setUrl( "jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername( "sa");
dataSource.setPassword( "");
10.2.3. SQLExceptionTranslator
SQLExceptionTranslator是一個需要實現的接口, 它被用來處理SQLException和我們的數據訪問異常org.springframework.dao.DataAccessException之間的轉換。
實現可以是通用的(比如使用JDBC的SQLState值),也可以為了更高的精確度特殊化 (比如使用Oracle的ErrorCode)。
SQLErrorCodeSQLExceptionTranslator 是SQLExceptionTranslator的實現,它被默認使用。比供應商指定的SQLState更為精確。 ErrorCode的轉換是基於被保存在SQLErrorCodes型的JavaBean中的值。 這個類是由SQLErrorCodesFactory建立並填充的,就像它的名字說明的, SQLErrorCodesFactory是一個基於一個名為"sql-error-codes.xml"的配置文件的內容建立SQLErrorCodes的工廠。 這個文件帶有供應商的碼一起發布,並且是基於DatabaseMetaData信息中的DatabaseProductName,適合當前數據庫的碼會被使用。
SQLErrorCodeSQLExceptionTranslator使用以下的匹配規則:
使用子類實現的自定義轉換。要注意的是這個類本身就是一個具體類,並可以直接使用, 在這種情況下,將不使用這條規則。
使用ErrorCode的匹配。在默認情況下,ErrorCode是從SQLErrorCodesFactory得到的。 它從classpath中尋找ErrorCode,並把從數據庫metadata中得到的數據庫名字嵌入它們。
如果以上規則都無法匹配,那麼是用SQLStateSQLExceptionTranslator作為默認轉換器。
SQLErrorCodeSQLExceptionTranslator可以使用以下的方式繼承:
public class MySQLErrorCodesTransalator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) {
if (sqlex.getErrorCode() == -12345)
return new DeadlockLoserDataAccessException(task, sqlex);
return null;
}
}
在這個例子中,只有特定的ErrorCode'-12345'被轉換了,其他的錯誤被簡單的返回,由默認的轉換實現來處理。要使用自定義轉換器時,需要通過setExceptionTranslator 方法將它傳遞給JdbcTemplate,並且在所有需要使用自定義轉換器的數據訪問處理中使用這個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 default translation 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中查找ErrorCode。
執行Statement
要執行一個SQL,幾乎不需要代碼。你所需要的全部僅僅是一個數據源和一個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, name varchar(100))");
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
執行查詢
除了execute方法,還有大量的查詢方法。其中的一些被用來執行那些只返回單個值的查詢。 也許你需要得到合計或者某一行中的一個特定的值。如果是這種情況,你可以使用queryForInt, queryForLong或者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 from mytable", java.lang.String.class);
return name;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
除了得到單一結果的查詢方法之外,還有一些方法,可以得到一個包含查詢返回的數據的List。 其中最通用的一個方法是queryForList,它返回一個List, 其中每一項都是一個表示字段值的Map。 你用下面的代碼在上面的例子中增加一個方法來得到一個所有記錄行的List:
public List getList() {
jt = new JdbcTemplate(dataSource);
List rows = jt.queryForList("select * from mytable");
return rows;
}
返回的List會以下面的形式: [{name=Bob, id=1}, {name=Mary, id=2}].
更新數據庫
還有很多更新的方法可以供你使用。我將展示一個例子,說明通過某一個主鍵更新一個字段。 在這個例子裡,我用了一個使用榜定參數的SQL Statement。大多數查詢和更新的方法都有這個功能。 參數值通過對象數組傳遞。
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 = ?", new Object[] {name, new Integer(id)});
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
控制如何連接數據庫
DataSourceUtils
這個輔助類提供從JNDI獲取連接和在必要時關閉連接的方法。它支持線程綁定的連接, 比如被用於DataSourceTransactionManager。
注意:方法getDataSourceFromJndi用以針對那些不使用BeanFactory或者ApplicationContext 的應用。對於後者,建議在factory中配置你的bean甚至JdbcTemplate的引用: 使用JndiObjectFactoryBean可以從JNDI中得到一個DataSource 並將DataSource的引用給別的bean。要切換到另一個DataSource 就僅僅是一個配置的問題:你甚至可以用一個非JNDI的DataSource來替換 FactoryBean的定義!
SmartDataSource
實現這個接口的類可以提供一個關系數據庫的連接。 它繼承javax.sql.DataSource接口,使用它可以知道在一次數據庫操作後, 是否需要關閉連接。如果我們需要重用一個連接,那麼它對於效率是很有用的。
AbstractDataSource
這個實現Spring的DataSource的抽象類,關注一些"無趣"的東西。 如果你要寫自己的DataSource實現,你可以繼承這個類。
SingleConnectionDataSource
這個SmartDataSource的實現封裝了單個在使用後不會關閉的連接。 所以很明顯,它沒有多線程的能力。
如果客戶端代碼想關閉這個認為是池管理的連接,比如使用持久化工具的時候, 需要將suppressClose設置成true。這樣會返回一個禁止關閉的代理來接管物理連接。 需要注意的是,你將無法將不再能將這個連接轉換成本地Oracle連接或者類似的連接。
它的主要作用是用來測試。例如,它可以很容易的讓測試代碼脫離應用服務器測試,而只需要一個簡易的JNDI環境。 和DriverManagerDataSource相反,它在所有的時候都重用一個連接, 以此來避免建立物理連接過多的消耗。
DriverManagerDataSource
這個SmartDataSource的實現通過bean的屬性配置JDBC驅動, 並每次都返回一個新的連接。
它對於測試或者脫離J2EE容器的獨立環境都是有用的, 它可以作為不同的ApplicationContext中的數據源bean, 也可以和簡易的JNDI環境一起工作。被認為是池管理的Connection.close()操作 的調用只會簡單的關閉連接,所以任何使用數據源的持久化代碼都可以工作。
DataSourceTransactionManager
這個PlatformTransactionManager的實現是對於單個JDBC數據源的。 從某個數據源綁定一個JDBC連接到一個線程,可能允許每個數據源一個線程連接。
應用程序的代碼需要通過DataSourceUtils.getConnection(DataSource)取得JDBC連接代替 J2EE標准的方法DataSource.getConnection。這是推薦的方法, 因為它會拋出org.springframework.dao中的unchecked的異常代替SQLException。 Framework中所有的類都使用這種方法,比如JdbcTemplate。 如果不使用事務管理,那麼就會使用標准的方法,這樣他就可以在任何情況下使用。
支持自定義的隔離級,以及應用於適當的JDBC statement查詢的超時。 要支持後者,應用程序代碼必須使用JdbcTemplate或者對每一個創建的statement 都調用DataSourceUtils.applyTransactionTimeout。
因為它不需要容器支持JTA,在只有單個資源的情況下, 這個實現可以代替JtaTransactionManager。 如果你堅持需要的連接的查找模式,兩者間的切換只需要更換配置。 不過需要注意JTA不支持隔離級。
JDBC操作的Java對象化
org.springframework.jdbc.object包由一些允許你 以更面向對象的方式訪問數據庫的類組成。你可以執行查詢並獲得一個包含業務對象的List, 這些業務對象關系數據的字段值映射成它們的屬性。你也可以執行存儲過程,更新,刪除和插入操作。
SqlQuery
這是一個表示SQL查詢的可重用的而且線程安全的對象。 子類必須實現newResultReader()方法來提供一個對象,它能在循環處理ResultSet的時候保存結果。 這個類很少被直接使用,而使用它的子類MappingSqlQuery,它提供多得多的方法 將數據行映射到Java類。MappingSqlQueryWithParameters 和UpdatableSqlQuery是繼承SqlQuery的另外兩個實現。
MappingSqlQuery
MappingSqlQuery是一個可以重用的查詢對象, 它的子類必須實現抽象方法mapRow(ResultSet, int)來把JDBC ResultSet的每一行轉換成對象。
在所有的SqlQuery實現中,這個類是最常使用並且也是最容易使用的。
下面是一個自定義查詢的簡單例子,它把customer表中的數據映射成叫做Customer的Java類。
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;
}
}
我們為customer查詢提供一個構建方法,它只有數據源這一個參數。 在構建方法中,我們調用超類的構建方法,並將數據源和將要用來查詢取得數據的SQL作為參數。 因為這個SQL將被用來建立PreparedStatement,所以它可以包含?來綁定執行時會得到的參數。 每一個參數必須通過declareParameter方法並傳遞給它一個SqlParameter來聲明。 SqlParameter有一個名字和一個在java.sql.Types定義的JDBC類型。 在所有的參數都定義完後,我們調用compile方法建立隨後會執行的PreparedStatement。
我們來看一段代碼,來實例化這個自定義查詢對象並執行:
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;
}
在例子中的這個方法通過一個參數id得到customer。在建立了CustomerMappingQuery 類的一個實例後,我們再創建一個數組,用來放置所有需要傳遞的參數。 在這個例子中只有一個Integer的參數需要傳遞。 現在我們使用這個數組執行查詢,我們會得到一個List包含著Customer對象, 它對應查詢返回結果的每一行。在這個例子中如果有匹配的話,只會有一個實體。
SqlUpdate
這個RdbmsOperation子類表示一個SQL更新操作。就像查詢一樣, 更新對象是可重用的。和所有的RdbmsOperation對象一樣,更新可以有參數並定義在SQL中。
類似於查詢對象中的execute()方法,這個類提供很多update()的方法。
這個類是具體的。通過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);
}
}
StoredProcedure
這是RDBMS存儲過程的對象抽象的超類。它是一個抽象類,它的執行方法都是protected的, 以避免被直接調用,而只能通過提供更嚴格形式的子類調用。
繼承的sql屬性是RDBMS中存儲過程的名字。雖然這個類中提供的其他功能在JDBC3.0中也十分的重要, 但最值得注意的是JDBC3.0中的使用命名的參數。
下面是一段例子程序,它調用Oracle數據庫提供的函數sysdate()。 要使用存儲過程的功能,你必須創建一個繼承StoredProcedure的類. 這裡沒有任何輸入參數,但需要使用SqlOutParameter類聲明一個date型的輸出參數。 execute()方法會返回一個使用名字作為key來映射每一個被聲明的輸出參數的實體的Map。
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 TestSP {
public static void main(String[] args) {
System.out.println("DB TestSP!");
TestSP t = new TestSP();
t.test();
System.out.println("Done!");
}
void test() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
ds.setUrl("jdbc:oracle:thin:@localhost:1521:mydb");
ds.setUsername("scott");
ds.setPassword("tiger");
MyStoredProcedure sproc = new MyStoredProcedure(ds);
Map res = sproc.execute();
printMap(res);
}
private class MyStoredProcedure extends StoredProcedure {
public 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() {
Map out = execute(new HashMap());
return out;
}
}
private static void printMap(Map r) {
Iterator i = r.entrySet().iterator();
while (i.hasNext()) {
System.out.println((String) i.next().toString());
}
}
}
SqlFunction
SQL "function"封裝返回單一行結果的查詢。默認的情況返回一個int,當然我們可以重載它, 通過額外返回參數得到其他類型。這和使用JdbcTemplate的 queryForXxx方法很相似。使用SqlFunction的好處是 不用必須在後台建立JdbcTemplate。
這個類的目的是調用SQL function,使用像"select user()"或者"select sysdate from dual" 得到單一的結果。它不是用來調用復雜的存儲功能也不是用來使用CallableStatement 來調用存儲過程或者存儲功能。對於這類的處理應當使用StoredProcedure或者SqlCall。
這是一個具體的類,它通常不需要子類化。使用這個包的代碼可以通過聲明SQL和參數創建這個類型的對象, 然後能重復的使用run方法執行這個function。下面是一個得到一張表的行數的例子:
public int countRows() {
SqlFunction sf = new SqlFunction(dataSource, "select count(*) from mytable");
sf.compile();
return sf.run();
}