1. 程式人生 > >《Spring 5 官方文件》15.使用JDBC實現資料訪問

《Spring 5 官方文件》15.使用JDBC實現資料訪問

15.1 介紹Spring JDBC框架

表格13.1很清楚的列舉了Spring框架針對JDBC操作做的一些抽象和封裝。裡面區分了哪些操作Spring已經幫你做好了、哪些操作是應用開發者需要自己負責的.

表13.1. Spring JDBC – 框架和應用開發者各自分工

操作 Spring 開發者
定義連線引數 X
開啟連線 X
指定SQL語句 X
宣告引數和提供引數值 X
準備和執行語句 X
返回結果的迭代(如果有) X
具體操作每個迭代 X
異常處理 X
事務處理 X
關閉連線、語句和結果集 X

一句話、Spring幫你遮蔽了很多JDBC底層繁瑣的API操作、讓你更方便的開發

15.1.1 選擇一種JDBC資料庫訪問方法

JDBC資料庫訪問有幾種基本的途徑可供選擇。除了JdbcTemplate的三種使用方式外,新的SimpleJdbcInsert和SimplejdbcCall呼叫類通過優化資料庫元資料(來簡化JDBC操作),還有一種更偏向於面向物件的RDBMS物件風格的方法、有點類似於JDO的查詢設計。即使你已經選擇了其中一種方法、你仍然可以混合使用另外一種方法的某一個特性。所有的方法都需要JDBC2.0相容驅動的支援,一些更高階的特性則需要使用JDBC3.0驅動支援。

  • JdbcTemplate 是經典的Spring JDBC訪問方式,也是最常用的。這是“最基礎”的方式、其他所有方式都是在 JdbcTemplate的基礎之上封裝的。
  • NamedParameterJdbcTemplate 在原有JdbcTemplate的基礎上做了一層包裝支援命名引數特性、用於替代傳統的JDBC“?”佔位符。當SQL語句中包含多個引數時使用這種方式能有更好的可讀性和易用性
  • SimpleJdbcInsert和SimpleJdbcCall操作類主要利用JDBC驅動所提供的資料庫元資料的一些特性來簡化資料庫操作配置。這種方式簡化了編碼、你只需要提供表或者儲存過程的名字、以及和列名相匹配的引數Map。但前提是資料庫需要提供足夠的元資料。如果資料庫沒有提供這些元資料,需要開發者顯式配置引數的對映關係。
  • RDBMS物件的方式包含MappingSqlQuery, SqlUpdate和StoredProcedure,需要你在初始化應用資料訪問層時建立可重用和執行緒安全的物件。這種方式設計上類似於JDO查詢、你可以定義查詢字串,宣告引數及編譯查詢語句。一旦完成這些工作之後,執行方法可以根據不同的傳入引數被多次呼叫。
  • 15.1.2 包層級
    Spring的JDBC框架一共包含4種不同型別的包、包括core,datasource,object和support.

    org.springframework.jdbc.core包含JdbcTemplate 類和它各種回撥介面、外加一些相關的類。它的一個子包
    org.springframework.jdbc.core.simple包含SimpleJdbcInsert和SimpleJdbcCall等類。另一個叫org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate及它的一些工具類。詳見:
    15.2:“使用JDBC核心類控制基礎的JDBC處理過程和異常處理機制
    15.4:“JDBC批量操作
    15.5:“利用SimpleJdbc 類簡化JDBC操作”.

    org.springframework.jdbc.datasource包包含DataSource資料來源訪問的工具類,以及一些簡單的DataSource實現用於測試和脫離JavaEE容器執行的JDBC程式碼。子包org.springfamework.jdbc.datasource.embedded提供Java內建資料庫例如HSQL, H2, 和Derby的支援。詳見:
    15.3:“控制資料庫連線
    15.8:“內建資料庫支援”.

    org.springframework.jdbc.object包含用於在RDBMS查詢、更新和儲存過程中建立執行緒安全及可重用的物件類。詳見15.6: “像Java物件那樣操作JDBC”;這種方式類似於JDO的查詢方式,不過查詢返回的物件是與資料庫脫離的。此包針對JDBC做了很多上層封裝、而底層依賴於org.springframework.jdbc.core包。

    org.springframework.jdbc.support包含SQLException的轉換類和一些工具類。JDBC處理過程中丟擲的異常會被轉換成org.springframework.dao裡面定義的異常類。這意味著SpringJDBC抽象層的程式碼不需要實現JDBC或者RDBMS特定的錯誤處理方式。所有轉換的異常都沒有被捕獲,而是讓開發者自己處理異常、具體的話既可以捕獲異常也可以直接拋給上層呼叫者
    詳見:15.2.3:“SQL異常轉換器”.

    15.2 使用JDBC核心類控制基礎的JDBC處理過程和異常處理機制

    15.2.1 JdbcTemplate

    JdbcTemplate是JDBC core包裡面的核心類。它封裝了對資源的建立和釋放,可以幫你避免忘記關閉連線等常見錯誤。它也包含了核心JDBC工作流的一些基礎工作、例如執行和宣告語句,而把SQL語句的生成以及查詢結果的提取工作留給應用程式碼。JdbcTemplate執行查詢、更新SQL語句和呼叫儲存過程,執行結果集迭代和抽取返回引數值。它也可以捕獲JDBC異常並把它們轉換成更加通用、解釋性更強的異常層次結構、這些異常都定義在org.springframework.dao包裡面。

    當你在程式碼中使用了JdbcTemplate類,你只需要實現回撥介面。PreparedStatementCreator回撥介面通過傳入的Connection類(該類包含SQL和任何必要的引數)建立已宣告的語句。CallableStatementCreator也提供類似的方式、該介面用於建立回撥語句。RowCallbackHandler用於獲取結果集每一行的值。

    可以在DAO實現類中通過傳入DataSource引用來完成JdbcTemplate的初始化;也可以在Spring IOC容器裡面配置、作為DAO bean的依賴Bean配置。

    備註:DataSource最好在Spring IOC容器裡作為Bean配置起來。在上面第一種情況下,DataSource bean直接傳給相關的服務;第二種情況下DataSource bean傳遞給JdbcTemplate bean。

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

    JdbcTemplate 使用示例

    這一節提供了JdbcTemplate類的一些使用例子。這些例子沒有囊括JdbcTemplate可提供的所有功能;全部功能和用法請詳見相關的javadocs.

    查詢 (SELECT)

    下面是一個簡單的例子、用於獲取關係表裡面的行數

    int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
    

    使用繫結變數的簡單查詢:

    int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
    		"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
    

    String查詢:

    String lastName = this.jdbcTemplate.queryForObject(
    		"select last_name from t_actor where id = ?",
    		new Object[]{1212L}, String.class);
    

    查詢和填充領域模型:

    Actor actor = this.jdbcTemplate.queryForObject(
    		"select first_name, last_name from t_actor where id = ?",
    		new Object[]{1212L},
    		new RowMapper<Actor>() {
    			public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    				Actor actor = new Actor();
    				actor.setFirstName(rs.getString("first_name"));
    				actor.setLastName(rs.getString("last_name"));
    				return actor;
    			}
    		});
    

    查詢和填充多個領域物件:

    List<Actor> actors = this.jdbcTemplate.query(
    		"select first_name, last_name from t_actor",
    		new RowMapper<Actor>() {
    			public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    				Actor actor = new Actor();
    				actor.setFirstName(rs.getString("first_name"));
    				actor.setLastName(rs.getString("last_name"));
    				return actor;
    			}
    		});
    

    如果上面的兩段程式碼實際存在於相同的應用中,建議把RowMapper匿名類中重複的程式碼抽取到單獨的類中(通常是一個靜態類),方便被DAO方法引用。例如,上面的程式碼例子更好的寫法如下:

    public List<Actor> findAllActors() {
    	return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
    }
    
    private static final class ActorMapper implements RowMapper<Actor> {
    
    	public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    		Actor actor = new Actor();
    		actor.setFirstName(rs.getString("first_name"));
    		actor.setLastName(rs.getString("last_name"));
    		return actor;
    	}
    }
    

    使用jdbcTemplate實現增刪改

    你可以使用update(..)方法實現插入,更新和刪除操作。引數值可以通過可變引數或者封裝在物件內傳入。

    this.jdbcTemplate.update(
    		"insert into t_actor (first_name, last_name) values (?, ?)",
    		"Leonor", "Watling");
    
    this.jdbcTemplate.update(
    		"update t_actor set last_name = ? where id = ?",
    		"Banjo", 5276L);
    
    this.jdbcTemplate.update(
    		"delete from actor where id = ?",
    		Long.valueOf(actorId));
    

    其他jdbcTemplate操作

    你可以使用execute(..)方法執行任何SQL,甚至是DDL語句。這個方法可以傳入回撥介面、繫結可變引數陣列等。

    this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
    

    下面的例子呼叫一段簡單的儲存過程。更復雜的儲存過程支援文件後面會有描述。

    this.jdbcTemplate.update(
    		"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
    		Long.valueOf(unionId));
    

    JdbcTemplate 最佳實踐
    JdbcTemplate例項一旦配置之後是執行緒安全的。這點很重要因為這樣你就能夠配置JdbcTemplate的單例,然後安全的將其注入到多個DAO中(或者repositories)。JdbcTemplate是有狀態的,內部存在對DataSource的引用,但是這種狀態不是會話狀態。

    使用JdbcTemplate類的常用做法是在你的Spring配置檔案裡配置好一個DataSource,然後將其依賴注入進你的DAO類中(NamedParameterJdbcTemplate也是如此)。JdbcTemplate在DataSource的Setter方法中被建立。就像如下DAO類的寫法一樣:

    public class JdbcCorporateEventDao implements CorporateEventDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	// JDBC-backed implementations of the methods on the CorporateEventDao follow...
    }
    

    相關的配置是這樣的:

    <?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:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="
    		http://www.springframework.org/schema/beans
    		http://www.springframework.org/schema/beans/spring-beans.xsd
    		http://www.springframework.org/schema/context
    		http://www.springframework.org/schema/context/spring-context.xsd">
    
    	<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
    		<property name="dataSource" ref="dataSource"/>
    	</bean>
    
    	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    		<property name="driverClassName" value="${jdbc.driverClassName}"/>
    		<property name="url" value="${jdbc.url}"/>
    		<property name="username" value="${jdbc.username}"/>
    		<property name="password" value="${jdbc.password}"/>
    	</bean>
    
    	<context:property-placeholder location="jdbc.properties"/>
    
    </beans>
    

    另一種替代顯式配置的方式是使用component-scanning和註解注入。在這個場景下需要新增@Repository註解(新增這個註解可以被component-scanning掃描到),同時在DataSource的Setter方法上新增@Autowired註解:

    @Repository
    public class JdbcCorporateEventDao implements CorporateEventDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	@Autowired
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	// JDBC-backed implementations of the methods on the CorporateEventDao follow...
    }
    

    相關的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:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="
    		http://www.springframework.org/schema/beans
    		http://www.springframework.org/schema/beans/spring-beans.xsd
    		http://www.springframework.org/schema/context
    		http://www.springframework.org/schema/context/spring-context.xsd">
    
    	<!-- Scans within the base package of the application for @Component classes to configure as beans -->
    	<context:component-scan base-package="org.springframework.docs.test" />
    
    	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    		<property name="driverClassName" value="${jdbc.driverClassName}"/>
    		<property name="url" value="${jdbc.url}"/>
    		<property name="username" value="${jdbc.username}"/>
    		<property name="password" value="${jdbc.password}"/>
    	</bean>
    
    	<context:property-placeholder location="jdbc.properties"/>
    
    </beans>
    

    如果你使用Spring的JdbcDaoSupport類,許多JDBC相關的DAO類都從該類繼承過來,這個時候相關子類需要繼承JdbcDaoSupport類的setDataSource方法。當然你也可以選擇不從這個類繼承,JdbcDaoSupport本身只是提供一些便利性。

    無論你選擇上面提到的哪種初始方式,當你在執行SQL語句時一般都不需要重新建立JdbcTemplate 例項。JdbcTemplate一旦被配置後其例項都是執行緒安全的。當你的應用需要訪問多個數據庫時你可能也需要多個JdbcTemplate例項,相應的也需要多個DataSources,同時對應多個JdbcTemplates配置。

    15.2.2 NamedParameterJdbcTemplate
    NamedParameterJdbcTemplate 提供對JDBC語句命名引數的支援,而普通的JDBC語句只能使用經典的 ‘?’引數。NamedParameterJdbcTemplate內部包裝了JdbcTemplate,很多功能是直接通過JdbcTemplate來實現的。本節主要描述NamedParameterJdbcTemplate不同於JdbcTemplate 的點;即通過使用命名引數來操作JDBC

    // some JDBC-backed DAO class...
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    
    public int countOfActorsByFirstName(String firstName) {
    
    	String sql = "select count(*) from T_ACTOR where first_name = :first_name";
    
    	SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
    
    	return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
    }
    

    上面程式碼塊可以看到SQL變數中命名引數的標記用法,以及namedParameters變數的相關賦值(型別為MapSqlParameterSource)

    除此以外,你還可以在NamedParameterJdbcTemplate中傳入Map風格的命名引數及相關的值。NamedParameterJdbcTemplate類從NamedParameterJdbcOperations介面實現的其他方法用法是類似的,這裡就不一一敘述了。

    下面是一個Map風格的例子:

    // some JDBC-backed DAO class...
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    
    public int countOfActorsByFirstName(String firstName) {
    
    	String sql = "select count(*) from T_ACTOR where first_name = :first_name";
    
    	Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
    
    	return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
    }
    

    與NamedParameterJdbcTemplate相關聯的SqlParameterSource介面提供了很有用的功能(兩者在同一個包裡面)。在上面的程式碼片段中你已經看到了這個介面的一個實現例子(就是MapSqlParameterSource類)。SqlParameterSource類是NamedParameterJdbcTemplate
    類的數值值來源。MapSqlParameterSource實現非常簡單、只是適配了java.util.Map,其中Key就是引數名字,Value就是引數值。

    另外一個SqlParameterSource 的實現是BeanPropertySqlParameterSource類。這個類封裝了任意一個JavaBean(也就是任意符合JavaBen規範的例項),在這個實現中,使用了JavaBean的屬性作為命名引數的來源。

    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;
    	}
    
    	// setters omitted...
    
    }
    
    // some JDBC-backed DAO class...
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    
    public int countOfActors(Actor exampleActor) {
    
    	// notice how the named parameters match the properties of the above 'Actor' class
    	String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";
    
    	SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
    
    	return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
    }
    

    之前提到過NamedParameterJdbcTemplate本身包裝了經典的JdbcTemplate模板。如果你想呼叫只存在於JdbcTemplate類中的方法,你可以使用getJdbcOperations()方法、該方法返回JdbcOperations介面,通過這個介面你可以呼叫內部JdbcTemplate的方法。

    NamedParameterJdbcTemplate 類在應用上下文的使用方式也可見:“JdbcTemplate最佳實踐

    15.2.3 SQLExceptionTranslator

    SQLExceptionTranslator介面用於在SQLExceptions和spring自己的org.springframework.dao.DataAccessException之間做轉換,要處理批量更新或者從檔案中這是為了遮蔽底層的資料訪問策略。其實現可以是比較通用的(例如,使用JDBC的SQLState編碼),或者是更精確專有的(例如,使用Oracle的錯誤型別編碼)

    SQLExceptionTranslator 介面的預設實現是SQLErrorCodeSQLExceptionTranslator,該實現使用的是指定資料庫廠商的錯誤編碼,因為要比SQLState的實現更加精確。錯誤碼轉換過程基於JavaBean型別的SQLErrorCodes。這個類通過SQLErrorCodesFactory建立和返回,SQLErrorCodesFactory是一個基於sql-error-codes.xml配置內容來建立SQLErrorCodes的工廠類。該配置中的資料庫廠商程式碼基於Database MetaData資訊中返回的資料庫產品名(DatabaseProductName),最終使用的就是你正在使用的實際資料庫中錯誤碼。

    SQLErrorCodeSQLExceptionTranslator按以下的順序來匹配規則:

    備註:SQLErrorCodesFactory是用於定義錯誤碼和自定義異常轉換的預設工廠類。錯誤碼參照Classpath下配置的sql-error-codes.xml檔案內容,相匹配的SQLErrorCodes例項基於正在使用的底層資料庫的元資料名稱

  • 是否存在自定義轉換的子類。通常直接使用SQLErrorCodeSQLExceptionTranslator就可以了,因此此規則一般不會生效。只有你真正自己實現了一個子類才會生效。
  • 是否存在SQLExceptionTranslator介面的自定義實現,通過SQLErrorCodes類的customSqlExceptionTranslator屬性指定
  • SQLErrorCodes的customTranslations屬性陣列、型別為CustomSQLErrorCodesTranslation類例項列表、能否被匹配到
  • 錯誤碼被匹配到
  • 使用兜底的轉換器。SQLExceptionSubclassTranslator是預設的兜底轉換器。如果此轉換器也不存在的話只能使用SQLStateSQLExceptionTranslator
  • 你可以繼承SQLErrorCodeSQLExceptionTranslator:

    public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
    
    	protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) {
    		if (sqlex.getErrorCode() == -12345) {
    			return new DeadlockLoserDataAccessException(task, sqlex);
    		}
    		return null;
    	}
    }
    

    這個例子中,特定的錯誤碼-12345被識別後單獨轉換,而其他的錯誤碼則通過預設的轉換器實現來處理。在使用自定義轉換器時,有必要通過setExceptionTranslator方法傳入JdbcTemplate ,並且使用JdbcTemplate來做所有的資料訪問處理。下面是一個如何使用自定義轉換器的例子

    private JdbcTemplate jdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    
    	// create a JdbcTemplate and set data source
    	this.jdbcTemplate = new JdbcTemplate();
    	this.jdbcTemplate.setDataSource(dataSource);
    
    	// create a custom translator and set the DataSource for the default translation lookup
    	CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
    	tr.setDataSource(dataSource);
    	this.jdbcTemplate.setExceptionTranslator(tr);
    
    }
    
    public void updateShippingCharge(long orderId, long pct) {
    	// use the prepared JdbcTemplate for this update
    	this.jdbcTemplate.update("update orders" +
    		" set shipping_charge = shipping_charge * ? / 100" +
    		" where id = ?", pct, orderId);
    }
    

    自定義轉換器需要傳入dataSource物件為了能夠獲取sql-error-codes.xml定義的錯誤碼

    15.2.4 執行SQL語句

    執行一條SQL語句非常方便。你只需要依賴DataSource和JdbcTemplate,包括JdbcTemplate提供的工具方法。
    下面的例子展示瞭如何建立一個新的資料表,雖然只有幾行程式碼、但已經完全可用了:

    import javax.sql.DataSource;
    import org.springframework.jdbc.core.JdbcTemplate;
    
    public class ExecuteAStatement {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public void doExecute() {
    		this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
    	}
    }
    

    15.2.5 查詢
    一些查詢方法會返回一個單一的結果。使用queryForObject(..)返回結果計數或特定值。當返回特定值型別時,將Java型別作為方法引數傳入、最終返回的JDBC型別會被轉換成相應的Java型別。如果這個過程中間出現型別轉換錯誤,則會丟擲InvalidDataAccessApiUsageException的異常。下面的例子包含兩個查詢方法,一個返回int型別、另一個返回了String型別。

    import javax.sql.DataSource;
    import org.springframework.jdbc.core.JdbcTemplate;
    
    public class RunAQuery {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public int getCount() {
    		return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
    	}
    
    	public String getName() {
    		return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
    	}
    }
    

    除了返回單一查詢結果的方法外,其他方法返回一個列表、列表中每一項代表查詢返回的行記錄。其中最通用的方式是queryForList(..),返回一個列表,列表每一項是一個Map型別,包含資料庫對應行每一列的具體值。下面的程式碼塊給上面的例子新增一個返回所有行的方法:

    private JdbcTemplate jdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    public List<Map<String, Object>> getList() {
    	return this.jdbcTemplate.queryForList("select * from mytable");
    }
    

    返回的列表結果資料格式是這樣的:

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

    15.2.6 更新資料庫

    下面的例子根據主鍵更新其中一列值。在這個例子中,一條SQL語句包含行引數的佔位符。引數值可以通過可變引數或者物件陣列傳入。元資料型別需要顯式或者自動裝箱成對應的包裝型別

    import javax.sql.DataSource;
    
    import org.springframework.jdbc.core.JdbcTemplate;
    
    public class ExecuteAnUpdate {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public void setName(int id, String name) {
    		this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
    	}
    }
    

    15.2.7 獲取自增Key
    update()方法支援獲取資料庫自增Key。這個支援已成為JDBC3.0標準之一、更多細節詳見13.6章。這個方法使用PreparedStatementCreator作為其第一個入參,該類可以指定所需的insert語句。另外一個引數是KeyHolder,包含了更新操作成功之後產生的自增Key。這不是標準的建立PreparedStatement 的方式。下面的例子可以在Oracle上面執行,但在其他平臺上可能就不行了。

    final String INSERT_SQL = "insert into my_test (name) values(?)";
    final String name = "Rob";
    
    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcTemplate.update(
    	new PreparedStatementCreator() {
    		public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
    			PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
    			ps.setString(1, name);
    			return ps;
    		}
    	},
    	keyHolder);
    
    // keyHolder.getKey() now contains the generated key
    

    15.3 控制資料庫連線
    15.3.1 DataSource

    Spring用DataSource來保持與資料庫的連線。DataSource是JDBC規範的一部分同時是一種通用的連線工廠。它使得框架或者容器對應用程式碼遮蔽連線池或者事務管理等底層邏輯。作為開發者,你無需知道連線資料庫的底層邏輯;這只是建立datasource的管理員該負責的模組。在開發測試過程中你可能需要同時扮演雙重角色,但最終上線時你不需要知道生產資料來源是如何配置的。

    當使用Spring JDBC時,你可以通過JNDI獲取資料庫資料來源、也可以利用第三方依賴包的連線池實現來配置。比較受歡迎的三方庫有Apache Jakarta Commons DBCP 和 C3P0。在Spring產品內,有自己的資料來源連線實現,但僅僅用於測試目的,同時並沒有使用到連線池。

    這一節使用了Spring的DriverManagerDataSource實現、其他更多的實現會在後面提到。

    注意:僅僅使用DriverManagerDataSource類只是為了測試目的、因為此類沒有連線池功能,因此在併發連線請求時效能會比較差

    通過DriverManagerDataSource獲取資料庫連線的方式和傳統JDBC是類似的。首先指定JDBC驅動的類全名,DriverManager 會據此來載入驅動類。接下來、提供JDBC驅動對應的URL名稱。(可以從相應驅動的文件裡找到具體的名稱)。然後傳入使用者名稱和密碼來連線資料庫。下面是一個具體配置DriverManagerDataSource連線的Java程式碼塊:

    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
    dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
    dataSource.setUsername("sa");
    dataSource.setPassword("");
    

    接下來是相關的XML配置:

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    	<property name="driverClassName" value="${jdbc.driverClassName}"/>
    	<property name="url" value="${jdbc.url}"/>
    	<property name="username" value="${jdbc.username}"/>
    	<property name="password" value="${jdbc.password}"/>
    </bean>
    
    <context:property-placeholder location="jdbc.properties"/>
    

    下面的例子展示的是DBCP和C3P0的基礎連線配置。如果需要連線更多的連線池選項、請檢視各自連線池實現的具體產品文件

    DBCP配置:

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    	<property name="driverClassName" value="${jdbc.driverClassName}"/>
    	<property name="url" value="${jdbc.url}"/>
    	<property name="username" value="${jdbc.username}"/>
    	<property name="password" value="${jdbc.password}"/>
    </bean>
    
    <context:property-placeholder location="jdbc.properties"/>
    

    C3P0配置:

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    	<property name="driverClass" value="${jdbc.driverClassName}"/>
    	<property name="jdbcUrl" value="${jdbc.url}"/>
    	<property name="user" value="${jdbc.username}"/>
    	<property name="password" value="${jdbc.password}"/>
    </bean>
    
    <context:property-placeholder location="jdbc.properties"/>
    

    15.3.2 DataSourceUtils
    DataSourceUtils類是一個方便有用的工具類,提供了從JNDI獲取和關閉連線等有用的靜態方法。它支援執行緒繫結的連線、例如:使用DataSourceTransactionManager的時候,將把資料庫連線繫結到當前的執行緒上。

    15.3.3 SmartDataSource
    實現SmartDataSource介面的實現類需要能夠提供到關係資料庫的連線。它繼承了DataSource介面,允許使用它的類查詢是否在某個特定的操作後需要關閉連線。這在當你需要重用連線時比較有用。

    15.3.4 AbstractDataSource
    AbstractDataSource是Spring DataSource實現的基礎抽象類,封裝了DataSource的基礎通用功能。你可以繼承AbstractDataSource自定義DataSource 實現。

    15.3.5 SingleConnectionDataSource
    SingleConnectionDataSource實現了SmartDataSource介面、內部封裝了一個在每次使用後都不會關閉的單一連線。顯然,這種場景下無法支援多執行緒。

    為了防止客戶端程式碼誤以為資料庫連線來自連線池(就像使用持久化工具時一樣)錯誤的呼叫close方法,你應將suppressClose設定為true。這樣,通過該類獲取的將是代理連線(禁止關閉)而不是原有的物理連線。需要注意你不能將這個類強制轉換成Oracle等資料庫的原生連線。

    這個類主要用於測試目的。例如,他使得測試程式碼能夠脫離應用伺服器,很方便的在單一的JNDI環境下除錯。和DriverManagerDataSource相反,它總是重用相同的連線,這是為了避免在測試過程中建立過多的物理連線。

    15.3.6 DriverManagerDataSource
    DriverManagerDataSource類實現了標準的DataSource介面,可以通過Java Bean屬性來配置原生的JDBC驅動,並且每次都返回一個新的連線。

    這個實現對於測試和JavaEE容器以外的獨立環境比較有用,無論是作為一個在Spring IOC容器內的DataSource Bean,或是在單一的JNDI環境中。由於Connection.close()僅僅只是簡單的關閉資料庫連線,因此任何能夠操作DataSource的持久層程式碼都能很好的工作。但是,使用JavaBean型別的連線池,比如commons-dbcp往往更簡單、即使是在測試環境下也是如此,因此更推薦commons-dbcp。

    15.3.7 TransactionAwareDataSourceProxy

    TransactionAwareDataSourceProxy會建立一個目標DataSource的代理,內部包裝了DataSource,在此基礎上添加了Spring事務管理功能。有點類似於JavaEE伺服器中提供的JNDI事務資料來源。

    注意:一般情況下很少用到這個類,除非現有程式碼在被呼叫的時候需要一個標準的 JDBC DataSource介面實現作為引數。在這種場景下,使用proxy可以仍舊重用老程式碼,同時能夠有Spring管理事務的能力。更多的場景下更推薦使用JdbcTemplate和DataSourceUtils等更高抽象的資源管理類.

    (更多細節請檢視TransactionAwareDataSourceProxy的JavaDoc)
    15.3.8 DataSourceTransactionManager
    DataSourceTransactionManager類實現了PlatformTransactionManager介面。它將JDBC連線從指定的資料來源繫結到當前執行的執行緒中,
    允許一個執行緒連線對應一個數據源。

    應用程式碼需要通過DataSourceUtils.getConnection(DataSource) 來獲取JDBC連線,而不是通過JavaEE標準的DataSource.getConnection來獲取。它會丟擲org.springframework.dao的執行時異常而不是編譯時SQL異常。所有框架類像JdbcTemplate都預設使用這個策略。如果不需要和這個 DataSourceTransactionManager類一起使用,DataSourceUtils 提供的功能跟一般的資料庫連線策略沒有什麼兩樣,因此它可以在任何場景下使用。

    DataSourceTransactionManager支援自定義隔離級別,以及JDBC查詢超時機制。為了支援後者,應用程式碼必須在每個建立的語句中使用JdbcTemplate或是呼叫DataSourceUtils.applyTransactionTimeout(..)方法

    在單一的資源使用場景下它可以替代JtaTransactionManager,不需要要求容器去支援JTA。如果你嚴格遵循連線查詢的模式的話、可以通過配置來做彼此切換。JTA本身不支援自定義隔離級別!

    15.4 JDBC批量操作
    大多數JDBC驅動在針對同一SQL語句做批處理時能夠獲得更好的效能。批量更新操作可以節省資料庫的來回傳輸次數。

    15.4.1 使用JdbcTemplate來進行基礎的批量操作
    通過JdbcTemplate 實現批處理需要實現特定介面的兩個方法,BatchPreparedStatementSetter,並且將其作為第二個引數傳入到batchUpdate方法呼叫中。使用getBatchSize提供當前批量操作的大小。使用setValues方法設定語句的Value引數。這個方法會按getBatchSize設定中指定的呼叫次數。下面的例子中通過傳入列表來批量更新actor表。在這個例子中整個列表使用了批量操作:

    public class JdbcActorDao implements ActorDao {
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public int[] batchUpdate(final List<Actor> actors) {
    		int[] updateCounts = jdbcTemplate.batchUpdate("update t_actor set first_name = ?, " +
    				"last_name = ? where id = ?",
    			new BatchPreparedStatementSetter() {
    				public void setValues(PreparedStatement ps, int i) throws SQLException {
    						ps.setString(1, actors.get(i).getFirstName());
    						ps.setString(2, actors.get(i).getLastName());
    						ps.setLong(3, actors.get(i).getId().longValue());
    					}
    
    					public int getBatchSize() {
    						return actors.size();
    					}
    				});
    		return updateCounts;
    	}
    
    	// ... additional methods
    }
    

    如果你需要處理批量更新或者從檔案中批量讀取,你可能需要確定一個合適的批處理大小,但是最後一次批處理可能達不到這個大小。在這種場景下你可以使用InterruptibleBatchPreparedStatementSetter介面,允許在輸入流耗盡之後終止批處理,isBatchExhausted方法使得你可以指定批處理結束時間。

    15.4.2 物件列表的批量處理
    JdbcTemplate和NamedParameterJdbcTemplate都提供了批量更新的替代方案。這個時候不是實現一個特定的批量介面,而是在呼叫時傳入所有的值列表。框架會迴圈訪問這些值並且使用內部的SQL語句setter方法。你是否已宣告引數對應API是不一樣的。針對已宣告引數你需要傳入qlParameterSource陣列,每項對應單次的批量操作。你可以使用SqlParameterSource.createBatch方法來建立這個陣列,傳入JavaBean陣列或是包含引數值的Map陣列。

    下面是一個使用已宣告引數的批量更新例子:

    public class JdbcActorDao implements ActorDao {
    	private NamedParameterTemplate namedParameterJdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    	}
    
    	public int[] batchUpdate(final List<Actor> actors) {
    		SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray());
    		int[] updateCounts = namedParameterJdbcTemplate.batchUpdate(
    				"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
    				batch);
    		return updateCounts;
    	}
    
    	// ... additional methods
    }
    

    對於使用“?”佔位符的SQL語句,你需要傳入帶有更新值的物件陣列。物件陣列每一項對應SQL語句中的一個佔位符,並且傳入順序需要和SQL語句中定義的順序保持一致。

    下面是使用經典JDBC“?”佔位符的例子:

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public int[] batchUpdate(final List<Actor> actors) {
    		List<Object[]> batch = new ArrayList<Object[]>();
    		for (Actor actor : actors) {
    			Object[] values = new Object[] {
    					actor.getFirstName(),
    					actor.getLastName(),
    					actor.getId()};
    			batch.add(values);
    		}
    		int[] updateCounts = jdbcTemplate.batchUpdate(
    				"update t_actor set first_name = ?, last_name = ? where id = ?",
    				batch);
    		return updateCounts;
    	}
    
    	// ... additional methods
    
    }
    

    上面所有的批量更新方法都返回一個數組,包含具體成功的行數。這個計數是由JDBC驅動返回的。如果拿不到計數。JDBC驅動會返回-2。

    15.4.3 多個批處理操作
    上面最後一個例子更新的批處理數量太大,最好能再分割成更小的塊。最簡單的方式就是你多次呼叫batchUpdate來實現,但是可以有更優的方法。要使用這個方法除了SQL語句,還需要傳入引數集合物件,每次Batch的更新數和一個ParameterizedPreparedStatementSetter去設定預編譯SQL語句的引數值。框架會迴圈呼叫提供的值並且將更新操作切割成指定數量的小批次。

    下面的例子設定了更新批次數量為100的批量更新操作:

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public int[][] batchUpdate(final Collection<Actor> actors) {
    		int[][] updateCounts = jdbcTemplate.batchUpdate(
    				"update t_actor set first_name = ?, last_name = ? where id = ?",
    				actors,
    				100,
    				new ParameterizedPreparedStatementSetter<Actor>() {
    					public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
    						ps.setString(1, argument.getFirstName());
    						ps.setString(2, argument.getLastName());
    						ps.setLong(3, argument.getId().longValue());
    					}
    				});
    		return updateCounts;
    	}
    
    	// ... additional methods
    
    }
    

    這個呼叫的批量更新方法返回一個包含int陣列的二維陣列,包含每次更新生效的行數。第一層陣列長度代表批處理執行的數量,第二層陣列長度代表每個批處理生效的更新數。每個批處理的更新數必須和所有批處理的大小匹配,除非是最後一次批處理可能小於這個數,具體依賴於更新物件的總數。每次更新語句生效的更新數由JDBC驅動提供。如果更新數量不存在,JDBC驅動會返回-2

    15.5 利用SimpleJdbc類簡化JDBC操作

    SimpleJdbcInsert類和SimpleJdbcCall類主要利用了JDBC驅動所提供的資料庫元資料的一些特性來簡化資料庫操作配置。這意味著可以在前端減少配置,當然你也可以覆蓋或是關閉底層的元資料處理,在程式碼裡面指定所有的細節。

    15.5.1 利用SimpleJdbcInsert插入資料
    讓我們首先看SimpleJdbcInsert類可提供的最小配置選項。你需要在資料訪問層初始化方法裡面初始化SimpleJdbcInsert類。在這個例子中,初始化方法是setDataSource。你不需要繼承SimpleJdbcInsert,只需要簡單的建立其例項同時呼叫withTableName設定資料庫名。

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
    	}
    
    	public void add(Actor actor) {
    		Map<String, Object> parameters = new HashMap<String, Object>(3);
    		parameters.put("id", actor.getId());
    		parameters.put("first_name", actor.getFirstName());
    		parameters.put("last_name", actor.getLastName());
    		insertActor.execute(parameters);
    	}
    
    	// ... additional methods
    }
    

    程式碼中的execute只傳入java.utils.Map作為唯一引數。需要注意的是Map裡面用到的Key必須和資料庫中表對應的列名一一匹配。這是因為我們需要按順序讀取元資料來構造實際的插入語句。

    15.5.2 使用SimpleJdbcInsert獲取自增Key
    接下來,我們對於同樣的插入語句,我們並不傳入id,而是通過資料庫自動獲取主鍵的方式來建立新的Actor物件並插入資料庫。 當我們建立SimpleJdbcInsert例項時, 我們不僅需要指定表名,同時我們通過usingGeneratedKeyColumns方法指定需要資料庫自動生成主鍵的列名。

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		Map<String, Object> parameters = new HashMap<String, Object>(2);
    		parameters.put("first_name", actor.getFirstName());
    		parameters.put("last_name", actor.getLastName());
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    }
    

    執行插入操作時第二種方式最大的區別是你不是在Map中指定ID,而是呼叫executeAndReturnKey方法。這個方法返回java.lang.Number物件,可以建立一個數值型別的例項用於我們的領域模型中。你不能僅僅依賴所有的資料庫都返回一個指定的Java類;java.lang.Number是你可以依賴的基礎類。如果你有多個自增列,或者自增的值是非數值型的,你可以使用executeAndReturnKeyHolder 方法返回的KeyHolder

    15.5.3 使用SimpleJdbcInsert指定列
    你可以在插入操作中使用usingColumns方法來指定特定的列名

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingColumns("first_name", "last_name")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		Map<String, Object> parameters = new HashMap<String, Object>(2);
    		parameters.put("first_name", actor.getFirstName());
    		parameters.put("last_name", actor.getLastName());
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    }
    

    這裡插入操作的執行和你依賴元資料決定更新哪個列的方式是一樣的。

    15.5.4 使用SqlParameterSource 提供引數值
    使用Map來指定引數值沒有問題,但不是最便捷的方法。Spring提供了一些SqlParameterSource介面的實現類來更方便的做這些操作。
    第一個是BeanPropertySqlParameterSource,如果你有一個JavaBean相容的類包含具體的值,使用這個類是很方便的。他會使用相關的Getter方法來獲取引數值。下面是一個例子:

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    
    }
    

    另外一個選擇是使用MapSqlParameterSource,類似於Map、但是提供了一個更便捷的addValue方法可以用來做鏈式操作。

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		SqlParameterSource parameters = new MapSqlParameterSource()
    				.addValue("first_name", actor.getFirstName())
    				.addValue("last_name", actor.getLastName());
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    
    }
    

    上面這些例子可以看出、配置是一樣的,區別只是切換了不同的提供引數的實現方式來執行呼叫。
    15.5.5 利用SimpleJdbcCall呼叫儲存過程

    SimpleJdbcCall利用資料庫元資料的特性來查詢傳入的引數和返回值,這樣你就不需要顯式去定義他們。如果你喜歡的話也以自己定義引數,尤其對於某些引數,你無法直接將他們對映到Java類上,例如ARRAY型別和STRUCT型別的引數。下面第一個例子展示了一個儲存過程,從一個MySQL資料庫返回Varchar和Date型別。這個儲存過程例子從指定的actor記錄中查詢返回first_name,last_name,和birth_date列。

    CREATE PROCEDURE read_actor (
    	IN in_id INTEGER,
    	OUT out_first_name VARCHAR(100),
    	OUT out_last_name VARCHAR(100),
    	OUT out_birth_date DATE)
    BEGIN
    	SELECT first_name, last_name, birth_date
    	INTO out_first_name, out_last_name, out_birth_date
    	FROM t_actor where id = in_id;
    END;
    

    in_id 引數包含你正在查詢的actor記錄的id.out引數返回從資料庫表讀取的資料

    SimpleJdbcCall 和SimpleJdbcInsert定義的方式比較類似。你需要在資料訪問層的初始化程式碼中初始化和配置該類。相比StoredProcedure類,你不需要建立一個子類並且不需要定義能夠在資料庫元資料中查詢到的引數。下面是一個使用上面儲存過程的SimpleJdbcCall配置例子。除了DataSource以外唯一的配置選項是儲存過程的名字

    public class JdbcActorDao implements ActorDao {
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcCall procReadActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.procReadActor = new SimpleJdbcCall(dataSource)
    				.withProcedureName("read_actor");
    	}
    
    	public Actor readActor(Long id) {
    		SqlParameterSource in = new MapSqlParameterSource()
    				.addValue("in_id", id);
    		Map out = procReadActor.execute(in);
    		Actor actor = new Actor();
    		actor.setId(id);
    		actor.setFirstName((String) out.get("out_first_name"));
    		actor.setLastName((String) out.get("out_last_name"));
    		actor.setBirthDate((Date) out.get("out_birth_date"));
    		return actor;
    	}
    
    	// ... additional methods
    
    }
    

    呼叫程式碼包括建立包含傳入引數的SqlParameterSource。這裡需要重視的是傳入引數值名字需要和儲存過程中定義的引數名稱相匹配。有一種場景不需要匹配、那就是你使用元資料去確定資料庫物件如何與儲存過程相關聯。在儲存過程原始碼中指定的並不一定是資料庫中儲存的格式。有些資料庫會把名字轉成大寫、而另外一些會使用小寫或者特定的格式。

    execute方法接受傳入引數,同時返回一個Map包含任意的返回引數,Map的Key是儲存過程中指定的名字。在這個例子中它們是out_first_name, out_last_name 和 out_birth_date

    execute 方法的最後一部分使用返回的資料建立Actor物件例項。再次需要強調的是Out引數的名字必須是儲存過程中定義的。結果Map中儲存的返回引數名必須和資料庫中的返回引數名(不同的資料庫可能會不一樣)相匹配,為了提高你程式碼的可重用性,你需要在查詢中區分大小寫,或者使用Spring裡面的LinkedCaseInsensitiveMap。如果使用LinkedCaseInsensitiveMap,你需要建立自己的JdbcTemplate並且將setResultsMapCaseInsensitive屬性設定為True。然後你將自定義的JdbcTemplate 傳入到SimpleJdbcCall的構造器中。下面是這種配置的一個例子:

    public class JdbcActorDao implements ActorDao {
    
    	private SimpleJdbcCall procReadActor;
    
    	public void setDataSource(DataSource dataSource) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		jdbcTemplate.setResultsMapCaseInsensitive(true);
    		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
    				.withProcedureName("read_actor");
    	}
    
    	// ... additional methods
    
    }
    

    通過這樣的配置,你就可以無需擔心返回引數值的大小寫問題。

    15.5.6 為SimpleJdbcCall顯式定義引數

    你已經瞭解如何通過元資料來簡化引數配置,但如果你需要的話也可以顯式指定引數。這樣做的方法是在建立SimpleJdbcCall類同時通過declareParameters方法進行配置,這個方式可以傳入一系列的SqlParameter。下面的章節會詳細描述如何定義一個SqlParameter

    備註:如果你使用的資料庫不是Spring支援的資料庫型別的話顯式定義就很有必要了。當前Spring支援以下資料庫的儲存過程元資料查詢能力:Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle, 和 Sybase. 我們同時對某些資料庫內建函式支援元資料特性:比如:MySQL、Microsoft SQL Server和Oracle。

    你可以選擇顯式定義一個、多個,或者所有引數。當你沒有顯式定義引數時元資料引數仍然會被使用。當你不想用元資料查詢引數功能、只想指定引數時,需要呼叫withoutProcedureColumnMetaDataAccess方法。假設你針對同一個資料函式定義了兩個或多個不同的呼叫方法簽名,在每一個給定的簽名中你需要使用useInParameterNames來指定傳入引數的名稱列表。下面是一個完全自定義的儲存過程呼叫例子

    public class JdbcActorDao implements ActorDao {
    
    	private SimpleJdbcCall procReadActor;
    
    	public void setDataSource(DataSource dataSource) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		jdbcTemplate.setResultsMapCaseInsensitive(true);
    		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
    				.withProcedureName("read_actor")
    				.withoutProcedureColumnMetaDataAccess()
    				.useInParameterNames("in_id")
    				.declareParameters(
    						new SqlParameter("in_id", Types.NUMERIC),
    						new SqlOutParameter("out_first_name", Types.VARCHAR),
    						new SqlOutParameter("out_last_name", Types.VARCHAR),
    						new SqlOutParameter("out_birth_date", Types.DATE)
    				);
    	}
    
    	// ... additional methods
    }
    

    兩個例子的執行結果是一樣的,區別是這個例子顯式指定了所有細節,而不是僅僅依賴於資料庫元資料。
    15.5.7 如何定義SqlParameters

    如何定義SimpleJdbc類和RDBMS操作類的引數,詳見15.6: “像Java物件那樣操作JDBC”,
    你需要使用SqlParameter或者是它的子類。通常需要在構造器中定義引數名和SQL型別。SQL型別使用java.sql.Types常量來定義。
    我們已經看到過類似於如下的定義:

    new SqlParameter("in_id", Types.NUMERIC),
    	new SqlOutParameter("out_first_name", Types.VARCHAR),
    

    上面第一行SqlParameter 定義了一個傳入引數。IN引數可以同時在儲存過程呼叫和SqlQuery查詢中使用,它的子類在下面的章節也有覆蓋。

    上面第二行SqlOutParameter定義了在一次儲存過程呼叫中使用的返回引數。還有一個SqlInOutParameter類,可以用於輸入輸出引數。也就是說,它既是一個傳入引數,也是一個返回值。

    備註:引數只有被定義成SqlParameter和SqlInOutParameter才可以提供輸入值。不像StoredProcedure類為了考慮向後相容允許定義為SqlOutParameter的引數可以提供輸入值

    對於輸入引數,除了名字和SQL型別,你可以定義數值區間或是自定義資料型別名。針對輸出引數,你可以使用RowMapper處理從REF遊標返回的行對映。另外一種選擇是定義SqlReturnType,可以針對返回值作自定義處理。

    15.5.8 使用SimpleJdbcCall呼叫內建儲存函式

    呼叫儲存函式幾乎和呼叫儲存過程的方式是一樣的,唯一的區別你提供的是函式名而不是儲存過程名。你可以使用withFunctionName方法作為配置的一部分表示我們想要呼叫一個函式,以及生成函式呼叫相關的字串。一個特殊的execute呼叫,executeFunction,用來指定這個函式並且返回一個指定型別的函式值,這意味著你不需要從結果Map獲取返回值。儲存過程也有一個名字為executeObject的便捷方法,但是隻要一個輸出引數。下面的例子基於一個名字為get_actor_name的儲存函式,返回actor的全名。下面是這個函式的Mysql原始碼:

    CREATE FUNCTION get_actor_name (in_id INTEGER)
    RETURNS VARCHAR(200) READS SQL DATA
    BEGIN
    	DECLARE out_name VARCHAR(200);
    	SELECT concat(first_name, ' ', last_name)
    		INTO out_name
    		FROM t_actor where id = in_id;
    	RETURN out_name;
    END;
    

    我們需要在初始方法中建立SimpleJdbcCall來呼叫這個函式

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcCall funcGetActorName;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		jdbcTemplate.setResultsMapCaseInsensitive(true);
    		this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
    				.withFunctionName("get_actor_name");
    	}
    
    	public String getActorName(Long id) {
    		SqlParameterSource in = new MapSqlParameterSource()
    				.addValue("in_id", id);
    		String name = funcGetActorName.executeFunction(String.class, in);
    		return name;
    	}
    
    	// ... additional methods
    
    }
    

    execute方法返回一個包含函式呼叫返回值的字串

    15.5.9 從SimpleJdbcCall返回ResultSet/REF遊標

    呼叫儲存過程或者函式返回結果集會相對棘手一點。一些資料庫會在JDBC結果處理中返回結果集,而另外一些資料庫則需要明確指定返回值的型別。兩種方式都需要迴圈迭代結果集做額外處理。通過SimpleJdbcCall,你可以使用returningResultSet方法,並定義一個RowMapper的實現類來處理特定的返回值。 當結果集在返回結果處理過程中沒有被定義名稱時,返回的結果集必須與定義的RowMapper的實現類指定的順序保持一致。 而指定的名字也會被用作返回結果集中的名稱。

    下面的例子使用了一個不包含輸入引數的儲存過程並且返回t_actor標的所有行。下面是這個儲存過程的Mysql原始碼:

    CREATE PROCEDURE read_all_actors()
    BEGIN
     SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
    END;
    

    呼叫這個儲存過程你需要定義RowMapper。因為我們定義的Map類遵循JavaBean規範,所以我們可以使用BeanPropertyRowMapper作為實現類。 通過將相應的class類作為引數傳入到newInstance方法中,我們可以建立這個實現類。

    public class JdbcActorDao implements ActorDao {
    
    	private SimpleJdbcCall procReadAllActors;
    
    	public void setDataSource(DataSource dataSource) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		jdbcTemplate.setResultsMapCaseInsensitive(true);
    		this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
    				.withProcedureName("read_all_actors")
    				.returningResultSet("actors",
    				BeanPropertyRowMapper.newInstance(Actor.class));
    	}
    
    	public List getActorsList() {
    		Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
    		return (List) m.get("actors");
    	}
    
    	// ... additional methods
    
    }
    

    execute呼叫傳入一個空Map,因為這裡不需要傳入任何引數。從結果Map中提取Actors列表,並且返回給呼叫者。

    15.6 像Java物件那樣操作JDBC

    org.springframework.jdbc.object包能讓你更加面向物件化的訪問資料庫。舉個例子,使用者可以執行查詢並返回一個list, 該list作為一個結果集將把從資料庫中取出的列資料對映到業務物件的屬性上。你也可以執行儲存過程,包括更新、刪除、插入語句。

    備註:許多Spring的開發者認為下面將描述的各種RDBMS操作類(StoredProcedure類除外)可以直接被JdbcTemplate代替; 相對於把一個查詢操作封裝成一個類而言,直接呼叫JdbcTemplate方法將更簡單而且更容易理解。但這僅僅是一種觀點而已, 如果你認為可以從直接使用RDBMS操作類中獲取一些額外的好處,你不妨根據自己的需要和喜好進行不同的選擇。

    15.6.1 SqlQuery

    SqlQuery類主要封裝了SQL查詢,本身可重用並且是執行緒安全的。子類必須實現newRowMapper方法,這個方法提供了一個RowMapper例項,用於在查詢執行返回時建立的結果集迭代過程中每一行對映並建立一個物件。SqlQuery類一般不會直接使用;因為MappingSqlQuery子類已經提供了一個更方便從列對映到Java類的實現。其他繼承SqlQuery的子類有MappingSqlQueryWithParameters和UpdatableSqlQuery。

    15.6.2 MappingSqlQuery
    MappingSqlQuery是一個可重用的查詢類,它的子類必須實現mapRow(..)方法,將結果集返回的每一行轉換成指定的物件型別。下面的例子展示了一個自定義的查詢例子,將t_actor關係表的資料對映成Actor類。

    public class ActorMappingQuery extends MappingSqlQuery<Actor> {
    
    	public ActorMappingQuery(DataSource ds) {
    		super(ds, "select id, first_name, last_name from t_actor where id = ?");
    		super.declareParameter(new SqlParameter("id", Types.INTEGER));
    		compile();
    	}
    
    	@Override
    	protected Actor mapRow(ResultSet rs, int rowNumber) 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;
    	}
    
    }
    

    這個類繼承了MappingSqlQuery,並且傳入Actor型別的泛型引數。這個自定義查詢類的建構函式將DataSource作為唯一的傳入引數。這個構造器中你呼叫父類的構造器,傳入DataSource以及相應的SQL引數。該SQL用於建立PreparedStatement,因此它可能包含任何在執行過程中傳入引數的佔位符。