1. 前言
本章介绍数据库事务的相关内容。我们知道,数据库事务与 JDBC 密切相关,在 Spring 框架中,tx 模块和 jdbc 模块是互相依赖的。这一情况是极为罕见的,因为 Spring 为了减少模块之间的耦合程度,尽量遵循单向依赖的原则。为了简化代码,我们将原来 jdbc 模块的内容整合到 tx 模块中,并用一节的篇幅来介绍 jdbc 模块中与事务有关的内容。
2. 数据库连接
2.1 ConnectionHolder
ConnectionHolder 封装了一个 JDBC 连接,事务管理器将该实例绑定到指定的数据源上。transactionActive 属性表示当前连接对象上是否存在事务。
public class ConnectionHolder {
//数据库连接
private Connection connection;
//是否事务存在的标记
private boolean transactionActive = false;
public ConnectionHolder(Connection connection) {
this.connection = connection;
}
}
2.2 DataSourceUtils
DataSourceUtils 工具类负责管理数据库连接,定义了获取连接和释放连接的方法。
getConnection方法:先尝试获取线程绑定的数据库连接,这一步骤与事务有关,暂未实现。然后从数据源中获取数据库连接。releaseConnection方法:先尝试关闭线程绑定的数据库连接,如果不存在,则关闭传入的数据库连接。
public class DataSourceUtils {
public static Connection getConnection(DataSource dataSource) throws SQLException {
//TODO 尝试获取线程绑定的连接
//从数据源中获取连接
return dataSource.getConnection();
}
public static void releaseConnection(Connection con, DataSource dataSource) {
if (con == null) {
return;
}
//TODO 尝试获取线程绑定的连接,并关闭
//关闭连接
con.close();
}
}
需要注意的是,数据库连接有两个来源,一是数据源 DataSource,二是缓存在事务中。由于我们尚未实现事务相关的功能,就目前而言,获取连接和释放连接都直接与数据源打交道。
3. JdbcTemplate
3.1 概述
Spring 对常用的数据库操作进行了封装,JdbcOperations 接口定义了大量方法。出于简化代码的考虑,我们只选取了三个比较常用的方法。
query方法:查询一组数据queryForObject方法:查询单条数据update方法:执行增删改操作
public interface JdbcOperations {
<T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args);
<T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args);
int update(String sql, Object... args);
}
RowMapper 接口的作用是将查询结果转换成一个实体类,row 代表数据表中的一行数据,也称做一条记录,可以映射成一个 Java Bean。
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
JdbcTemplate 实现了 JdbcOperations 接口,由于涉及到原生的 JDBC 操作,持有一个 DataSource 实例以获取数据库连接。
public class JdbcTemplate implements JdbcOperations{
//数据源
private DataSource dataSource;
public JdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}
}
3.2 查询操作
query 方法的作用是查询一组数据,我们对查询流程进行了简化处理,分为以下四步:
- 获取数据库连接
Connection和预处理语句PreparedStatement - 填充查询参数
- 调用
executeQuery方法,执行查询操作 - 对返回的数据进行处理,这里使用
RowMapper接口来处理结果集中的数据
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
List<T> list = new ArrayList<>();
Connection connection;
PreparedStatement statement = null;
ResultSet rs = null;
try {
//1. 获取数据库连接和预处理语句
connection = DataSourceUtils.getConnection(this.dataSource);
statement = connection.prepareStatement(sql);
//2. 填充查询参数
for (int i = 0; i < args.length; i++) {
statement.setObject(i +1, args[i]);
}
//3. 执行查询
rs = statement.executeQuery();
//4. 对查询结果进行处理
int n = 1;
while(rs.next()){
list.add(rowMapper.mapRow(rs, n++));
}
} finally {
release(rs, statement, connection);
}
return list;
}
queryForObject 方法用于查询单条数据,首先调用 query 方法获取查询结果,然后检查返回结果,如果记录数量不是 1 则抛出异常。
@Override
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
List<T> results = query(sql, rowMapper, args);
if (CollectionUtils.isEmpty(results)) {
throw new DataAccessException("查询单条数据,结果集为空");
}
if (results.size() > 1) {
throw new DataAccessException("期望的查询结果数量为1,实际为" + results.size());
}
return results.iterator().next();
}
3.3 修改操作
update 方法用于执行新增、修改、删除操作,主要由三步组成。
- 获取数据库连接和预处理语句
- 填充参数
- 调用
executeUpdate方法,执行增删改操作,返回受影响的行数
public int update(String sql, Object... args) {
Connection connection;
PreparedStatement statement = null;
try {
//获取数据库连接和预处理语句
connection = DataSourceUtils.getConnection(this.dataSource);
statement = connection.prepareStatement(sql);
//填充参数
for (int i = 0; i < args.length; i++) {
statement.setObject(i + 1, args[i]);
}
//执行修改操作
return statement.executeUpdate();
} finally {
release(null, statement);
}
}
4. 测试
4.1 准备工作
第一步,引入 mysql 数据库驱动和 Druid 数据源,pom.xml 文件的修改如下:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.24</version>
<scope>provided</scope>
</dependency>
需要注意的是,测试代码使用的数据库平台是 mysql 9.0 ,如果读者使用的是 mysql 5.x,请按如下方式修改依赖项。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.8</version>
<scope>provided</scope>
</dependency>
第二步,在 resources 目录中创建配置文件 jdbc.properties,内容如下:
#数据库地址
spring.datasource.url=jdbc:mysql:///springwheel?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
#用户名
spring.datasource.username=root
#密码
spring.datasource.password=root
#数据库驱动
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
如果读者使用的是 mysql 5.x,请修改 driverClassName 这项属性。
spring.datasource.driverClassName=com.mysql.jdbc.Driver
第三步,创建数据库和数据表 t_user,其中 id 为自增主键,phone 为唯一索引。建表语句如下:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_unique_phone`(`phone`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
第四步,创建配置类,首先通过 @PropertySource 注解加载配置文件,将数据库相关的配置项注入到相应字段上。然后通过单例方法创建 DataSource 和 JdbcTemplate 实例,最终得到一个可用的 JdbcTemplate 实例。此外,@PropertySource 注解负责扫描 Service 和 DAO 类,这些组件统一存放在 tx.test.common 目录下,详见下文。
//测试类,配置jdbc组件
@Configuration
@PropertySource("jdbc.properties")
@ComponentScan("tx.test.common")
public class DataSourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.type}")
private Class<? extends DataSource> type;
//创建数据源
@Bean
public DataSource dataSource(){
DataSource dataSource = this.type.newInstance();
DataBinder dataBinder = new DataBinder(dataSource);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("url", url);
pvs.addPropertyValue("username", username);
pvs.addPropertyValue("password", password);
pvs.addPropertyValue("driverClassName", driverClassName);
dataBinder.bind(pvs);
return dataSource;
}
//创建模板类
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
}
4.2 新增操作
准备一个实体类和 DAO 类,实体类比较简单,定义了三个属性。
//测试类,实体对象
public class User {
private int id;
private String name;
private String phone;
}
接下来是 DAO 类的实现。首先声明 @Repository 注解,确保可以通过组件扫描的方式加载。然后注入 JdbcTemplate 实例,接下来定义 save 方法来保存数据。(修改和删除操作只是 SQL 语句不同,此处不赘述,读者请自行测试)
//测试类
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void save(String name, String phone){
String sql = "INSERT INTO t_user SET name = ?, phone = ?";
jdbcTemplate.update(sql, name, phone);
}
}
测试方法比较简单,首先创建 AnnotationConfigApplicationContext 实例,完成 DataSource、JdbcTemplate、UserDao 等主要组件的加载,然后调用 UserDao 的 save 方法插入数据。
//测试方法
@Test
public void testInsert(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DataSourceConfig.class);
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.save("Tom", "12305");
System.out.println("新增测试");
}
打开数据表 t_user,可以看到数据已经添加进去了。
mysql> select * from t_user;
+----+-------+-------+
| id | name | phone |
+----+-------+-------+
| 1 | Tom | 12305 |
+----+-------+-------+
1 rows in set (0.00 sec)
4.3 查询操作
先来看测试类的修改。在 UserDao 类中定义一个 RowMapper 接口的匿名类,用于将查询得到的结果转换成 User 实体类。然后定义两个查询方法,其中 getAll 方法查询所有的用户,getByPhone 方法查询指定手机号的用户。
//测试类
@Repository
public class UserDao {
private static final RowMapper<User> ROW_MAPPER = new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getInt(1));
user.setName(rs.getString(2));
user.setPhone(rs.getString(3));
return user;
}
};
public List<User> getAll(){
String sql = "SELECT id, phone, name FROM t_user";
return this.jdbcTemplate.query(sql, ROW_MAPPER);
}
public User getByPhone(String phone){
String sql = "SELECT id, phone, name FROM t_user WHERE phone = ?";
return this.jdbcTemplate.queryForObject(sql, ROW_MAPPER, phone);
}
}
查询的测试方法与新增操作的差不多,只是最终调用的是 getAll 方法,获取数据表 t_user 中的所有数据。
//测试方法
@Test
public void testQuery(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DataSourceConfig.class);
UserDao userDao = context.getBean("userDao", UserDao.class);
List<User> list = userDao.getAll();
System.out.println("查询测试: " + list);
}
从测试结果可以看到,用户表中所有的数据都查询出来了。
查询测试: [User{id=1, name='Tom', phone='12305'}, User{id=2, name='Jerry', phone='12306'}]
5. 总结
由于数据库事务与 JDBC 密切相关,在深入研究事务的运行机制之前,我们有必要了解一下 Spring 对于 JDBC 是如何封装的。jdbc 原本是独立的模块,为了方便起见,我们将 jdbc 与 tx 模块合并到一起。由于 jdbc 模块不是本章重点,因此只实现一部分功能:
- 数据库连接的管理,包括获取和释放
Connection对象 JdbcTemplate模板类,包括增删改查的操作。其中queryXxx方法负责查询,update方法负责增删改。- 事务管理器与事务对象(本节不涉及)
6. 项目信息
新增修改一览,新增(12),修改(0)。
tx
├─ src
│ ├─ main
│ │ └─ java
│ │ └─ cn.stimd.spring
│ │ ├─ dao
│ │ │ └─ DataAccessException.java (+)
│ │ └─ jdbc
│ │ ├─ core
│ │ │ ├─ JdbcOperations.java (+)
│ │ │ ├─ JdbcTemplate.java (+)
│ │ │ └─ RowMapper.java (+)
│ │ └─ datasource
│ │ ├─ ConnectionHolder.java (+)
│ │ └─ DataSourceUtils.java (+)
│ └─ test
│ ├─ java
│ │ └─ tx
│ │ ├─ common
│ │ │ ├─ User.java (+)
│ │ │ └─ UserDao.java (+)
│ │ └─ jdbc
│ │ ├─ DataSourceConfig.java (+)
│ │ └─ JdbcTest.java (+)
│ └─ resources
│ └─ jdbc.properties (+)
└─ pom.xml (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。