spring 2.0 技術手冊第五章 - jdbc、交易支援

68
JDBC、交易支援 Spring DAO Spring JDBC API Spring JdbcTemplate JDBC API Spring Programmatic transaction management Declarative transaction management Spring IoC AOP JDBC Spring JDBC 5

Upload: justin-lin

Post on 18-Jul-2015

2.503 views

Category:

Technology


3 download

TRANSCRIPT

Page 1: Spring 2.0 技術手冊第五章 - JDBC、交易支援

JDBC、交易支援

對於資料庫存取的支援,Spring 提供了 DAO 框架,您可以不用接觸底層資料庫的技術細節。Spring 對資料庫存取時的一些例外加以重新封裝,您可以選擇是否處理特定的例外。在 JDBC API 的使用上,Spring 也提供了 JdbcTemplate 等類別以簡化 JDBC 在 API 上的操作使用。關於交易的處理方面, Spring 提供了編程式交易管理( Programmatic transaction management)與宣告式交易管理(Declarative transaction management)。 總而言之,結合 Spring IoC 容器與 AOP 框架所提供的功能,可以簡化 JDBC 的運用方式,並輕易獲得交易等相關服務的功能。在這個章節中,將以一些實際的例子來介紹 Spring 於 JDBC 存取以及交易上的一些支援。

5

Page 2: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��2

5.1 Spring持久層入門 Spring 提供了 DAO 框架,讓應用程式開發時無須耦合於特定資料庫技術,這個小節先來簡介 Spring 的 DAO 設計,並以實際的例子來入門

Spring持久層。

5.1.1 Spring的 DAO支持

Spring 的 DAO 框架讓您在進行資料庫存取時,無須接觸到所使用的特定資料庫技術細節,DAO的全名為 Data Access Object,在應用程式中,需要使用到資料存取時,是透過一個資料存取介面來操作,而實際上進行資料庫存取的物件要實作該介面,並在規範的方法之中,實作存取時的相關細節。 舉個 DAO的例子,假設應用程式中有個 User物件,在進行資料庫存取時(例如 select、insert、update、delete),應用程式不應依賴於一個實際的類別實作,而可以讓它依賴一個介面,例如一個 IUserDAO介面:

package onlyfun.caterpillar;

public interface IUserDAO {

public void insert(User user);

public User find(Integer id);

public void update(User user);

public void delete(User user);

}

Page 3: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��3

實際上進行資料庫存取的類別可以實作 IUserDAO介面,例如定義一個簡單的 UserDAO類別:

package onlyfun.caterpillar;

...

public class UserDAO implements IUserDAO {

private DataSource dataSource;

public void setDataSource(DataSource dataSource) {

this.dataSource = dataSource;

}

public void insert(User user) {

String name = user.getName();

int age = user.getAge().intValue();

Connection conn = null;

PreparedStatement stmt = null;

try {

conn = dataSource.getConnection();

stmt = conn.prepareStatement (

"INSERT INTO user (name,age) VALUES(?,?)");

stmt.setString(1, name);

stmt.setInt(2, age);

stmt.executeUpdate();

} catch (SQLException e) {

e.printStackTrace();

}

finally {

if(stmt != null) {

try {

stmt.close();

}

catch(SQLException e) {

e.printStackTrace();

}

}

if(conn != null) {

try {

conn.close();

}

Page 4: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��4

catch(SQLException e) {

e.printStackTrace();

}

}

}

}

public User find(Integer id) {

...

return user;

}

public void update(User user) {

...

}

public void delete(User user) {

...

}

} 您的應用程式主流程在進行資料存取時,可以使用 IUserDAO來宣告操作介面,例如:

...

User user = new User();

user.setName("caterpillar");

user.setAge(new Integer(30));

IUserDAO userDao = getUserDAO();

userDao.insert(user);

... 由於依賴於介面,所以可以隨時替換 IUserDAO 的實作類別,而IUserDAO 介面宣告的操作方法上,並沒有任何與底層資料庫存取的技術細節,Spring 的 DAO 框架正是基於這樣的基本原理,將應用程式與底層存取技術隔離開來:

Page 5: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

���

圖 5.1 DAO 運作示意圖 資料存取介面上只有與特定資料庫存取技術無關的方法(例如update、insert、delete 等),設計上依賴於介面,程式也易於測試,也不讓應用程式受限於只能使用某一資料庫技術。 在之前的示範程式中,對於實際的資料庫存取流程來說,有幾個步驟是固定的,例如取得 DataSource、取得 Connection、處理例外等,對於不同的資料庫技術,這些步驟大致上是相同的,只有少部份流程不同,就設計上而言,可以使用 Template-Callback 模式,將固定的流程撰寫於Template類別之中,而對於不同的一些細節步驟,則委由 Callback物件來完成,例如以下的程式片段中,非粗體字部份是固定的流程,對於其中有差異的部份,可以實作個別的 Callback物件來執行或傳回適當的物件:

...

Connection conn = null;

PreparedStatement stmt = null;

try {

conn = dataSource.getConnection();

stmt = callback.createPreparedStatement(conn);

stmt.executeUpdate();

} catch (SQLException e) {

e.printStackTrace();

}

finally {

if(stmtCallback != null) {

try {

stmt.close();

}

Page 6: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��6

catch(SQLException e) {

e.printStackTrace();

}

}

if(conn != null) {

try {

conn.close();

}

catch(SQLException e) {

e.printStackTrace();

}

}

}

Spring 就運用了 Template-Callback 模式,將固定的流程撰寫於Template 類別之中(例如 JdbcTemplate、HibernateTemplate 類別),而對於不同的一些細節步驟,則委託特定 DAO 支援物件來處理(可以自定義或由 Spring自動產生):

圖 5.2 DAO Template 與 DAO Support 在例外處理方面,Spring也提供與特定技術無關的例外處理體系,讓應用程式不會因處理特定例外(像是 SQLException、HibernateException),而耦合於某種資料庫或持久層技術。

Page 7: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��7

首先來瞭解例外處理, Java 的例外有 Checked exception 與Unchecked exception。Checked exception是編譯時期在語法上必須處理的例外,因為這些例外通常是可以預期發生的,編譯器要求一定要處理,因此必須在語法上以 try...catch加以處理;Unckecked exception則是執行時期例外( Runtime exception),在例外繼承架構上是 java.lang.

RuntimeException類別的子類別,通常是由於程式邏輯上的錯誤,在執行時期所引發的例外,例外真的發生時,可以選擇使用 try...catch來作處理,或是讓例外直接丟出至應用程式最上層處理(例如使用者圖形介面)。

Checked exception的立意本來是好的,對於這類例外發生時,希望的是程式設計人員可以加以處理至程式能回復正常運作,然而有時候,對於Checked exception往往無力回復至正常運作,當這類的例外在底層的資料庫存取發生時(例如無法取得連線),最好的處理方式就是不處理,讓例外傳播至上層應用程式,由上層應用程式捕捉以顯示相關訊息,讓使用者得知問題出在哪邊,而不是在底層的資料庫存取程式中,作一些無能為力的處理(例如記錄下無法連線的訊息?),然而面對 Checked exception,由於編譯器要求一定要處理,程式設計人員只好莫可奈何的撰寫一些例外處理語法來處理掉這些例外。 直接使用 throws在方法上宣告例外,讓例外可以向上層傳播也不是一個好的主意,另一方面,有些程式或框架會自行繼承相關的例外類別,包括一些相關的例外訊息,它們也會在定義介面時,於方法上聲明 throws某些類型的例外,然而如果在這些方法中發生了某些不是方法上聲明的例外(可能由於使用的底層技術不同而有這種情況,像是 JDBC 或是Hibernate),就無法將之 throw,只能自行撰寫一些 try..catch來暗自處理掉,如果想要讓這些例外丟出至上層,就要更多道的手續。

Spring 的 DAO 框架並不丟出與資料庫技術相關的例外,Spring 所有的例外都是 org.springframework.dao.DataAccessException 的子類

Page 8: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��8

別,一個與資料庫技術無關的通用例外,而且 DataAccessException 是RuntimeException的子類別,也就是說它是屬於 Unchecked exception,您不用被強迫使用 try...catch 來處理例外,而可以自己選擇要不要處理,在不處理例外的情況下,也可以很簡單的將例外傳播至上層的應用程式。 對於 JDBC存取,Spring將 SQLException等轉化為自己的 DAO例外物件, org.springframework.dao 套件下提供一致性的例外處理層次,DataAccessException 是這個層次的基礎類別,它繼承自 org.springframe-

work.core套件的 NestedRuntimeException,而 NestedRuntimeException繼承自 RuntimeException,對於一些例外,您可以選擇處理它,或者忽略它,由最上層的應用程式或是最後由 JVM來處理。 如果要處理特定的例外,Spring 也將例外作好了分類,例如資料庫連結錯誤時會丟出的 DataAccessResourceFailureException、SQL語法錯誤時的 BadSqlGrammarException 等,您可以針對想處理的例外加以捕捉。可以看看 Spring參考文件中的 DAO support章節,當中有一些對例外處理的說明,也有個 DataAccessException的類別繼承圖。

5.1.2 DataSource注入 對於不同的資料連接來源需求,Spring 提供了 javax.sql.DataSource注入,更換資料來源只要在 Bean 定義檔中修改配置,而不用修改任何一行程式。 因應不同的系統,應用程式可能使用不同的資料來源,例如純綷的使用 JDBC、透過連接池、或是透過 JNDI等,資料來源的更動是底層的行為,這些行為不應影響到上層的商務邏輯,為此,可以在需要取得連接來源的Bean 物件上保留一個 Datasource 注入的操作介面, Spring 提供org.springframework.jdbc.datasource.DriverManagerDataSource來取

