Spring(六):事务

spring 框架提供了能够方便操作数据库的 JdbcTemplate 类,以及简化事务开发的声明式事务。

JdbcTemplate

JdbcTemplate 是 Spring 对 JDBC 进行封装的结果,使用 JdbcTemplate 方便实现对数据库操作。

准备工作

  1. 创建数据库

  2. 创建子模块

    CREATE DATABASE `spring`;
    
    use `spring`;
    
    CREATE TABLE `t_emp` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(20) DEFAULT NULL COMMENT '姓名',
      `age` int(11) DEFAULT NULL COMMENT '年龄',
      `sex` varchar(2) DEFAULT NULL COMMENT '性别',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
  3. 引入依赖

        <dependencies>
            <!--spring jdbc  Spring 持久化层支持jar包-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>6.0.6</version>
            </dependency>
            <!-- MySQL驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.32</version>
            </dependency>
            <!-- 数据源 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.16</version>
            </dependency>
        </dependencies>
    
  4. 创建 jdbc.properties

    jdbc.user=root
    jdbc.password=root
    jdbc.url=jdbc:mysql://localhost:3306/spring?serverTimezone=UTC
    jdbc.driver=com.mysql.cj.jdbc.Driver
    
  5. 创建 Spring 的配置文件 bean.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">
        <!-- 导入外部属性文件 -->
        <context:property-placeholder location="classpath:jdbc.properties"/>
    
        <!-- 配置数据源 -->
        <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="url" value="${jdbc.url}"/>
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="username" value="${jdbc.user}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
    
        <!-- 配置 JdbcTemplate -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!-- 装配数据源 -->
            <property name="dataSource" ref="druidDataSource"/>
        </bean>
    </beans>
    
  6. 准备测试类,按照上一篇文章的方法整合 JUnit

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
    
    /**
     * @author hope
     * @date 2023/3/30 - 12:04
     */
    @SpringJUnitConfig(locations = "classpath:bean.xml")
    public class JdbcTemplateTest {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Test
        public void updateTest() {
            // test code
        }
    }
    

实现 CRUD

在上一小节,我们获得了 JdbcTemplate 实例类,这节我将展示如何使用这个类。

添加操作

@Test
public void updateTest() {
    // Spring.t_emp 的表结构:
    // +-------+-------------+------+-----+---------+----------------+
    // | Field | Type        | Null | Key | Default | Extra          |
    // +-------+-------------+------+-----+---------+----------------+
    // | id    | int         | NO   | PRI | NULL    | auto_increment |
    // | name  | varchar(20) | YES  |     | NULL    |                |
    // | age   | int         | YES  |     | NULL    |                |
    // | sex   | varchar(2)  | YES  |     | NULL    |                |
    // +-------+-------------+------+-----+---------+----------------+

    // 实现添加操作
    // Step1. 编写 Sql 语句

    String sql = "INSERT INTO `t_emp` VALUES(NULL, ?, ?, ?)";
    // Step2. 调用 jdbcTemplate 的方法,执行 Sql 添加语句
    // 返回值是影响的行数
    int rows = jdbcTemplate.update(sql, "北原春希", 18, "男");
    System.out.println(rows); // 1
    // 另一种写法:
    // Object[] params = {"北原春希", 18, "男"}
    // jdbcTemplate.update(sql, params);

}

操作结果:

mysql> select * from t_emp;
+----+--------------+------+------+
| id | name         | age  | sex  |
+----+--------------+------+------+
|  1 | 北原春希     |   18 | 男   |
+----+--------------+------+------+
1 row in set (0.00 sec)

现在,请你给数据库添加下面的条目:

name age sex
小木曾雪菜 19
冬马和纱 17
野原新之助 6
东方不败 40 未知

