问题: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;
/**
* 该类主要用于 JdbcTemplate的测试,使用前需要先导入宠物店的DB Schema
*/
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() {
/*
* 这里使用RowCallbackHandler匿名内部类来进行回调
* processRow方法会根据查询记录条数,循环向list中加载User对象
*/
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"));
/*
* 这行代码将导致插入主键重复数据,并且抛出一个Spring运行时异常
*/
userDao.createUser(INIT_USER);
}
}
接着给出该DAO的上下文配置,见例6.31。
例6.31:dao-context.xml
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
jdbcsupport/jdbc.properties
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
至此,一个基本的DAO编码和配置就完成了,然而有些小组的成员对DAO发生异常处理时事务如何回滚的问题产生了疑问,L将这个问题留到了下一小节中。
6.4.4 如何声明配置JDBC事务
顺着上一小节中留下的事务疑问,L将在本小节中给出解答,和传统的处理方式不同,Spring的事务配置非常灵活,它不需要强迫客户去更改程序代码,而只是使用一种外部声明配置的方式来给业务对象加载事务特性,于是L写下了这个事务配置文件,见例6.32。
例6.32:transaction-context.xml
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
有了事务声明后,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 = ?";
/**
* 调用超类构造函数,传入DataSource和SQL语句
* 该SQL语句的每一个参数需要通过declareParameter方法进行声明
* SqlParameter声明了参数对应的由java.sql.Types定义的JDBC类型
* compile()方法预建PreparedStatement
*/
public UserMappingQuery(DataSource ds) {
super(ds, RETRIEVE_USER_BY_NAME);
super.declareParameter(
new SqlParameter(Types.VARCHAR));
compile();
}
/**
* 覆写了mapRow()方法,ResultSet中的行集将被
* 转换成Java对象
*/
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
"http://www.springframework.org/dtd/spring-beans.dtd">
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
classpath:jdbc.properties
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
为了清晰地分离,L单独给出了一个RDBMS操作对象的配置,见例6.37。
例6.37:sql-operation.xml
"http://www.springframework.org/dtd/spring-beans.dtd">
class="jdbcsupport.rdbms.UserMappingQuery">
最后,给出客户测试代码,见例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属性列表中,添加如下配置:
回到例6.37的sql-operation.xml,添加如下的bean定义:
class="jdbcsupport.rdbms. UserSqlUpdate">
最后,回到例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));