Page 9: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

���

得它的實例,DriverManagerDataSource 實作了 javax.sql.DataSource,您可以在 Bean定義檔中這麼撰寫:

...

<beans...>

<bean id="dataSource"

class="org.springframework.jdbc.

→ datasource.DriverManagerDataSource">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

<property name="password" value="123456"/>

</bean>

...

</beans> 其中 "driverClassName"、"url"、"username"、"password" 四個屬性分別是用來設定 JDBC驅動程式類別、資料庫 URL協定、使用者名稱、密碼。 在這邊實際使用一個程式來作為 DataSource注入的示範,並示範之前有關於 DAO介紹的實作,假設您定義了一個 DAO的操作介面如下:

DataSourceDemo IUserDAO.java

package onlyfun.caterpillar;

public interface IUserDAO {

public void insert(User user);

public User find(Integer id);

} 基於篇幅的限制,這邊關於 DAO的介面定義只有 insert() 與 find() 兩個方法。在這個介面中所使用到的 User類別則如下定義:

Page 10: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��1�

DataSourceDemo User.java

package onlyfun.caterpillar;

public class User {

private Integer id;

private String name;

private Integer age;

public Integer getId() {

return id;

}

public void setId(Integer id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

} 接著可以定義一個 UserDAO 類別,它實作了 IUserDAO 介面,是實際進行資料庫存取服務的物件,如下所示:

Page 11: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��11

DataSourceDemo UserDAO.java

package onlyfun.caterpillar;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import javax.sql.DataSource;

public class UserDAO implements IUserDAO {

private DataSource dataSource;

public void setDataSource(DataSource dataSource) {

this.dataSource = dataSource;

}

public void insert(User user) {

String name = user.getName();

int age = user.getAge().intValue();

Connection conn = null;

PreparedStatement stmt = null;

try {

conn = dataSource.getConnection();

stmt = conn.prepareStatement (

"INSERT INTO user (name,age) VALUES(?,?)");

stmt.setString(1, name);

stmt.setInt(2, age);

stmt.executeUpdate();

} catch (SQLException e) {

e.printStackTrace();

}

finally {

if(stmt != null) {

try {

stmt.close();

}

catch(SQLException e) {

e.printStackTrace();

}

}

Page 12: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��12

if(conn != null) {

try {

conn.close();

}

catch(SQLException e) {

e.printStackTrace();

}

}

}

}

public User find(Integer id) {

Connection conn = null;

PreparedStatement stmt = null;

try {

conn = dataSource.getConnection();

stmt = conn.prepareStatement(

"SELECT * FROM user WHERE id=?");

stmt.setInt(1, id.intValue());

ResultSet result = stmt.executeQuery();

if(result.next()) {

Integer i = new Integer(result.getInt(1));

String name = result.getString(2);

Integer age = new Integer(result.getInt(3));

User user = new User();

user.setId(i);

user.setName(name);

user.setAge(age);

return user;

}

} catch (SQLException e) {

e.printStackTrace();

}

finally {

if(stmt != null) {

try {

stmt.close();

}

Page 13: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��13

catch(SQLException e) {

e.printStackTrace();

}

}

if(conn != null) {

try {

conn.close();

}

catch(SQLException e) {

e.printStackTrace();

}

}

}

return null;

}

}

UserDAO 類別上宣告一個 setDataSource() 方法,可以讓您注入DataSource的實例,可以在 Bean定義檔中進行依賴注入的定義,如下所示:

DataSourceDemo beans-config.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"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="dataSource"

class="org.springframework.jdbc.

→ datasource.DriverManagerDataSource">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

<property name="password" value="123456"/>

</bean>

Page 14: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��14

<bean id="userDAO"

class="onlyfun.caterpillar.UserDAO">

<property name="dataSource" ref="dataSource"/>

</bean>

</beans> 可以撰寫一個簡單的測試程式來操作 UserDAO 的實例,看看是否能進行資料的儲存與查詢:

DataSourceDemo SpringDAODemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringDAODemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

User user = new User();

user.setName("caterpillar");

user.setAge(new Integer(30));

IUserDAO userDAO =

(IUserDAO) context.getBean("userDAO");

userDAO.insert(user);

user = userDAO.find(new Integer(1));

System.out.println("name: " + user.getName());

}

}

Page 15: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��1�

在進行程式的測試之前,您要先開啟資料庫服務,並將 beans-

config.xml 中相關的驅動程式類別、資料庫 URL、使用者名稱與密碼等修改為您的設定,在這邊所使用的是 MySQL資料庫,而建立的 user 表格是使用以下的 SQL 來建立的:

CREATE TABLE user (

id INT(11) NOT NULL auto_increment PRIMARY KEY,

name VARCHAR(100) NOT NULL default '',

age INT

); 程式的執行結果會先在資料庫的 user表格中存入一筆資料,接著根據id值查詢出先前所存入的資料,最後結果會顯示 "name: caterpillar" 的文字。 您需要 spring-core.jar、spring-beans.jar、spring-context.jar、

spring-dao.jar與 spring-jdbc.jar這幾個檔案,如果您使用的是spring.jar,當中已經包括了相關 API,另外也需要相依的commons-logging.jar,當然,為了使用 JDBC,您必須要有 JDBC驅動程式的 jar 檔案。

5.1.3 DataSource置換

DriverManagerDataSource並沒有提供連接池的功能,只是用來作簡單的單機連接測試,並不適合使用於真正的專案之中。您可以使用 DBCP以獲得連接池的功能。如果使用 Spring,則置換 DataSource並不需要修改程式原始碼,只要修改 Bean定義檔就可以了,例如修改一下 DataSourceDemo專案中的 beans-config.xml如下:

Page 16: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��16

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="dataSource"

class="org.apache.commons.dbcp.BasicDataSource"

destroy-method="close">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

<property name="password" value="123456"/>

</bean>

<bean id="userDAO"

class="onlyfun.caterpillar.UserDAO">

<property name="dataSource" ref="dataSource"/>

</bean>

</beans> 現在所使用的是 org.apache.commons.dbcp.BasicDataSource 作為注入的 DataSource實例,為了使用 DBCP的功能,您需要在 Classpath路徑中設定 commons-dbcp.jar、commons-pool.jar與 commonscollec-

tions.jar,這些都可以在 Spring的相依版本中的 lib目錄下找到。 注意到在 dataSource上設定了 "destroy-method" 屬性,如此可以確保 BeanFactory在關閉時也一併關閉 BasicDataSource。 如果 Servlet容器提供了 JNDI(Java Naming and Directory Interface)的 DataSource,也可以簡單的換上這個 DataSource:

Page 17: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��17

...

<bean id="dataSource"

class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName" value="jdbc/demo"/>

</bean>

... 為了使用 org.springframework.indi.JndiObjectFactoryBean,您需要 spring-context.jar,"jndiName" 實際上要根據所設定的 JNDI查詢名稱,例如在 Tomcat中可以於 server.xml這麼設定:

...

<!—- 假設應用程式目錄是 JSP -->

<Context path="/JSP" docBase="JSP">

<!-- 使用資料庫名稱是 GUESTBOOK --> <Resource name="jdbc/demo" scope="Shareable"

type="javax.sql.DataSource"/>

<ResourceParams name="jdbc/demo">

<parameter>

<name>factory</name>

<value>

org.apache.commons.dbcp.BasicDataSourceFactory

</value>

</parameter>

<!-- DBCP database connection settings -->

<!-- JDBC URL -->

<parameter>

<name>url</name>

<value>jdbc:mysql://localhost/demo</value>

</parameter>

<!-- JDBC驅動程式 -->

<parameter>

<name>driverClassName</name>

<value>com.mysql.jdbc.Driver</value>

</parameter>

<!-- 資料庫使用者與密碼 -->

<parameter>

<name>username</name>

<value>caterpillar</value>

Page 18: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��18

</parameter>

<parameter>

<name>password</name>

<value>123456</value>

</parameter>

<!-- DBCP connection pooling options -->

<!-- 等待 Connection的時間,單位 ms,-1表不限制 -->

<parameter>

<name>maxWait</name>

<value>3000</value>

</parameter>

<!-- 連接池中最多可 idle的 Connection數,

也就是最少的 Connection數,0表不限制 -->

<parameter>

<name>maxIdle</name>

<value>10</value>

</parameter>

<!-- 連接池至多的 Connection數,0表示不限制 -->

<parameter>

<name>maxActive</name>

<value>100</value>

</parameter>

</ResourceParams>

</Context>

...

Page 19: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��1�

5.2 JDBC支援 在使用 JDBC時,總是要處理繁瑣的細節,例如 Connection、Statement的獲得、SQLException 的處理、Connection、Statement 的關閉等問題,Spring在 JDBC的使用上提供了幾個類別,可以簡化 JDBC使用時的流程。

5.2.1 使用 JdbcTemplate 在前一個小節中介紹的 DataSourceDemo專案中,UserDAO中直接使用 JDBC來實作 insert() 與 find() 方法,當中要處理 Connection的取得、Statement的建立、例外的處理、Statement的關閉、Connection的關閉等,對於一個基本的 JDBC存取,這些流程是大同小異的,每一次都必須作這樣的流程著實令人厭煩。