修改操作

        // 现在的表:
        //+----+-----------------+------+--------+
        //| id | name            | age  | sex    |
        //+----+-----------------+------+--------+
        //|  1 | 北原春希        |   18 | 男     |
        //|  2 | 小木曾雪菜      |   19 | 女     |
        //|  3 | 冬马和纱        |   17 | 女     |
        //|  4 | 野原新之助      |    6 | 男     |
        //|  5 | 东方不败        |   40 | 未知   |
        //+----+-----------------+------+--------+
        // 实现修改操作
        // Step1. 编写 Sql 语句
        String sql = "update t_emp set name=? where id=?";
        // Step2. 调用 jdbcTemplate 的方法,执行 Sql 修改语句
        int rows = jdbcTemplate.update(sql, "桐谷和人", 1);
        System.out.println(rows);
        // 修改后:
        //+----+-----------------+------+--------+
        //| id | name            | age  | sex    |
        //+----+-----------------+------+--------+
        //|  1 | 桐谷和人        |   18 | 男     |
        //|  2 | 小木曾雪菜      |   19 | 女     |
        //|  3 | 冬马和纱        |   17 | 女     |
        //|  4 | 野原新之助      |    6 | 男     |
        //|  5 | 东方不败        |   40 | 未知   |
        //+----+-----------------+------+--------+

删除操作

        // 现在的表:
        //+----+-----------------+------+--------+
        //| id | name            | age  | sex    |
        //+----+-----------------+------+--------+
        //|  1 | 桐谷和人        |   18 | 男     |
        //|  2 | 小木曾雪菜      |   19 | 女     |
        //|  3 | 冬马和纱        |   17 | 女     |
        //|  4 | 野原新之助      |    6 | 男     |
        //|  5 | 东方不败        |   40 | 未知   |
        //+----+-----------------+------+--------+
        // 实现删除操作
        // Step1. 编写 Sql 语句
        String sql = "delete from t_emp where id=?";
        // Step2. 调用 jdbcTemplate 的方法,执行 Sql 删除语句
        int rows = jdbcTemplate.update(sql, 5);
        System.out.println(rows);
        // 删除后:
        //+----+-----------------+------+------+
        //| id | name            | age  | sex  |
        //+----+-----------------+------+------+
        //|  1 | 桐谷和人        |   18 | 男   |
        //|  2 | 小木曾雪菜      |   19 | 女   |
        //|  3 | 冬马和纱        |   17 | 女   |
        //|  4 | 野原新之助      |    6 | 男   |
        //+----+-----------------+------+------+

在使用 JdbcTemplate 进行数据库的增、删、改时,使用的都是其中的 update 方法。

查询操作

先准备好用于接收查询结果的对象类:

public class Emp {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
    
    // toString() ...
    
    // setter and getter ...
手动查询
    @Test
    public void selectTest() {
        // 实现查询操作,为了性能和可读性,在实际开发中,最好不要使用 select * from... 的形式
        String sql = "select id, name, age, sex from `t_emp` where id=?";

        // queryForObject 包含三部分参数,
        // 1. sql 语句
        // 2. RowMapper 转换接口的实现类,用于实现查询行到对象的转换(不常用的转换可以用匿名类实现,常用的可以保留实现类)
        // 3. Object... 用来替换?部分的参数
        // 返回值和 RowMapper 的泛型有关
        Emp emp = jdbcTemplate.queryForObject(sql, new RowMapper<Emp>() {
            @Override
            public Emp mapRow(ResultSet rs, int rowNum) throws SQLException {
                Emp ans = new Emp();
                ans.setId(rs.getInt("id"));
                ans.setName(rs.getString("name"));
                ans.setAge(rs.getInt("age"));
                ans.setSex(rs.getString("sex"));
                return ans;
            }
        }, 1);
        System.out.println(emp); // Emp{id=1, name='桐谷和人', age=18, sex='男'}
    }
自动查询

使用 spring 官方提供的 BeanPropertyRowMapper 实现自动查询,它是 RowMapper 的实现类,可以直接将数据库表的属性名和对象的属性名对应起来进行赋值。

String sql = "select id, name, age, sex from `t_emp` where id=?";
// 使用 BeanPropertyRowMapper 时,需要传入目标对象的对象类型
Emp emp = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Emp.class), 1);
System.out.println(emp); // Emp{id=1, name='桐谷和人', age=18, sex='男'}

通过 jdbcTemplate.query 可以获得符合条件的多个对象,参数和使用方式类似于 queryForObject

String sql = "select id, name, age, sex from `t_emp` where sex=?";       
// 使用 query 方法可以得到查询结果的列表
List<Emp> empList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class), "女");
System.out.println(empList);// [Emp{id=2, name='小木曾雪菜', age=19, sex='女'}, Emp{id=3, name='冬马和纱', age=17, sex='女'}]

