問題:Spring如何支援JDBC 設計師L常常發現在項目中經常會出現冗長易錯的JDBC代碼,Spring為了改善這種情況,提供了一系列強大的API使用者簡化傳統JDBC的編碼。 6.4.1 什麼是JdbcTemplate Spring提供了一個JdbcTemplate模闆類,該類是一個助手類。它在JDBC API之上做一層小小的抽象封裝,大大降低了JDBC API使用冗繁和易錯,并且JdbcTemplate還保障了資源建立、釋放、關閉等易錯操作的正确執行,提供了處理JDBC異常的統一方式。 說明 在使用JdbcTemplate時,需要和一個實作javax.sql.DataSource接口的資料源對象進行合作。資料源可以看作是一個通用的連接配接工廠,它允許容器或架構在應用程式代碼中,隐藏連接配接池和事務的管理操作。 6.4.2 如何使用JdbcTemplate 這裡将給出一個簡單的示例,展示如何通過程式設計方式來使用JdbcTemplate,該例實作了JDBC最基本的CRUD操作(新增、查找、更新、删除)。其中的DB Schema是Spring寵物店的。下面首先給出一個用以測試的實體類User和jdbc資料源的屬性檔案,見例6.26和例6.27。 例6.26:User.java package jdbcsupport; public class User { private String userName; private String password; public User(String userName, String password) { this.userName = userName; this.password = password; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } } 例6.27:jdbc.properties jdbc.driverClassName = org.postgresql.Driver jdbc.url = jdbc:postgresql://127.0.0.1:5432/jpetstore jdbc.username = postgres jdbc.password = 1111 接着給出完整的JdbcTemplate測試類,見例6.28。 例6.28:JdbcTemplateTest.java package jdbcsupport; import java.io.FileInputStream; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.apache.commons.dbcp.BasicDataSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; public class JdbcTemplateTest { public static final String CREATE USER = "INSERT INTO SIGNON (USERNAME, PASSWORD) VALUES (?, ?)"; public static final String UPDATE_USER = "UPDATE SIGNON SET PASSWORD = ? WHERE USERNAME = ?"; public static final String DELETE_USER = "DELETE FROM SIGNON WHERE USERNAME = ?"; public static final String RETRIEVE_USER_COUNT = "SELECT COUNT(*) FROM SIGNON"; public static final String RETRIEVE_USER_BY_NAME = "SELECT * FROM SIGNON WHERE USERNAME = ?"; public static final String RETRIEVE_ALL_USER = "SELECT * FROM SIGNON"; public static final User INIT_USER1 = new User("j2ee", "j2ee"); public static final User TEST_USER1 = new User("L1", "1111"); public static final User TEST_USER2 = new User("L2", "2222"); public static final String STATMENT1 = "DELETE FROM signon"; public static final String STATMENT2 = "INSERT INTO signon VALUES ('j2ee','j2ee')"; public static final String STATMENT3 = "INSERT INTO signon VALUES ('ACID','ACID')"; private JdbcTemplate template; public void init() throws Exception { BasicDataSource ds = new BasicDataSource(); Properties prop = new Properties(); prop.load(new FileInputStream("jdbcsupport/jdbc.properties")); ds.setDriverClassName(prop.getProperty("jdbc.driverClassName")); ds.setUrl(prop.getProperty("jdbc.url")); ds.setUsername(prop.getProperty("jdbc.username")); ds.setPassword(prop.getProperty("jdbc.password")); template = new JdbcTemplate(ds); //批量更新,執行靜态SQL STATEMENT template.batchUpdate( new String[]{STATMENT1, STATMENT2, STATMENT3}); } public void testCreate() { //使用匿名内部類進行SQL語句的條件指派 template.update(CREATE_USER, new PreparedStatementSetter() { public void setValues(PreparedStatement ps) throws SQLException { ps.setString(1, TEST_USER1.getUserName()); ps.setString(2, TEST_USER1.getPassword()); } }); //通過對象數組,對SQL語句條件進行指派 template.update(CREATE_USER, new String[]{TEST_USER2.getUserName(), TEST_USER2.getPassword()}); } public void testUpdate() { template.update(UPDATE_USER, new String[]{"5555", TEST_USER2.getUserName()}); } public void testDelete() { template.update(DELETE_USER, new String[]{TEST_USER1.getUserName()}); } public void testRetrieveBasicType() { template.queryForInt(RETRIEVE_USER_COUNT); } public void testRetrieveObject() { //通過RowMapper進行對象的手工映射 template.queryForObject(RETRIEVE_USER_BY_NAME, new String[]{INIT_USER1.getUserName()}, new RowMapper() { public Object mapRow(ResultSet rs, int rowNum) throws SQLException { return new User(rs.getString(1), rs.getString(2)); } }); } public void testRetrieveList() { final List list = new ArrayList(); template.query(RETRIEVE_ALL_USER, new RowCallbackHandler() { public void processRow(ResultSet rs) throws SQLException { User user = new User(rs.getString(1), rs.getString(2)); list.add(user); } }); } public static void main(String[] args) { JdbcTemplateTest test = new JdbcTemplateTest(); test.init(); test.testCreate(); test.testUpdate(); test.testDelete(); test.testRetrieveBasicType(); test.testRetrieveObject(); test.testRetrieveList(); } } 6.4.3 如何使用JdbcDaoSupport 設計師L發現,除了支援基本JDBC操作的JdbcTemplate,Spring還提供了一個JdbcDaoSupport類,它是JDBC資料通路對象(DAO)的抽象基類。本小節将展示JdbcDaoSupport的使用方法。L依然沿用上小節中的User類,首先為User類專門撰寫一個UserDao類,使該DAO繼承JdbcDaoSupport類,見例6.29。 例6.29:UserDao.java package jdbcsupport; public class UserDao extends JdbcDaoSupport { public static final String CREATE_USER = "INSERT INTO SIGNON (USERNAME, PASSWORD) VALUES (?, ?)"; public static final String UPDATE_USER = "UPDATE SIGNON SET PASSWORD = ? WHERE USERNAME = ?"; public void createUser(final User user) { getJdbcTemplate().update(CREATE_USER, new PreparedStatementSetter() { public void setValues(PreparedStatement ps) throws SQLException { ps.setString(1, user.getUserName()); ps.setString(2, user.getPassword()); } }); } public void updateUser(final User user) { getJdbcTemplate().update(UPDATE_USER, new String[]{user.getPassword(), user.getUserName()}); } 為了使該示例更加真實,L又撰寫了一個業務對象,Spring可以向該業務對象注入DAO執行個體,見例6.30。 例6.30:BusinessObject.java package jdbcsupport; public class BusinessObject { private UserDao userDao; public static final User TEST_USER = new User("L", "1111"); public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void errorCreateAndUpdateUser() { userDao.createUser(TEST_USER); userDao.updateUser( new User(TEST_USER.getUserName(), "2222")); userDao.createUser(INIT_USER); } } 接着給出該DAO的上下文配置,見例6.31。 例6.31:dao-context.xml <beans> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value> jdbcsupport/jdbc.properties</value> </list> </property> </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> <bean id="userDao" class="jdbcsupport.UserDao"> <property name="dataSource"> <ref local="dataSource"/> </property> </bean> </beans> 至此,一個基本的DAO編碼和配置就完成了,然而有些小組的成員對DAO發生異常處理時事務如何復原的問題産生了疑問,L将這個問題留到了下一小節中。 6.4.4 如何聲明配置JDBC事務 順着上一小節中留下的事務疑問,L将在本小節中給出解答,和傳統的處理方式不同,Spring的事務配置非常靈活,它不需要強迫客戶去更改程式代碼,而隻是使用一種外部聲明配置的方式來給業務對象加載事務特性,于是L寫下了這個事務配置檔案,見例6.32。 例6.32:transaction-context.xml <beans> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" > <ref bean="dataSource" /> </property> </bean> <bean id="businessBean" class="jdbcsupport.BusinessObject"> <property name="userDao"> <ref bean="userDao"/> </property> </bean> <bean id="businessProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref bean="transactionManager" /> </property> <property name="target"> <ref bean="businessBean" /> </property> <property name="transactionAttributes"> <props> <prop key="errorCreateAndUpdate*">PROPAGATION_REQUIRED</prop> </props> </property> </bean> </beans> 有了事務聲明後,L給出了最後的測試代碼,如果調用了業務對象上的方法errorCreateAndUpdateUser (),由于它被聲明為具有事務處理能力的方法。是以執行它一旦産生異常,将導緻事務復原,抛出一個Spring運作時異常,見例6.33。 例6.33:BusinessObjectTest.java package jdbcsupport; public class BusinessObjectTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( new String[]{"daosupport/transaction-context.xml", "daosupport/dao-context.xml"}); BusinessObject bo = (BusinessObject)ctx.getBean("businessBeanProxy"); try { bo.errorCreateAndUpdateUser(); } catch (Exception e) { System.out.println(e); } } } 6.4.5 什麼是RDBMS Operation 設計師L在研究了JdbcTemplate和JdbcSupport之後,又發現了一些Spring自身提供的一些非常有趣實用的JDBC抽象輔助API類階層,這些類階層的基類是一個名為RdbmsOperation的類。通過研究,L發現這些類其實是對RDBMS查詢、更新甚至存儲過程的抽象,Spring将這些RDBMS操作過程單獨地抽象成對象的形式。L仔細想了一下,終于發現了RdbmsOperation類階層的優點:它們為RDBMS操作的重用提供了可能,由于這些RDBMS操作對象執行個體隻需要被建立一次,是以在程式中提升了性能。通常,這些執行個體會被儲存在DAO的執行個體變量中,它們都經過了預編譯并且是線程安全的。L将在下文中對其中的兩個類進行介紹,它們分别是MappingSqlQuery和SqlUpdate。 6.4.6 如何使用MappingSqlQuery MappingSqlQuery是一個查詢對象,它的子類需要實作mapRow(ResultSet, int) 方法,該方法會将JDBC ResultSet行集轉換成Java對象。接着,L将依然改寫上文中的UserDao以展示MappingSqlQuery的使用,見例6.34。 例6.34:UserDao.java package jdbcsupport.rdbms; import java.util.List; import org.springframework.jdbc.object.MappingSqlQuery; import org.springframework.jdbc.object.SqlUpdate; import jdbcsupport.User; public class UserDao { private MappingSqlQuery sqlQuery; public void setMappingSqlQuery(MappingSqlQuery sqlQuery) { this.sqlQuery = sqlQuery; } public User retrieveUserByName(String name) { List userList = sqlQuery.execute(new String[]{name}); return (userList != null) ? (User)userList.get(0) : null; } } 可以發現,該DAO中沒有任何SQL語句,取而代之的是一個SQL對象,該對象是一個MappingSqlQuery類的執行個體,見例6.35。 例6.35:UserMappingQuery.java package jdbcsupport.rdbms; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import javax.sql.DataSource; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.object.MappingSqlQuery; import jdbcsupport.User; public class UserMappingQuery extends MappingSqlQuery { public static final String RETRIEVE_USER_BY_NAME = "SELECT * FROM SIGNON WHERE USERNAME = ?"; public UserMappingQuery(DataSource ds) { super(ds, RETRIEVE_USER_BY_NAME); super.declareParameter( new SqlParameter(Types.VARCHAR)); compile(); } protected Object mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(rs.getString(1), rs.getString(2)); return user; } } 接着給出相關的Spring配置檔案,首先是DAO的上下文配置,見例6.36。 例6.36:dao-context.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:jdbc.properties</value> </list> </property> </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> <bean id="userDao" class="jdbcsupport.rdbms.UserDao"> <property name="mappingSqlQuery"> <ref bean="userQuery"/> </property> </bean> </beans> 為了清晰地分離,L單獨給出了一個RDBMS操作對象的配置,見例6.37。 例6.37:sql-operation.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="userQuery" class="jdbcsupport.rdbms.UserMappingQuery"> <constructor-arg> <ref bean="dataSource"/> </constructor-arg> </bean> </beans> 最後,給出客戶測試代碼,見例6.38。 例6.38:RdbmsOpTest.java package jdbcsupport.rdbms; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import jdbcsupport.User; //該類main函數運作前,需要先導入寵物店的DB Schema public class RdbmsOpTest { private static final User INIT_USER = new User("j2ee", "j2ee"); public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( new String[]{"jdbcsupport/rdbms/dao-context.xml", "jdbcsupport/rdbms/sql-operation.xml"}); UserDao dao = (UserDao)ctx.getBean("userDao"); User user = dao.retrieveUserByName(INIT_USER.getUserName()); System.out.println(user.getUserName().equals(INIT_USER.getUserName())); } } 6.4.7 如何使用SqlUpdate 在了解了查詢對象MappingSqlQuery後,本小節将展示SQL更新對象SqlUpdate的使用方法。回到例6.34的UserDao,L向其中添加了相應的更新方法,見例6.39。 例6.39:UserDao.java … public class UserDao { … private SqlUpdate sqlUpdate; public void setSqlUpdate(SqlUpdate sqlUpdate) { this.sqlUpdate = sqlUpdate; } public void updateUser(User user) { sqlUpdate.update(user.getPassword(), user.getUserName()); } } 接着給出SqlUpdate的實作類,見例6.40。 例6.40:UserSqlUpdate.java package jdbcsupport.rdbms; import java.sql.Types; import javax.sql.DataSource; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.object.SqlUpdate; public class UserSqlUpdate extends SqlUpdate { public static final String UPDATE_USER = "UPDATE SIGNON SET PASSWORD = ? WHERE USERNAME = ?"; public UserSqlUpdate(DataSource ds) { super(ds, UPDATE_USER); super.declareParameter(new SqlParameter(Types.VARCHAR)); super.declareParameter(new SqlParameter(Types.VARCHAR)); compile(); } } 同樣,回到例6.36的dao-context.xml,在id為userDao的Bean屬性清單中,添加如下配置: <property name="sqlUpdate"> <ref bean="userUpdate"/> </property> 回到例6.37的sql-operation.xml,添加如下的bean定義: <bean id="userUpdate" class="jdbcsupport.rdbms. UserSqlUpdate"> <constructor-arg> <ref bean="dataSource"/> </constructor-arg> </bean> 最後,回到例6.38的RdbmsOpTest、Java的main函數中,添加如下測試代碼: String timeSave = String.valueOf(System.currentTimeMillis()); dao.updateUser( new User(INIT_USER.getUserName(), timeSave)); User user = dao.retrieveUserByName(INIT_USER.getUserName()); System.out.println(user.getPassword(), timeSave));