Spring 提供了 org.springframework.jdbc.core.JdbcTemplate 類別,它被設計為執行緒安全(Thread-safe),當中所提供的一些操作方法封裝了類似以上的流程,例如 DataSourceDemo專案中的 UserDAO類別可以簡單的使用 JdbcTemplate 來改寫,要建立 JdbcTemplate 的實例,必須要有一個 DataSource物件作為建構時的物件:

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); 來改寫一下 DataSourceDemo專案中 UserDAO的內容實作,可以看到使用 JdbcTemplate時,在程式的撰寫流程上會有什麼樣的改進:

Page 20: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��2�

JdbcTemplateDemo UserDAO.java

package onlyfun.caterpillar;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

public class UserDAO implements IUserDAO {

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

jdbcTemplate = new JdbcTemplate(dataSource);

}

public void insert(User user) {

String name = user.getName();

int age = user.getAge().intValue();

jdbcTemplate.update("INSERT INTO user (name,age) "

+ "VALUES('" + name + "'," + age + ")");

}

public User find(Integer id) {

List rows = jdbcTemplate.queryForList(

"SELECT * FROM user WHERE id=" + id.intValue());

Iterator it = rows.iterator();

if(it.hasNext()) {

Map userMap = (Map) it.next();

Integer i = new Integer(userMap.get("id").toString());

String name = userMap.get("name").toString();

Integer age =

new Integer(userMap.get("age").toString());

User user = new User();

user.setId(i);

user.setName(name);

user.setAge(age);

Page 21: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��21

return user;

}

return null;

}

} 只要改寫 UserDAO 就可以了,其它的程式與設定檔都不用變動,Spring的 JdbcTemplate一如其名稱所示的,主要是藉由 Template Method模式來實現 JDBC的處理流程封裝。 如果使用 Hibernate 之類的 ORM(Object-Relational Mapping)工具,則 find()方法中物件模型與關聯式模型的轉換還可以進一步的簡化。

Spring 的 JDBC 封裝等功能基本上可以獨立於 Spring 來使用,除了JdbcTemplate 之外,Spring 還提供了其它的 Template 類別,像是對Hibernate、JDO、iBatis等的 Template實現。在資料庫的交易處理方面,Spring提供了編程式與宣告式的交易管理功能,簡化了持久層程式的複雜度,並提供了更好的維護性。

5.2.2 JdbcTemplate執行與更新 您可以使用 JdbcTemplate的 execute() 方法執行 SQL陳述,例如:

jdbcTemplate.execute(

"CREATE TABLE USER (user_id integer, name varchar(100))"); 如果是 UPDATE或 INSERT,您可以使用 update() 方法,它有數個重載 ( Overload ) 版 本 , 例 如 接 受 實 作 org.springframework.jdbc.

Page 22: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��22

core.PreparedStatementCreator 介面的物件,PreparedStatementCreator 介面的定義如下:

package org.springframework.jdbc.core;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.SQLException;

public interface PreparedStatementCreator {

PreparedStatement createPreparedStatement(Connection con)

throws SQLException;

} 例如可以將 5.2.1的 JdbcTemplateDemo專案中 UserDAO的 insert() 方法改寫如下:

...

public void insert(User user) {

final String name = user.getName();

final int age = user.getAge().intValue();

jdbcTemplate.update(

new PreparedStatementCreator() {

public PreparedStatement createPreparedStatement(

Connection con) throws SQLException {

String sql =

"INSERT INTO user (name,age) VALUES(?,?)";

PreparedStatement ps =

con.prepareStatement(sql);

ps.setString(1, name);

ps.setInt(2, age);

return ps;

}

});

}

...

Page 23: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��23

在這個例子中,使用了 PreparedStatement 來準備 SQL,JdbcTemplate實現了 Template-Callback機制,update()方法中實現了 Template流程,而PreparedStatementCreator則實現了 Callback物件,在執行 JDBC的流程中,必要時會執行所定義的 callback 方法,也就是 createPreparedStatement()方法,傳回一個 PreparedStatement 物件,以完成 update()方法中的 Template流程。 與 PreparedStatementCreator互補的介面是 org.springframework.jdbc.

core.PreparedStatementSetter介面:

package org.springframework.jdbc.core;

import java.sql.PreparedStatement;

import java.sql.SQLException;

public interface PreparedStatementSetter {

void setValues(PreparedStatement ps) throws SQLException;

} 例如可以將 JdbcTemplateDemo專案中UserDAO的 insert() 方法改寫如下:

...

public void insert(User user) {

final String name = user.getName();

final int age = user.getAge().intValue();

jdbcTemplate.update(

"INSERT INTO user (name,age) VALUES(?,?)",

new PreparedStatementSetter() {

public void setValues(PreparedStatement ps)

throws SQLException {

ps.setString(1, name);

ps.setInt(2, age);

}

});

}

...

Page 24: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��24

JdbcTemplate 會自動建立 PreparedStatementCreator 的實例,以提供傳遞給 setValues() 方法的 PreparedStatement物件。 您也可以直接提供 SQL,就如 JdbcTemplateDemo 專案中所示範的UserDAO中的 insert() 方法:

...

public void insert(User user) {

String name = user.getName();

int age = user.getAge().intValue();

jdbcTemplate.update("INSERT INTO user (name,age) "

+ "VALUES('" + name + "'," + age + ")");

}

... 在直接下 SQL語句時,也可以使用 "?" 作為佔位字元,並使用物件陣列作為引數傳遞給 JdbcTemplate的 update() 方法,例如改寫 JdbcTemplate-

Demo專案中所示範的 UserDAO中的 insert() 方法:

...

public void insert(User user) {

jdbcTemplate.update(

"INSERT INTO user (name, age) VALUES(?,?)",

new Object[] {user.getName(), user.getAge()});

}

...

JdbcTemplate 會自動建立 PreparedStatementCreator 與 Prepared-

StatementSetter的實例,然而這些細節您不用理會,只要提供 SQL與引數就好了。 如果需要批次處理時,可以實作 org.springframework.jdbc.core.Batch-

PreparedStatementSetter介面:

Page 25: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��2�

package org.springframework.jdbc.core;

import java.sql.PreparedStatement;

import java.sql.SQLException;

public interface BatchPreparedStatementSetter {

void setValues(PreparedStatement ps,

int i) throws SQLException;

int getBatchSize();

} 例如可以在 JdbcTemplateDemo 專案中的 IUserDAO 介面及 UserDAO類別上增加一個 insertUsers() 方法的定義與實作,像是以下的內容:

...

public int[] insertUsers(final List users) {

String sql = "INSERT INTO user (name,age) VALUES(?,?)";

BatchPreparedStatementSetter setter =

new BatchPreparedStatementSetter() {

public void setValues(

PreparedStatement ps, int i) throws SQLException {

User user = (User) users.get(i);

ps.setString(1, user.getName());

ps.setInt(2, user.getAge().intValue());

}

public int getBatchSize() {

return users.size();

}

};

return jdbcTemplate.batchUpdate(sql, setter);

}

... 如果 JDBC驅動程式支援批次處理的話,則直接使用它的功能,如果不支援,則 Spring會自動一個一個處理更新以模擬批次處理。

Page 26: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��26

5.2.3 JdbcTemplate查詢 使用 JdbcTemplate 進行查詢時,可以使用 queryForXXX() 等方法,例如使用 queryForInt() 方法傳回 user表格中的資料筆數:

jdbcTemplate.queryForInt("SELECT COUNT(*) FROM user"); 也可以使用 queryForObject() 傳回一個查詢後的結果物件,例如傳回一個 String物件:

String name = (String) jdbcTemplate.queryForObject(

"SELECT name FROM USER WHERE id = ?",

new Object[] {id},

java.lang.String.class); 上面兩個例子傳回的都是單一筆資料,如果傳回多筆資料,則可以使用 queryForList() 方法,例如:

List rows = jdbcTemplate.queryForList(

"SELECT * FROM user WHERE id=" + id.intValue()); 傳回的 List中包括的是 Map物件,每個 Map物件代表查詢結果中的一筆資料,每筆資料包括多個欄位內容,要取得欄位中的值,要使用欄位名稱作為"鍵(Key)",例如:

...

Iterator it = rows.iterator();

while(it.hasNext()) {

Map userMap = (Map) it.next();

System.out.println(userMap.get("id"));

System.out.println(userMap.get("name"));

System.out.println(userMap.get("age"));

...

}

...

Page 27: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��27

您可以實作 org.springframework.jdbc.core.RowCallbackHandler 介面,在查詢到資料之後先作一些處理再傳回,例如修改一下 5.2.1 的JdbcTemplateDemo專案中 UserDAO的 find() 方法如下,在 RowCallback-

Handler 的 processRow()方法中實作簡單的 ORM(Object-Relational

Mapping)動作:

...

public User find(Integer id) {

final User user = new User();

jdbcTemplate.query(

"SELECT * FROM user WHERE id = ?",

new Object[] {id},

new RowCallbackHandler() {

public void processRow(ResultSet rs)

throws SQLException {

user.setId(new Integer(rs.getInt("id")));

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

user.setAge(new Integer(rs.getInt("age")));

}

});

return user;

}

... 如果一次要取回很多查詢結果的物件,則可以先實作 org.springframe-

work.jdbc.core.RowMapper介面,例如:

package onlyfun.caterpillar;

import java.sql.ResultSet;

import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