另外,queryForObject 的另一种使用方式是在中间参数直接填入一个类型,表明返回单个该类型的值:

String sql = "select count(*) from `t_emp` where sex=?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, "女");
System.out.println(count); // 2

声明式事务

事务的基本概念

事务(Transaction)就是一组数据库操作序列,这些操作是一个不可分割的工作单位,要么全部成功,要么全部失败。

事务的特性:ACID,这里不再赘述。

编程式事务

与声明式事务对应的是编程式事务。它的核心就是通过直接编码实现事务。典型的代码如下:

Connection conn = ...;
    
try {
    
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    
    // 核心操作
    
    // 提交事务
    conn.commit();
    
}catch(Exception e){
 
    // 回滚事务
    conn.rollBack();
    
}finally{
    
    // 释放数据库连接
    conn.close();
    
}

编程式事务的缺陷在于:

  • 细节没有被屏蔽,细节较多,编码繁琐
  • 代码复用性不高,没有实现有效的抽取

声明式事务

事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。

基于注解的声明式事务

环境准备和说明

  1. 数据库准备(图书表、用户表)

    CREATE TABLE `t_book` (
      `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
      `price` int(11) DEFAULT NULL COMMENT '价格',
      `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
      PRIMARY KEY (`book_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    
    insert  into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
    
    CREATE TABLE `t_user` (
      `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `username` varchar(20) DEFAULT NULL COMMENT '用户名',
      `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    
    insert  into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
    
  2. 子环境搭建、自动扫描配置、JdbcTemplate 配置

  3. 创建组件

    Controller 层:

    package site.penghao.spring6.jdbc.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import site.penghao.spring6.jdbc.service.BookService;
    
    /**
     * @author hope
     * @date 2023/3/31 - 14:52
     */
    @Controller
    public class BookController {
    
        @Autowired
        private BookService bookService;
    
        public void buyBook(Integer bookId, Integer userId){
            bookService.buyBook(bookId, userId);
        }
    }
    

    Service 层:

    package site.penghao.spring6.jdbc.service;
    
    /**
     * @author hope
     * @date 2023/3/31 - 14:53
     */
    public interface BookService {
        void buyBook(Integer bookId, Integer userId);
    }
    
    package site.penghao.spring6.jdbc.service;
    
    import site.penghao.spring6.jdbc.dao.BookDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @author hope
     * @date 2023/3/31 - 14:53
     */
    @Service
    public class BookServiceImpl implements BookService {
    
        @Autowired
        private BookDao bookDao;
    
        @Override
        public void buyBook(Integer bookId, Integer userId) {
            //查询图书的价格
            Integer price = bookDao.getPriceByBookId(bookId);
            //更新图书的库存
            bookDao.updateStock(bookId);
            //更新用户的余额
            bookDao.updateBalance(userId, price);
        }
    }
    

    Dao 层:

    package site.penghao.spring6.jdbc.dao;
    
    /**
     * @author hope
     * @date 2023/3/31 - 14:54
     */
    public interface BookDao {
        Integer getPriceByBookId(Integer bookId);
    
        void updateStock(Integer bookId);
    
        void updateBalance(Integer userId, Integer price);
    }
    
    package site.penghao.spring6.jdbc.dao;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author hope
     * @date 2023/3/31 - 14:54
     */
    @Repository
    public class BookDaoImpl implements BookDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public Integer getPriceByBookId(Integer bookId) {
            String sql = "select price from t_book where book_id = ?";
            return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
        }
    
        @Override
        public void updateStock(Integer bookId) {
            String sql = "update t_book set stock = stock - 1 where book_id = ?";
            jdbcTemplate.update(sql, bookId);
        }
    
        @Override
        public void updateBalance(Integer userId, Integer price) {
            String sql = "update t_user set balance = balance - ? where user_id = ?";
            jdbcTemplate.update(sql, price, userId);
        }
    }
    

不添加事务

上面的环境配置可以看出,用户 admin 的余额是 50 元,而《斗破苍穹》的价格是 80 元,如果按照上面的逻辑执行买书:

  1. 获得图书价格——正常
  2. 更新库存——正常
  3. 更新余额——出错,原因是用户的 balance 字段设置为 unsigned,50 - 80 的结果为负,会抛出 SQLException

我们测试看看:

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class TransactionTest {
    @Autowired
    BookController bookController;
    @Test
    public void transactionTest() {
        bookController.buyBook(1, 1); // org.springframework.dao.DataIntegrityViolationException ...
    }
}

再看看数据库的情况,用户的余额不变,书籍的库存却被更新了:

mysql> select * from t_user;
+---------+----------+---------+
| user_id | username | balance |
+---------+----------+---------+
|       1 | admin    |      50 |
+---------+----------+---------+
1 row in set (0.00 sec)

mysql> select *from t_book;
+---------+--------------+-------+-------+
| book_id | book_name    | price | stock |
+---------+--------------+-------+-------+
|       1 | 斗破苍穹     |    80 |    99 |
|       2 | 斗罗大陆     |    50 |   100 |
+---------+--------------+-------+-------+
2 rows in set (0.00 sec)

这里的问题在于,前两步已经执行了,balance 却无法更新,这就导致数据库的信息出错。我们期望的是这些步骤要么全成功、要么全失败,所以需要给这些步骤添加事务。

加入事务

  1. 添加事务配置

    在 spring 的配置文件中添加下面的配置(需要引入 tx 命名空间):

    <!-- 配置事务的事件源,使用我们前面创建的 druidDataSource -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
    
    <!--
        开启事务的注解驱动:通过注解 @Transactional 所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
    -->
    <!-- transaction-manager 属性的默认值是 transactionManager,
    	如果事务管理器 bean 的 id 正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
    
  2. 添加事务注解

    一般来说,service 层代表业务层,一个方法就表示完成一个功能,因此处理事务一般在 service 层添加。在我们的例子中,只需要在 BookServiceImpl 的 buybook() 添加注解 @Transactional 即可。

    @Service
    public class BookServiceImpl implements BookService {
    	...
        @Transactional
        @Override
        public void buyBook(Integer bookId, Integer userId) {
           ...
    

    @Transactional 注解也可以添加到类的前面,相当于对该类的所有方法添加 @Transactional

  3. 执行测试

    可以看到,结果是报错了,book 的库存并没有再次减少:

    mysql> select * from t_user;
    +---------+----------+---------+
    | user_id | username | balance |
    +---------+----------+---------+
    |       1 | admin    |      50 |
    +---------+----------+---------+
    1 row in set (0.00 sec)
    
    mysql> select *from t_book;
    +---------+--------------+-------+-------+
    | book_id | book_name    | price | stock |
    +---------+--------------+-------+-------+
    |       1 | 斗破苍穹     |    80 |    99 |
    |       2 | 斗罗大陆     |    50 |   100 |
    +---------+--------------+-------+-------+
    2 rows in set (0.00 sec)
    

@Transactional 的属性

readOnly

如果设置为 true,则只能查询,不允许修改添加删除。默认值 false。

timeout

设置超时时长,单位为秒。例如设置为 timeout = 3,则在 3s 内没有完成,就抛出异常并回滚;默认值为 -1,不使用超时。

回滚策略

设置哪些情况回滚,哪些情况不回滚。

  • rollbackFor 属性:需要设置一个 Class 类型的对象

  • rollbackForClassName 属性:需要设置一个字符串类型的全类名

  • noRollbackFor 属性:需要设置一个 Class 类型的对象

  • noRollbackForClassName 属性:需要设置一个字符串类型的全类名

例如,设置出现 ArithmeticException 异常时不进行回滚。

@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}

测试结果:

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class TransactionTest {
    @Autowired
    BookController bookController;
    @Test
    public void transactionTest() {
        bookController.buyBook(2, 1);
    }
}
mysql> select *from t_book;
+---------+--------------+-------+-------+
| book_id | book_name    | price | stock |
+---------+--------------+-------+-------+
|       1 | 斗破苍穹     |    80 |    99 |
|       2 | 斗罗大陆     |    50 |    99 |
+---------+--------------+-------+-------+
2 rows in set (0.00 sec)

mysql> select * from t_user;
+---------+----------+---------+
| user_id | username | balance |
+---------+----------+---------+
|       1 | admin    |       0 |
+---------+----------+---------+
1 row in set (0.00 sec)
isolation

可以设置数据库的隔离级别。有下面的隔离级别可选:

  • 读未提交:READ UNCOMMITTED

    允许 Transaction01 读取 Transaction02 未提交的修改。

  • 读已提交:READ COMMITTED(Oracle 默认)

    要求 Transaction01 只能读取 Transaction02 已提交的修改。

  • 可重复读:REPEATABLE READ(Mysql 默认)

    确保 Transaction01 可以多次从一个字段中读取到相同的值,即 Transaction01 执行期间禁止其它事务对这个字段进行更新。

  • 串行化:SERIALIZABLE

    确保 Transaction01 可以多次从一个表中读取到相同的行,在 Transaction01 执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

propagation

可以设置事务的传播行为。

在 service 类中有 a() 方法和 b() 方法,a() 方法上有事务,b() 方法上也有事务,当 a() 方法执行过程中调用了 b() 方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。换言之,就是说如果一个事务方法调用了多个事务方法,被调用的某个方法执行失败了,是回退整个事务方法,还是仅回退失败的方法呢?

一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**
  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
  • NEVER:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像 REQUIRED 一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和 REQUIRED 一样。】

最常用的两个:REQUIRED(默认)、REQUIRES_NEW。

全注解的事务管理

和之前的思路一致,使用配置类替换 bean.xml 实现全注解。

/**
 * @author hope
 * @date 2023/3/31 - 16:17
 */
@Configuration
@ComponentScan("site.penghao.spring6.jdbc")
@EnableTransactionManagement // 开启事务管理
@PropertySource({"classpath:jdbc.properties"}) // 引入数据源
public class SpringConfig {
    private final Environment env;

    @Autowired // 注入环境,环境中包含数据源信息
    public SpringConfig(Environment env) {
        this.env = env;
    }

    @Bean // 声明 dataSource
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(env.getProperty("jdbc.user"));
        dataSource.setPassword(env.getProperty("jdbc.password"));
        dataSource.setUrl(env.getProperty("jdbc.url"));
        dataSource.setDriverClassName(env.getProperty("jdbc.driver"));
        return dataSource;
    }

    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean(name = "transactionManager") // 设置 bean id
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

基于 XML 的声明式事务

在 XML 文件中,进行如下配置也可以实现声明式事务:

<!-- tx:advice 标签:配置事务通知 -->
<!-- id 属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager 属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- tx:method 标签:配置具体的事务方法 -->
        <!-- name 属性:指定方法名,可以使用星号代表多个字符 -->
        <tx:method name="get*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="find*" read-only="true"/>
    
        <!-- read-only 属性:设置只读属性 -->
        <!-- rollback-for 属性:设置回滚的异常 -->
        <!-- no-rollback-for 属性:设置不回滚的异常 -->
        <!-- isolation 属性:设置事务的隔离级别 -->
        <!-- timeout 属性:设置事务的超时属性 -->
        <!-- propagation 属性:设置事务的传播行为 -->
        <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        
    </tx:attributes>
</tx:advice>

<aop:config>
    <!-- 配置事务通知和切入点表达式的绑定 -->
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* site.penghao.spring6.jdbc.service.*.*(..))"></aop:advisor>
</aop:config>

注意:使用 XML 配置声明式事务时,需要引入依赖 AspectJ。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.2</version>
</dependency>

小结

  • spring 提供了导入外部属性文件的方法,可以将数据库的配置信息储存在一个配置文件中,需要时导入,这样就实现了数据库配置和工程文件的解耦。
  • 使用 JdbcTemplate 能简化数据库的操作,对于增加、删除、修改操作统一使用 update() 方法,对于查询操作可以使用 queryForObject() 查询一个,或者使用 query() 查询多个。
  • RowMapper 是 JdbcTemplate 查询可选的参数,它是一个接口,需要实现从数据库查询行到对象的转化逻辑。
  • 声明式事务将事务控制的代码结构抽取出来,使得事务开发的代码更为简洁。使用声明式事务的基本步骤是:配置 transactionManager、开启事务的注解驱动 tx:annotation-driven、添加事务注解 @Transactional。
  • 事务注解 @Transactional 可以设置多种属性,包括:设置只读、超时、回滚策略、隔离模式、传播行为等。
  • 使用配置类也可以实现事务管理。