public class UserRowMapper implements RowMapper {

public Object mapRow(ResultSet rs,

Page 28: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��28

int rowNum) throws SQLException {

User user = new User();

user.setId(new Integer(rs.getInt("id")));

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

user.setAge(new Integer(rs.getInt("age")));

return user;

}

} 在 Spring 2.0 中,ResultReader 介面與其實作類別已經被移除,查詢方法現在直接接受 RowMapper實例,例如使用 queryForObject()方法時:

...

public User find(Integer id) {

User user = (User) jdbcTemplate.queryForObject(

"select * from user where id=?",

new Object[] {id},

new UserRowMapper());

return user;

}

... 傳回的結果已使用 UserRowMapper的定義,將之封裝為 User物件。以下是使用 query()方法時的一個示範:

List users = jdbcTemplate.query("select * from user",

new UserRowMapper());

for(int i = 0; i < users.size(); i++) {

User user = (User) users.get(i);

System.out.println("\tId:\t" + user.getId());

System.out.println("\tName:\t" + user.getName());

System.out.println("\tAge:\n" + user.getAge());

}

Page 29: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��2�

傳回的 List物件中,包括了從資料庫中查詢出來的結果,並已封裝為User類別的實例。

5.2.4 JdbcTemplate的 Lob支援 在 JDBC中可以使用 Clob(Character large object)與 Blob(Binary large

object),以分別針對文字檔案與二進位檔案進行儲存。Spring 中可以透過 JdbcTemplate 來處理 CLOB 與 BLOB,以避免處理特定資料庫(例如Oracle 9i)的 Clob、Blob差異問題。舉個例子來說,假設您的 MySQL資料庫表格如下:

CREATE TABLE test (

id INT AUTO_INCREMENT PRIMARY,

txt TEXT,

image BLOB

); 假設現在分別讀進一個文字檔案與二進位檔案,並想將之儲存至資料庫中,則可以使用 JdbcTemplate,例如:

final File binaryFile = new File("wish.jpg");

final File txtFile = new File("test.txt");

final InputStream is = new FileInputStream(binaryFile);

final Reader reader = new FileReader(txtFile);

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

final LobHandler lobHandler = new DefaultLobHandler();

jdbcTemplate.execute("INSERT INTO test (txt, image) VALUES(?, ?)",

new AbstractLobCreatingPreparedStatementCallback(lobHandler) {

protected void setValues(PreparedStatement pstmt,

LobCreator lobCreator) throws SQLException,DataAccessException {

lobCreator.setClobAsCharacterStream(

pstmt, 1, reader, (int) txtFile.length());

Page 30: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��3�

lobCreator.setBlobAsBinaryStream(

pstmt, 2, is, (int) binaryFile.length());

}

});

reader.close();

is.close(); 在建立 AbstractLobCreatingPreparedStatementCallback物件時,要傳遞一個 LobHandler 實例,對於 MySQL(MS SQL Server 或 Oracle

10g),這邊使用 DefaultLobHandler 即可,對於 Oracle 9i 特定的 LOB處理,可以使用 OracleLobHandler。在 setValues()方法實作中,使用LogCreator來分別設定 Blob與 Clob的來源串流,索引 1、2表示第一與第二個佔位字元'?'的位置,並指定讀取長度。 如果要從資料庫中將資料讀取出來,並另存為檔案,可以使用以下的程式:

final Writer writer = new FileWriter("test_bak.txt");

final OutputStream os = new FileOutputStream(new File(wish_bak.jpg"));

jdbcTemplate.query("SELECT txt,image FROM test WHERE id = ?",

new Object[] {new Integer(1)},

new AbstractLobStreamingResultSetExtractor() {

protected void streamData(ResultSet rs)

throws SQLException, IOException, DataAccessException {

FileCopyUtils.copy(

lobHandler.getClobAsCharacterStream(rs, 1), writer);

FileCopyUtils.copy(

lobHandler.getBlobAsBinaryStream(rs, 2), os);

}

});

writer.close();

os.close(); 在這邊使用 FileCopyUtils的 copy()方法,將 LogHandler取得的串流直接轉接給檔案輸出 FileWriter、FileOutputStream物件。

Page 31: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��31

5.2.5 以物件方式進行操作 就 JdbcTemplate 上的各個方法來看,它封裝了 JDBC 處理的細節,讓您不用接觸底層的資料庫技術,然而 JdbcTemplate 的各個方法仍須熟悉如何使用 SQL語法,如果想讓一些程式開發人員不用接觸到 SQL語法,或是重用某些 SQL,在 Spring中,您可以進一步建立可重用的操作物件,建立之後在進行資料庫設計時,將這些設計完成的可重用物件給開發人員使用,他們就無須接觸到 SQL 的細節,從他們的觀點來看,資料庫操作更接近物件導向的操作方式。

Spring提供 org.springframework.jdbc.object套件,讓您以更物件導向的方式來設計資料庫操作方面的程式,只要事先繼承或直接實例化相對應的類別並編譯,之後就可以重複利用這個實例,執行它的方法來進行資料庫相關操作。

org.springframework.jdbc.object.RdbmsOperation是個抽象類,代表RDBMS(Relational Database Management System)的查詢、更新、預存程序等操作,您可以使用其子類 org.springframework.jdbc.object.SqlUpdate、org.springframework.jdbc.object.MappingSqlQuery等類別,它們被設計為執行緒安全(Thread-safe),所以可以在多執行緒的環境下重複使用這些類別的實例。

SqlFunction用來執行 SQL Function,例如可以執行 COUNT(),然後回傳一個整數表示查詢到的資料筆數,下面是一個例子:

SqlFunction sf = new SqlFunction(

dataSource, "SELECT COUNT(*) from user");

sf.compile();

sf.run();

Page 32: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��32

RdbmsOperation 的子類別在設定好 DataSource、SQL 以及相關參數後,必須先執行 compile() 進行編譯,之後就可以重複使用這個實例,在設計時可以繼承 SqlFunction來封裝 SQL,例如修改 5.2.1中 JdbcTemplate-

Demo專案的內容,新增一個類別如下所示:

RdbmsDemo UserFunction.java

package onlyfun.caterpillar;

import javax.sql.DataSource;

import org.springframework.jdbc.object.SqlFunction;

public class UserFunction extends SqlFunction {

public UserFunction(DataSource dataSource) {

super(dataSource, "SELECT COUNT(*) from user");

compile();

}

}

SqlUpdate類別用來表示一個 SQL的更新操作,您也可以設定相關參數,設計時也可以繼承它來進行 SQL的封裝,例如:

RdbmsDemo UserUpdate.java

package onlyfun.caterpillar;

import java.sql.Types;

import javax.sql.DataSource;

import org.springframework.jdbc.object.SqlUpdate;

public class UserUpdate extends SqlUpdate {

public UserUpdate(DataSource dataSource) {

super(dataSource,

"INSERT INTO user (name,age) VALUES(?,?)");

int[] types = {Types.VARCHAR, Types.INTEGER};

Page 33: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��33

setTypes(types);

compile();

}

}

setTypes() 方法用來設定 SQL中 "?" 佔位字元所要插入的數據類型,之後執行 update() 方法時,可以僅指定物件陣列作為引數來進行查詢,每一個陣列值將實際取代 "?" 佔位字元,例如可以這麼使用:

...

SqlUpdate userUpdate = new UserUpdate(dataSource);

...

userUpdate.update(

new Object[] {user.getName(), user.getAge()});

SqlQuery類別表示一個 SQL查詢操作,但通常很少直接使用這個類別,因為查詢到資料之後,會作一些處理,例如封裝為 User類別的實例,可以使用它的子類別 org.springframework.jdbc.object.MappingSqlMapping 來進行這個動作,例如:

RdbmsDemo UserQuery.java

package onlyfun.caterpillar;

import java.sql.ResultSet;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.jdbc.object.MappingSqlQuery;

public class UserQuery extends MappingSqlQuery {

public UserQuery(DataSource dataSource) {

super(dataSource, "SELECT * FROM user");

compile();

}

protected Object mapRow(ResultSet rs,

Page 34: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��34

int rowNum) throws SQLException {

User user = new User();

user.setId(new Integer(rs.getInt("id")));

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

user.setAge(new Integer(rs.getInt("age")));

return user;

}

} 設計好這個類別之後,可以這麼使用它:

...

SqlQuery userQuery = new UserQuery(dataSource);

...

List users = userQuery.execute(); 執行完 execute() 方法後,實際上傳回的 List中會有 User類別的實例,當中並封裝了查詢後的每一筆資料。 配合以上的幾個實作,可以再改寫一下 IUserDAO 介面與 UserDAO實作,例如:

RdbmsDemo IUserDAO.java

package onlyfun.caterpillar;

import java.util.List;

public interface IUserDAO {

public void insert(User user);

public List allUser();

public int count();

}

Page 35: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��3�

假設您是程式開發人員,有其他的開發人員負責資料庫存取方面的程式,並提供了以上的 UserFunction、UserUpdate與 UserQuery類別,則您就不用關心實際的 SQL是如何撰寫,可以重用 UserFunction、UserUpdate與 UserQuery類別來設計您的 UserDAO類別:

RdbmsDemo UserDAO.java

package onlyfun.caterpillar;

import java.util.List;

import javax.sql.DataSource;

import org.springframework.jdbc.object.SqlFunction;

import org.springframework.jdbc.object.SqlQuery;

import org.springframework.jdbc.object.SqlUpdate;

public class UserDAO implements IUserDAO {

private SqlUpdate userUpdate;

private SqlQuery userQuery;

private SqlFunction userFunction;

public void setDataSource(DataSource dataSource) {

userUpdate = new UserUpdate(dataSource);

userQuery = new UserQuery(dataSource);

userFunction = new UserFunction(dataSource);

}

public void insert(User user) {

userUpdate.update(

new Object[] {user.getName(), user.getAge()});

}

public List allUser() {

return userQuery.execute();

}

public int count() {

return userFunction.run();

}

}

Page 36: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��36

將 SQL封裝重用之後,從 UserDAO程式撰寫的角度來看,完全是物件操作的方式來進行,配合程式的修改,可以寫個簡單的程式來看看運作是否正常:

RdbmsDemo SpringDAODemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

import java.util.List;

public class SpringDAODemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

User user = new User();

user.setName("just933");

user.setAge(new Integer(26));

IUserDAO userDAO =

(IUserDAO) context.getBean("userDAO");

userDAO.insert(user);

System.out.println("筆數: " + userDAO.count());

List list = userDAO.allUser();

for(int i = 0; i < list.size(); i++) {

User next = (User) list.get(i);

System.out.println("\n\tId:\t" + next.getId());

System.out.println("\tName:\t" + next.getName());

System.out.println("\tAge:\t" + next.getAge());

}

}

}

Page 37: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��37

為什麼 UserDAO 中的 UserUpdate、UserQuery、UserFunction等不使用 IoC 依賴注入呢?除了簡化範例實作之外,還有一個觀念:不要為了 IoC 而 IoC!每個物件不管三七二十一都要來個依賴注入的話,被注入依賴的物件上,將會有一大堆冗長的 Setter方法。建議的是,有兩個以上的物件需要注入相同的依賴時,才考慮使用 Spring IoC 容器來進行依賴注入。

5.2.6 DataFieldMaxValueIncrementer 在插入資料至資料庫中時,您會需要設定主鍵,主鍵的設置最好是與商務鍵值無關,在 Spring 中,您可以使用 org.springframework.jdbc.

support.incrementer.DataFieldMaxValueIncrementer 來為您產生主鍵值,DataFieldMaxValueIncrementer 有許多針對不同資料庫的實例,例如DB2SequenceMaxValueIncrementer、HsqlMaxValueIncrementer、MySQL-

MaxValueIncrementer、 OracleSequenceMaxValueIncrementer、 Postgre-

SQLSequenceMaxValueIncrementer,您可以操作它的 nextIntValue()、nextLongValue() 或 nextStringValue() 來產生下一個主鍵值。

5.2.7 Spring 2.0的 NamedParameterJdbcTemplate 在 Spring 2.0中新增了 NamedParameterJdbcTemplate類別,讓您在撰寫 JDBC的 SQL陳述時,不必使用佔位字元'?',而是使用實際的命名參數(Named parameter)來保留 SQL中會變動的資料部份,例如原本您是這麼撰寫一個查詢:

String sql = "SELECT * FROM user WHERE id=?";

List rows = jdbcTemplate.queryForList(sql,

new Object[] {id.intValue()});

Page 38: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��38

現在可以使用 NamedParameterJdbcTemplate改寫為以下的方式:

String sql = "SELECT * FROM user WHERE id=:userId";

SqlParameterSource namedParameters =

new MapSqlParameterSource("userId", id);

NamedParameterJdbcTemplate jdbcTemplate =

new NamedParameterJdbcTemplate(dataSource);

List rows = jdbcTemplate.queryForList(sql, namedParameters); 注意到命名參數 ":userId"實際值的指定方式,在這邊是使用SqlParameterSource,在建構時直接指定命名參數與實際值。您也可以使用 Map物件來指定多個命名參數的實際值,例如:

String sql = "INSERT INTO user (name,age) VALUES(:userName, :userAge)";

Map namedParameters = new HashMap();

namedParameters.put("userName", name);

namedParameters.put("userAge", age);

NamedParameterJdbcTemplate jdbcTemplate =

new NamedParameterJdbcTemplate(dataSource);

jdbcTemplate.update(sql, namedParameters); 您也可以根據一個 POJO(Plain Old Java Object)物件的值作為命名參數值的依據,例如:

String sql = "INSERT INTO user (name,age) VALUES(:name, :age)";

SqlParameterSource namedParameters =

new BeanPropertySqlParameterSource(user);

NamedParameterJdbcTemplate jdbcTemplate =

new NamedParameterJdbcTemplate(dataSource);

jdbcTemplate.update(sql, namedParameters); 其中"user"所參考的物件是 JdbcTemplateDemo專案中的 User實例。以下具體將 JdbcTemplateDemo 專案的 UserDAO 類別,改寫為使用NameParameterJdbcTemplate的方式:

Page 39: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��3�

NamedParameterDemo UserDAO.java

package onlyfun.caterpillar;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.namedparam

.BeanPropertySqlParameterSource;

import org.springframework.jdbc.core.namedparam

.MapSqlParameterSource;

import org.springframework.jdbc.core.namedparam

.NamedParameterJdbcTemplate;

import org.springframework.jdbc.core.namedparam.SqlParameterSource;

public class UserDAO implements IUserDAO {

private NamedParameterJdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);

}

public void insert(User user) {

String sql = "INSERT INTO user (name,age) VALUES(:name, :age)";

SqlParameterSource namedParameters =

new BeanPropertySqlParameterSource(user);

jdbcTemplate.update(sql, namedParameters);

}

public User find(Integer id) {

String sql = "SELECT * FROM user WHERE id=:userId";

SqlParameterSource namedParameters =

new MapSqlParameterSource("userId", id);

List rows = jdbcTemplate.queryForList(sql, namedParameters);

Iterator it = rows.iterator();

if(it.hasNext()) {

Map userMap = (Map) it.next();

Integer i = new Integer(userMap.get("id").toString());

Page 40: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��4�

String name = userMap.get("name").toString();

Integer age =

new Integer(userMap.get("age").toString());

User user = new User();

user.setId(i);

user.setName(name);

user.setAge(age);

return user;

}

return null;

}

}

5.2.8 Spring 2.0的 SimpleJdbcTemplate 如果您使用的 JDK 是 5.0 以上的版本,則可以利用 Spring 2.0 中SimpleJdbcTemplate所提供的泛型(Generic)功能,例如原來您可能是這麼查詢資料:

public User find(Integer id) {

String sql = "SELECT * FROM user WHERE id=?";

RowMapper mapper = new RowMapper() {

public Object mapRow(ResultSet rs, int rowNum)

throws SQLException {

User user = new User();

user.setId(new Integer(rs.getInt("id")));

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

user.setAge(new Integer(rs.getInt("age")));

return user;

}

};

JdbcTemplate simpleJdbcTemplate =

new JdbcTemplate(this.getDataSource());

Page 41: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��41

return (User) jdbcTemplate.queryForObject(sql, mapper, id);

} 若改用 SimpleJdbcTemplate,則可以如下撰寫:

public User find(Integer id) {

String sql = "SELECT * FROM user WHERE id=?";

ParameterizedRowMapper<User> mapper =

new ParameterizedRowMapper<User>() {

public User mapRow(ResultSet rs, int rowNum)

throws SQLException {

User user = new User();

user.setId(new Integer(rs.getInt("id")));

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

user.setAge(new Integer(rs.getInt("age")));

return user;

}

};

SimpleJdbcTemplate simpleJdbcTemplate =

new SimpleJdbcTemplate(this.getDataSource());

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

} 可以注意到,當中使用到泛型的功能,可以直接在 mapRow()方法之後傳回 User實例,而 SimpleJdbcTemplate的 queryForObject()方法也可以直接傳回查詢結果的 User 實例。以下將 JdbcTemplateDemo 專案的UserDAO改寫為使用 SimpleJdbcTemplate的方式:

SimpleJdbcTemplateDemo UserDAO.java

package onlyfun.caterpillar;

import java.sql.ResultSet;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;

Page 42: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��42

public class UserDAO implements IUserDAO {

private SimpleJdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

jdbcTemplate = new SimpleJdbcTemplate(dataSource);

}

public void insert(User user) {

String sql = "INSERT INTO user (name,age) VALUES(?, ?)";

String name = user.getName();

Integer age = user.getAge();

jdbcTemplate.update(sql, new Object[] {name, age});

}

public User find(Integer id) {

String sql = "SELECT * FROM user WHERE id=?";

ParameterizedRowMapper<User> mapper =

new ParameterizedRowMapper<User>() {

public User mapRow(ResultSet rs, int rowNum)

throws SQLException {

User user = new User();

user.setId(new Integer(rs.getInt("id")));

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

user.setAge(new Integer(rs.getInt("age")));

return user;

}

};

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

}

}

Page 43: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��43

5.3 JDBC交易管理 Spring提供編程式的交易管理(Programmatic transaction manage-

ment)與宣告式的交易管理(Declarative transaction management),為不同的交易實作提供了一致的編程模型,這節以 JDBC交易為例,介紹Spring的交易管理。

5.3.1 Spring對交易的支援 交易是一組原子(Atomic)操作的工作單元,以資料庫存取的實例來說,就是一組 SQL 指令,這一組 SQL 指令必須全部執行成功,若因為某個原因(例如其中一行 SQL有錯誤),則先前所有執行過的 SQL指令撤消。 舉個簡單的例子,一個客戶從 A銀行轉帳至 B銀行,要作的動作為從A銀行的帳戶扣款、在 B銀行的帳戶加上轉帳的金額,兩個動作必須成功,如果有一個動作失敗,則此次轉帳失敗。 交易還必須保持所參與資源的一致性(Consistent),例如在銀行帳戶的例子中,兩個帳戶的轉帳金額,B帳戶取款的金額不能大於 A帳戶存款的金額。每個交易彼此之間必須是隔離的(Isolated),例如在 A 帳戶中可能有兩筆交易同時進行存款與提款的動作,兩個交易基本上不需意識到彼此的存在。交易還必須是可持續的(Durable),在某一筆交易之後,這筆交易必須是被記錄下來的。 在這邊將介紹 JDBC如何使用交易管理。首先來看看交易的原子性實現,在 JDBC中,可以操作 Connection的 setAutoCommit() 方法,給定它false引數,在下達一連串的 SQL語句後,自行執行 Connection的 commit() 來送出變更,如果中間發生錯誤,則執行 rollback() 來撤消所有的執行,例如:

Page 44: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��44

try {

.....

connection.setAutoCommit(false);

.....

// 一連串 SQL操作

connection.commit();

} catch(SQLException) {

// 發生錯誤,撤消所有變更

connection.rollback();

} 在 Spring中對 JDBC的交易管理加以封裝,Spring交易管理的抽象其關鍵在於 org.springframework.transaction.PlatformTransactionManager介面的實現:

...

public interface PlatformTransactionManager {

TransactionStatus getTransaction(TransactionDefinition

definition) throws TransactionException;

void commit(TransactionStatus status)

throws TransactionException;

void rollback(TransactionStatus status)

throws TransactionException;

}

PlatformTransactionManager 介面有許多具體的交易實現類別,例如DataSourceTransactionManager、HibernateTransactionManager、JdoTrans-

actionManager、 JtaTransactionManager 等,藉由依賴於 PlatformTran-

sactionManager 介面及各種的技術實現,Spring 在交易管理上可以讓開發人員使用一致的編程模型,即使所使用的是不同的交易管理技術。

TransactionException是 Unchecked Exception。交易的失敗通常都是致命的錯誤,Spring不強迫您一定要處理,而讓您自行選擇是否要捕捉例外。

Page 45: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��4�

getTransaction() 方法根據一個 TransactionDefinition物件來回傳一個 TransactionStatus物件,TransactionDefinition介面的實例定義了交易的隔離程度(Isolation level)、傳播行為(Propagation behavior)、超時(Timeout)、唯讀(Read-only)等,TransactionStatus 代表著一個新的交易發起或已經存在的交易,您可以藉由它控制交易的執行或調查交易的狀態:

...

public interface TransactionStatus {

boolean isNewTransaction();

void setRollbackOnly();

boolean isRollbackOnly();

}

Spring提供編程式的交易管理(Programmatic transaction management)與宣告式的交易管理(Declarative transaction management):

� 編程式的交易管理 編程式的交易管理可以清楚的控制交易的邊界,也就是讓您自行實現交易何時開始、撤消操作的時機、交易何時結束等,可以實現細粒度的交易控制。

� 宣告式的交易管理 然而多數的情況下,交易並不需要細粒度的控制,採用宣告式的交易管理,好處是 Spring 交易管理的相關 API 可以不用介入程式之中,從物件的角度來看,並不知道它正被納入交易管理之中,在不需要交易管理的時候,只要在設定檔案上修改一下設定,即可移去交易管理服務。

Page 46: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��46

5.3.2 JDBC編程交易管理

Spring提供兩種方式實現編程式的交易管理,一是直接使用 Platform-

TransactionManager 實現,二是使用 org.springframework.transaction.

support.TransactionTemplate。 先來看看如何使用 PlatformTransactionManager,在這邊使用它的實現類別 DataSourceTransactionManager,可以改寫一下之前的 5.2.1 中JdbcTemplateDemo專案,讓它具有交易管理功能,修改一下 UserDAO類別的 insert() 方法來作示範:

ProgrammaticTransactionDemo UserDAO.java

package onlyfun.caterpillar;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.jdbc.

datasource.DataSourceTransactionManager;

import org.springframework.transaction.TransactionDefinition;

import org.springframework.transaction.TransactionStatus;

import org.springframework.transaction.

support.DefaultTransactionDefinition;

public class UserDAO implements IUserDAO {

private DataSourceTransactionManager transactionManager;

private DefaultTransactionDefinition def;

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

jdbcTemplate = new JdbcTemplate(dataSource);

transactionManager =

new DataSourceTransactionManager(dataSource);

// 建立交易的定義

Page 47: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��47

def = new DefaultTransactionDefinition();

def.setPropagationBehavior(

TransactionDefinition.PROPAGATION_REQUIRED);

}

public void insert(User user) {

String name = user.getName();

int age = user.getAge().intValue();

TransactionStatus status =

transactionManager.getTransaction(def);

try {

jdbcTemplate.update("INSERT INTO user (name,age) "

+ "VALUES('" + name + "'," + age + ")");

// 下面的 SQL 有錯誤,用以測試交易

jdbcTemplate.update("INSER INTO user (name,age) "

+ "VALUES('" + name + "'," + age + ")");

}

catch(DataAccessException e) {

transactionManager.rollback(status);

throw e;

}

transactionManager.commit(status);

}

public User find(Integer id) {

List rows = jdbcTemplate.queryForList(

"SELECT * FROM user WHERE id=" + id.intValue());

Iterator it = rows.iterator();

if(it.hasNext()) {

Map userMap = (Map) it.next();

Integer i = new Integer(

userMap.get("id").toString());

String name = userMap.get("name").toString();

Integer age = new Integer(

userMap.get("age").toString());

User user = new User();

user.setId(i);

user.setName(name);

Page 48: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��48

user.setAge(age);

return user;

}

return null;

}

} 在 insert() 方法中使用了 DataSourceTransactionManager 來進行交易管理,如果發生了例外,則 catch區塊中會進行交易的 Rollback,在 insert() 方法中故意寫入錯誤的 SQL(注意 INSERT方法少寫了一個 T),因此實際上資料並不會被儲存至資料庫中。 要使用 MySQL 資料庫進行交易處理,必須建立支援交易的表格類型,例如 InnoDB 的表格類型,這邊用來建立表格的 SQL 如下所示:

CREATE TABLE user (

id INT(11) NOT NULL auto_increment PRIMARY KEY,

name VARCHAR(100) NOT NULL default '',

age INT

) TYPE = InnoDB; 另一個實現編程式交易管理的方法是使用 TransactionTemplate,它需要一個 TransactionManager實例,如下所示:

...

TransactionTemplate transactionTemplate =

new TransactionTemplate(transactionManager);

...

transactionTemplate.execute(new TransactionCallback() {

public Object doInTransaction(TransactionStatus status) {

return jdbcTemplate.update("INSERT INTO user (name,age) "

+ "VALUES('" + name + "'," + age + ")");

Page 49: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��4�

}

}); 如果發生了例外,則會進行 Rollback,否則提交交易,如果沒有回傳值,則也可以使用 TransactionCallbackWithoutResult:

...

transactionTemplate.execute(

new TransactionCallbackWithoutResult() {

public void doInTransactionWithoutResult(

TransactionStatus status) {

. ...

}

});

5.3.3 JDBC宣告交易管理

Spring宣告式的交易管理依賴於它的 AOP框架來完成。使用宣告交易管理的好處是,交易管理不侵入您所開發的組件,具體來說,DAO物件不會意識到正在交易管理之中,事實上也應當如此,因為交易管理是屬於系統層面的服務,而不是商務邏輯的一部份,如果想要改變交易管理策略的話,也只需要在定義檔中重新組態即可。 舉個例子來說,可以將 5.2.1中 JdbcTemplateDemo專案修改一下,在不修改 UserDAO 類別的情況下,可以為它加入交易管理的服務,一個簡單的方法是使用 TransactionProxyFactoryBean,指定要介入的交易管理對象及其方法,這需要在定義檔案修改,如下所示:

DeclarativeTransactionDemo beans-config.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"

xsi:schemaLocation="http://www.springframework.org/schema/beans

Page 50: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

����

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="dataSource"

class="org.springframework.jdbc.

→ datasource.DriverManagerDataSource"

destroy-method="close">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

<property name="password" value="123456"/>

</bean>

<bean id="transactionManager"

class="org.springframework.jdbc.

→ datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="userDAO"

class="onlyfun.caterpillar.UserDAO">

<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="userDAOProxy"

class="org.springframework.transaction.

→ interceptor.TransactionProxyFactoryBean">

<property name="proxyInterfaces">

<list>

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

<property name="target" ref="userDAO"/>

<property name="transactionManager"

ref="transactionManager"/>

<property name="transactionAttributes">

<props>

<prop key="insert*">PROPAGATION_REQUIRED</prop>

</props>

</property>

</bean>

Page 51: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

���1

</beans>

TransactionProxyFactoryBean需要一個 TransactionManager,由於這邊使用 JDBC,所以使用 DataSourceTransactionManager,Transaction-

ProxyFactoryBean是個代理物件,"target" 屬性指定要代理的對象,交易管理會自動介入指定的方法前後,這邊是使用 "transactionAttributes" 屬性指定,"insert*" 表示指定方法名稱 insert開頭的都要納入交易管理,您也可以指定方法全名,如果在方法執行過程中發生錯誤,則所有先前的操作自動撤回,否則正常提交。

"insert*" 等方法上指定了 "PROPAGATION_REQUIRED",表示在目前的交易中執行操作,如果交易不存在就建立一個新的,相關的常數意義都可以在 API文件中 TransactionDefinition介面中找到。您可以加上多個交易定義,中間使用逗號 "," 區隔,例如可以加上唯讀,或者是指定某個例外發生時撤回操作:

PROPAGATION_REQUIRED,readOnly,-MyCheckedException

MyCheckedException 前面加上 "-" 時,表示發生指定例外時撤消操作,如果前面加上 "+",表示發生例外時立即提交。 由於"userDAO"被"userDAOProxy"代理了,所以要做的是取得"user-

DAOProxy",而不是"userDAO",例如:

DeclarativeTransactionDemo SpringDAODemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringDAODemo {

Page 52: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

���2

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

User user = new User();

user.setName("caterpillar");

user.setAge(new Integer(30));

IUserDAO userDAO =

(IUserDAO) context.getBean("userDAOProxy");

userDAO.insert(user);

user = userDAO.find(new Integer(1));

System.out.println("name: " + user.getName());

}

} 您也可以設定不同的 TransactionInterceptor來得到更多的管理細節,例如:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="dataSource"

class="org.springframework.jdbc.

→ datasource.DriverManagerDataSource"

destroy-method="close">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

<property name="password" value="123456"/>

Page 53: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

���3

</bean>

<bean id="transactionManager"

class="org.springframework.jdbc.

→ datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="userDAO"

class="onlyfun.caterpillar.UserDAO">

<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="transactionInterceptor"

class="org.springframework.transaction.

→ interceptor.TransactionInterceptor">

<property name="transactionManager" ref="transactionManager"/>

<property name="transactionAttributeSource"

value="onlyfun.caterpillar.UserDAO.insert*=

→ PROPAGATION_REQUIRED "/>

</bean>

<bean id="userDAOProxy"

class="org.springframework.aop.

→ framework.ProxyFactoryBean">

<property name="proxyInterfaces">

<list>

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

<property name="target" ref="userDAO"/>

<property name="interceptorNames">

<list>

<value>transactionInterceptor</value>

</list>

</property>

</bean>

</beans> 即使後來不再需要交易管理,則直接在 Bean定義檔中修改配置即可,而不用修改程式進行重新編譯等動作。

Page 54: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

���4

宣告交易管理是利用 Spring AOP 來達成,所以執行以上的程式時,請記得您的 Classpath 設定中必須包括 spring-aop.jar。

5.3.4 交易的屬性介紹

Spring使用 AOP來完成宣告式的交易管理,因而宣告式交易是以方法為邊界,Spring 的交易屬性(Transaction attribute)自然就在於描述交易應用至方法上的策略,在 Spring中交易屬性分作以下的幾個參數:

� 傳播行為(Propagation behavior) 傳播行為定義了交易應用於方法上之邊界(Boundaries),它告知何時該開始一個新的交易,或何時交易該被暫停,或者方法是否要在交易中進行。

Spring 定義了幾個傳播行為,可以在 TransactionDefinition 的 API 文件說明上找到相對應的常數與說明,以下列出幾個: 表 5.1 交易傳播行為說明 傳播行為 說明

PROPAGATION_MANDATORY 方法必須在一個現存的交易中進行,否則丟出例外

PROPAGATION_NESTED 在一個巢狀的交易中進行,如果不是的話,則同 PROPAGATION_REQUIRED

PROPAGATION_NEVER 指出不應在交易中進行,如果有的話就丟出例外

PROPAGATION_NOT_SUPPORTED 指出不應在交易中進行,如果有的話就暫停現存的交易

Page 55: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

����

傳播行為 說明

PROPAGATION_REQUIRED 支援現在的交易,如果沒有的話就建立一個新的交易

PROPAGATION_REQUIRES_NEW 建立一個新的交易,如果現存一個交易的話就暫停它

PROPAGATION_SUPPORTS 支援現在的交易,如果沒有的話就以非交易的方式執行 舉個例子來說,如果傳播行為被宣告為 PROPAGATION_REQUIRED,則交易的邊界在於第一個開始事務的方法呼叫及結束時,如果先前沒有交易被開始,則交易邊界即為目前的方法執行前後。又如果傳播行為被宣告為PROPAGATION_REQUIRES_NEW,則交易的邊界即為該方法執行的前後。

� 隔離層級(Isolation level) 在一個應用程式中,可能有多個交易同時在進行,這些交易應當彼此之間互相不知道另一個交易的存在,好比現在整個應用程式就只有一個交易存在,由於交易彼此之間獨立,若讀取的是同一個資料的話,就容易發生問題,例如: ▲ Dirty read 某個交易已更新一份資料,另一個交易在此時讀取了同一份資料,由於某些原因,前一個 Roll back 了操作,則後一個交易所讀取的資料就會是不正確的。 ▲ Non-repeatable read 在一個交易的兩次查詢之中資料不一致,這可能是因為兩次查詢過程中間插入了一個交易更新的原有的資料。

Page 56: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

���6

▲ Phantom read 在一個交易的兩次查詢中資料筆數不一致,例如有一個交易查詢了幾列(Row)資料,而另一個交易卻在此時插入了新的幾列數據,先前的交易在接下來的查詢中,就會發現有幾列數據是它先前所沒有的。 為了避免以上問題的方法之一,就是在某個交易進行過程中鎖定正在更新或查詢的資料欄位,直到目前的交易完成,然而完全的鎖定欄位時,另一個交易來進行查詢同一份資料時就必須等待,直到前一個交易完成並解除鎖定為止,因而會造成應用程式在查詢或更新資料時效能上的問題,而事實上根據需求的不同,並不用在交易進行時完全的鎖定資料,隔離層級讓您根據實際的需求,對資料的鎖定進行設置。

Spring 提供了幾種隔離層級設定,同樣的可以在 TransactionDefinition 的API 文件說明上找到相對應的常數與說明,以下列出幾個: 表 5.2 交易隔離層級說明 隔離層級 說明

ISOLATION_DEFAULT 使用底層資料庫預設的隔離層級

ISOLATION_READ_COMMITTED

允許交易讀取其它並行的交易已經送出(Commit)的資料欄位,可以防止 Dirty

read 問題

ISOLATION_READ_UNCOMMITTED

允許交易讀取其它並行的交易還沒送出的資料,會發生 Dirty、 Nonrepeatable、Phantom read 等問題

ISOLATION_REPEATABLE_READ

要求多次讀取的資料必須相同,除非交易本身更新資料,可防止 Dirty、Nonrepeatable

read 問題

ISOLATION_SERIALIZABLE

完整的隔離層級,可防止 Dirty、Nonre-

peatable、Phantom read 等問題,會鎖定對應的資料表格,因而有效能問題

Page 57: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

���7

� 唯讀提示(Read-only hints) 如果交易只進行讀取的動作,則可以利用底層資料庫在唯讀操作時的一些最佳化動作,由於這個動作利用到資料庫在唯讀的交易操作最佳化,因而必須在交易中才有效,也就是說要搭配傳播行為 PROPAGATION_

REQUIRED、 PROPAGATION_REQUIRES_NEW、 PROPAGATION_NESTED來設置。

� 交易超時期間(The transaction timeout period) 有的交易操作可能延續一段很長的時間,交易本身可能關聯到資料表格的鎖定,因而長時間的交易操作會有效能上的問題,對於過長的交易操作,您要考慮 Roll back 交易並要求重新操作,而不是無限時的等待交易完成。 您可以設置交易超時期間,計時是從交易開始時,所以這個設置必須搭配傳播行為 PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED 來設置。

5.3.5 TransactionAttributeSource、TransactionAttribute 在 TransactionProxyFactoryBean上有 setTransactionAttributeSource() 與 setTransactionAttributes() 方法,它們是用來設定交易屬性的策略實例。

org.springframework.transaction.interceptor.TransactionAttributeSource介面上有一個 getTransactionAttribute() 方法,您可以根據傳遞給它的Method 實例與 Class 實例,決定該回傳一個什麼內容的 org.springframe-

work.transaction.interceptor.TransactionAttribute 實例,一個最簡單的TransactionAttributeSource 實作是 org.springframework.transaction.inter-

ceptor.MatchAlwaysTransactionAttributeSource,對於每一個方法執行都會應用交易,它回傳的 TransactionAttribute 實例之預設傳播行為是PROPAGATION_REQUIRED,隔離層級為 ISOLATION_DEFAULE。

Page 58: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

���8

一個應用的例子如下所示:

...

<bean id="transactionAttributeSource"

class="org.springframework.transaction.interceptor.

→ MatchAlwaysTransactionAttributeSource"/>

<bean id="userDAOProxy"

class="org.springframework.transaction.

→ interceptor.TransactionProxyFactoryBean">

<property name="proxyInterfaces">

<list>

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

<property name="target" ref="userDAO"/>

<property name="transactionManager" ref="transactionManager"/>

<property name="transactionAttributeSource"

ref="transactionAttributeSource"/>

</bean>

... 您可以使用 org.springframework.transaction.interceptor.DefaultTran-

sactionAttribute,並設置自己的交易策略,之後設定給 TransactionAttri-

buteSource,例如:

...

<bean id="myTransactionAttribute"

class="org.springframework.transaction.

→ interceptor.DefaultTransactionAttribute">

<property name="propagationBehaviorName"

value="PROPAGATION_REQUIRES_NEW"/>

<property name="isolationLevelName"

value="ISOLATION_REPEATABLE_READ"/>

</bean>

<bean id="transactionAttributeSource"

class="org.springframework.transaction.

→ interceptor.MatchAlwaysTransactionAttributeSource">

Page 59: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

����

<property name="transactionAttribute"

ref="myTransactionAttribute"/>

</bean>

<bean id="userDAOProxy"

class="org.springframework.transaction.

→ interceptor.TransactionProxyFactoryBean">

<property name="proxyInterfaces">

<list>

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

<property name="target" ref="userDAO"/>

<property name="transactionManager" ref="transactionManager"/>

<property name="transactionAttributeSource"

ref="transactionAttributeSource"/>

</bean>

... 可 以 使 用 org.springframework.transaction.interceptor.NameMatch-

TransactionAttributeSource 來指定某些方法要應用交易,以及要應用的交易策略,例如:

...

<bean id="transactionAttributeSource"

class="org.springframework.transaction.

→ interceptor.NameMatchTransactionAttributeSource">

<property name="properties">

<props>

<prop key="insert*">PROPAGATION_REQUIRES_NEW</prop>

</props>

</property>

</bean>

<bean id="userDAOProxy"

class="org.springframework.transaction.

→ interceptor.TransactionProxyFactoryBean">

<property name="proxyInterfaces">

<list>

Page 60: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��6�

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

<property name="target" ref="userDAO"/>

<property name="transactionManager" ref="transactionManager"/>

<property name="transactionAttributeSource"

ref="transactionAttributeSource"/>

</bean>

... 在 NameMatchTransactionAttributeSource的 "properties" 屬性上,可以指定方法名稱與交易策略,方法名稱的指定可以指定全名,也可以使用Wildcard來指定,例如上面的指定中,只要方法名稱以 insert為開頭的都會應用相對應的交易策略。 在指定交易策略時,指定的格式如下: 傳播行為傳播行為傳播行為傳播行為,隔離層級隔離層級隔離層級隔離層級,唯讀唯讀唯讀唯讀,+例外例外例外例外, -例外例外例外例外 除了傳播行為一定要設置之外,其它都可選擇性的設置,中間以逗號區隔,例如:

PROPAGATION_REQUIRED,readOnly,-MyCheckedException

MyCheckedException 前面加上 "-" 時,表示發生指定例外時撤消操作,如果前面加上 "+",表示發生例外時立即提交。 在比較簡單的設置中,可以僅設置 TransactionProxyFactoryBean,並在它的 "transactionAttributes" 屬性上直接設置要應用交易的方法及交易策略,例如:

Page 61: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��61

...

<bean id="userDAOProxy"

class="org.springframework.transaction.

→ interceptor.TransactionProxyFactoryBean">

<property name="proxyInterfaces">

<list>

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

<property name="target" ref="userDAO"/>

<property name="transactionManager" ref="transactionManager"/>

<property name="transactionAttributes">

<props>

<prop key="insert*">PROPAGATION_REQUIRED</prop>

</props>

</property>

</bean>

... 甚至也可以直接指定 TransactionInterceptor,以獲得更多的控制, 例如:

...

<bean id="transactionInterceptor"

class="org.springframework.transaction.

→ interceptor.TransactionInterceptor">

<property name="transactionManager">

ref="transactionManager"/>

<property name="transactionAttributeSource"

value="onlyfun.caterpillar.UserDAO.insert*=

→ PROPAGATION_REQUIRED"/>

</bean>

<bean id="userDAOProxy"

class="org.springframework.aop.

→ framework.ProxyFactoryBean">

<property name="proxyInterfaces">

<list>

<value>onlyfun.caterpillar.IUserDAO</value>

</list>

</property>

Page 62: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��62

<property name="target" ref="userDAO"/>

<property name="interceptorNames" value="transactionInterceptor"/>

</bean>

... 選擇哪一種設定方式是需求的問題,您可以嘗試在 Declarative-

TransactionDemo專案的 Bean定義檔上設定以上所介紹的方式,基於篇幅的限制,以上僅列出部份的設定內容。

5.3.6 Spring 2.0宣告式交易管理:基於 XML Schmea 在 Spring 2.0 中要設定宣告式交易管理,可以依賴於 Spring 2.0 的<aop>與<tx>標籤,因而要記得加入相關的名稱空間宣告:

<?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:aop="http://www.springframework.org/schema/aop"

xmlns:tx="http://www.springframework.org/schema/tx"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd

http://www.springframework.org/schema/tx

http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

</beans> 交易是系統層面的服務,也就是一個 Aspect,其實具體來說就是一個Advice,您可以使用<tx:advice>標籤來提供這個 Advice,它需要設定一個 TransactionManager,並在當中使用<tx:attributes>設定交易相關屬性。

Page 63: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��63

可以將先前的 DeclarativeTransactionDemo 專案改寫,修改其beans-config.xml為使用<aop>與<tx>標籤的方式:

DeclarativeTransactionDemo2 beans-config.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:aop="http://www.springframework.org/schema/aop"

xmlns:tx="http://www.springframework.org/schema/tx"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd

http://www.springframework.org/schema/tx

http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

<bean id="dataSource"

class="org.springframework.jdbc.

→ datasource.DriverManagerDataSource"

destroy-method="close">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

<property name="password" value="123456"/>

</bean>

<bean id="transactionManager"

class="org.springframework.jdbc.

→ datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="userDAO"

class="onlyfun.caterpillar.UserDAO">

<property name="dataSource" ref="dataSource"/>

</bean>

<tx:advice id="txAdvice"

Page 64: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��64

transaction-manager="transactionManager">

<tx:attributes>

<tx:method name="insert*" propagation="REQUIRED"/>

<tx:method name="find*" read-only="true"/>

</tx:attributes>

</tx:advice>

<aop:config>

<aop:pointcut id="userDAOPointcut"

expression="execution(* onlyfun.caterpillar.IUserDAO.*(..))"/>

<aop:advisor advice-ref="txAdvice"

pointcut-ref="userDAOPointcut"/>

</aop:config>

</beans> 注意到當中,<tx:method>中的屬性設定,對於傳播行為、隔離層級、唯讀、超時、例外時撤回或提交,都有對應的"propagation"、"isolation"、"timeout"、"read-only"、"rollback-for"、"no-rollback-for"屬性可以設定,若不設定,"propagation"屬性預設是"REQUIRE","isolation"屬性預設是"DEFAULT"、"timeout"屬性預設是"-1"(單位是秒)、"read-only"屬性預設是"false"。 與先前介紹 Spring 2.0基於 XML Schema的 AOP設定相同,由於不再於設定檔中設定代理物件,所以直接取得"userDAO"實例進行操作即可。

5.3.7 Spring 2.0宣告式交易管理:基於 Annotation 宣告式交易管理在 Spring 2.0中,也支援使用 Annotation的標示方式,方法是使用@Transactional 來標示,例如可以將 DeclarativeTran-

sactionDemo 專案的 UserDAO 改寫,在上頭直接標示@Transactional,並設定相關屬性:

Page 65: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��6�

DeclarativeTransactionDemo3 UserDAO.java

package onlyfun.caterpillar;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

public class UserDAO implements IUserDAO {

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

jdbcTemplate = new JdbcTemplate(dataSource);

}

@Transactional(propagation = Propagation.REQUIRED)

public void insert(User user) {

String name = user.getName();

int age = user.getAge().intValue();

jdbcTemplate.update("INSERT INTO user (name,age) "

+ "VALUES('" + name + "'," + age + ")");

}

@Transactional(readOnly=true)

public User find(Integer id) {

List rows = jdbcTemplate.queryForList(

"SELECT * FROM user WHERE id=" + id.intValue());

Iterator it = rows.iterator();

if(it.hasNext()) {

Map userMap = (Map) it.next();

Integer i = new Integer(userMap.get("id").toString());

String name = userMap.get("name").toString();

Integer age =

Page 66: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��66

new Integer(userMap.get("age").toString());

User user = new User();

user.setId(i);

user.setName(name);

user.setAge(age);

return user;

}

return null;

}

} 在使用@Transactional 時,相關的屬性設定為 "propagation"、"isolation"、 "readOnly"、 "timeout"、 "rollbackFor"、 "noRollbackFor"等,而在 beans-config.xml中,則要使用<tx:annotation-driven>標籤,並指定 TransactionManager,例如:

DeclarativeTransactionDemo3 beans-config.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:tx="http://www.springframework.org/schema/tx"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd

http://www.springframework.org/schema/tx

http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

<bean id="dataSource"

class="org.springframework.jdbc.

→ datasource.DriverManagerDataSource"

destroy-method="close">

<property name="driverClassName"

value="com.mysql.jdbc.Driver"/>

<property name="url"

value="jdbc:mysql://localhost:3306/demo"/>

<property name="username" value="caterpillar"/>

Page 67: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Chapter 5 JDBC、交易支援

��67

<property name="password" value="123456"/>

</bean>

<bean id="transactionManager"

class="org.springframework.jdbc.

→ datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"/>

</bean>

<bean id="userDAO"

class="onlyfun.caterpillar.UserDAO">

<property name="dataSource" ref="dataSource"/>

</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>

</beans> 同樣的,由於不再於設定檔中設定代理物件,所以直接取得"userDAO"實例進行操作即可。

Page 68: Spring 2.0 技術手冊第五章 - JDBC、交易支援

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

��68

5.4 接下來的主題 這個小節所介紹的是 Spring 在持久層、JDBC、交易等議題的封裝與簡化,Spring提供了持久層一致性的設計模型,即使在其它持久層框架的整合上,Spring中結合這些框架來使用在操作都很類似,在下一個章節中,將來簡介一下 Hibernate的使用,以及看看如何在 Spring中整合 Hibernate。

圖 5.3 Spring 在持久層提供一致性的使用模型