Spring 学习笔记③:JDBC与事务管理


收藏这三篇笔记,完整回顾Spring常见问题及使用方式速查:

  1. Spring 学习笔记①:IoC容器、Bean与注入
  2. Spring 学习笔记②:动态代理及面向切面编程
  3. Spring 学习笔记③:JDBC与事务管理(即本篇)

0. 基本概念

  • Spring 框架提供的JDBC支持主要由四个包组成,分别是 core(核心包)、object(对象包)、dataSource(数据源包)和 support(支持包)。
<!-- JDBC模板 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>
<!-- 事务控制 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
<!-- mysql依赖 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.20</version>
</dependency>

1. JDBCTemplate使用示例

1.1 数据源及配置

数据源的基本类即为 org.springframework.jdbc.dataSource.DriverManagerDataSource ,主要功能是获取数据库连接,还可以引入缓冲池、分布式事务等,需要进行以下配置:

<!-- 配置数据源 --> 
<bean id="dataSource" class="org.springframework.jdbc.dataSource.DriverManagerDataSource">
  <!-- 数据库驱动-->
  <!-- 注意:com.mysql.jdbc.Driver 在`8.0.x`的mysql驱动中已废弃 -->
  <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> 
  <!-- 连接数据库的url,需要同步时区-->
  <property name= "url" value="jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Shanghai" />
  <!-- 连接数据库的用户名 -->
  <property name="username" value="$root" />
  <!-- 连接数据库的密码 -->
  <property name="password" value="$password" />
  <!-- 连接池的配置 -->
  <property name="initialPoolSize" value="3"></property>
  <property name="maxPoolSize" value="10"></property>
  <property name="maxStatements" value="100"></property>
  <property name="acquireIncrement" value="2"></property>

</bean>

核心类是 org.springframework.jdbc.core.JDBCTemplate ,需要载入数据源进行实例化:

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.jdbcTemplate">
  <!--默认必须使用数据源-->
  <property name="dataSource" ref="dataSource"/>
</bean>

也可以使用配置文件类完成以上配置:

@Bean
public DriverManagerDataSource dataSource(){
    String url = "jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Shanghai";
    String username = "root";
    String password = "$password";
    DriverManagerDataSource d = new DriverManagerDataSource(url, username, password);
    d.setDriverClassName("com.mysql.cj.jdbc.Driver");
    return d;
}

@Bean
public JdbcTemplate jdbcTemplate(){
    return new JdbcTemplate(dataSource());
}

1.2 数据库配置

假设数据库有表 student

CREATE TABLE student (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);

INSERT INTO student (id, name, age, email) VALUES
  (1, 'Jack',  28, '[email protected]'),
  (2, 'Louis', 20, '[email protected]'),
  (3, 'Tom',   24, '[email protected]'),
  (4, 'Sandy', 12, '[email protected]'),
  (5, 'Lily',  85, '[email protected]');

以及对应的Model:

@Data
@AllArgsConstructor
public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}

1.3 查询语句示例

JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
jdbcTemplate.query(
    "SELECT * FROM student;",
    (resultSet, i) -> new Student(resultSet.getInt(1), resultSet.getString(2), resultSet.getInt(3), resultSet.getString(4))
).forEach(System.out::println);

2. Spring事务控制

  • 事务控制一般在Service层,分为“编程式事务”和“声明式事务”;Sring提供的是后者,基于AOP实现。
  • Spring基于JDBC的事务管理器为 DataSourceTransactionManager

2.1 配置文件

由于Spring的事务控制是基于AOP实现的,因此也需要引入AOP的命名空间。

<beans xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>

2.2 事务接口

Spring-tx (即Spring Transaction)中的三个核心接口为:

  • PlatformTransactionManager :用于管理事务。
  • TransactionDefinition :用于定义事务的的相关信息。
  • TransactionStatus :描述事务的状态。

2.2.1 事务管理:PlatformTransactionManager

  • TransactionStatus getTransaction(TransactionDefinition definition) :用于获取事务状态信息。
  • void commit(TransactionStatus status) :用于提交事务。
  • void rollback(TransactionStatus status) :用于回滚事务。

2.2.2 事务信息:TransactionDefinition

  • String getName() :获取事务对象名称。
  • int getIsolationLevel() :获取事务的隔离级别。
  • int getPropagationBehavior() :获取事务的传播行为。
  • int getTimeout() :获取事务的超时时间。
  • boolean isReadOnly() :获取事务是否只读。

其中,事务的传播行为被定义为:

属性名称 描述 的值
PROPAGATION_REQUIRED 如果当前有事务环境就加入当前正在执行的事务环境,否则就新建一个事务。【默认】 REQUIRED
PROPAGATION_SUPPORTS 指定当前方法加入当前事务环境,如果当前没有事务,就以非事务方式执行。 SUPPORTS
PROPAGATION_MANDATORY 指定当前方法必须加入当前的事务环境,如果当前没有事务,则抛出异常。 MANDATORY
PROPAGATION_REQUIRES_NEW 将创建新的事务,如果当前方法已经在事务中,则将当前新建的事务挂起等待执行。 REQUIRES_NEW
PROPAGATION_NOT_SUPPORTED 不支持当前事务,以非事务状态执行。如果当前存在事务环境,则将其挂起等待当前方法先执行。 NOT_SUPPORTED
PROPAGATION_NEVER 不支持当前事务,如果当前方法在事务中,则抛出异常。 NEVER
PROPAGATION_NESTED 指定当前方法执行时,如果已经有一个事务存在,则运行在这个嵌套的事务中。如果当前环境没有运行的事务,就新建并与父事务相互独立的新事务,这个事务拥有多个可以回滚的保存点——内部事务回滚不会对外部事务造成影响(只对 DataSourceTransactionManager 事务管理器起效)。 NESTED

2.2.3 事务状态:TransactionStatus

  • void flush() :刷新事务。
  • boolean hasSavepoint() :获取是否存在保存点。
  • boolean isCompleted() :获取事务是否完成。
  • boolean isNewTransaction() :获取是否是新事务。
  • boolean isRollbackOnly() :获取是否回滚。
  • void setRollbackOnly() :设置事务回滚。

2.3 Spring的声明式事务管理

构建一个转账的数据库事务场景,则数据库表如下:

CREATE TABLE account (
    id INT (11) PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    username VARCHAR(20) NOT NULL COMMENT '姓名',
    money INT DEFAULT NULL COMMENT '账户余额'
);
/* 初始数据 */
INSERT INTO account VALUES (1, '张三', 2000);
INSERT INTO account VALUES (2, '李四', 1000);

对应的Model类:

@Data
@AllArgsConstructor
public class Account {
    private Integer id;
    private String username;
    private Integer money;
}

出具持久层:

@Repository(value = "accountDao")
public class AccountDaoImpl implements AccountDaoInterface{
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void send(int money, Account sbPaid) {
        val sql = "UPDATE account SET money=money-? WHERE username=?;";
        this.jdbcTemplate.update(sql, money, sbPaid.getUsername());
    }

    @Override
    public void receive(int money, Account sbReceived) {
        val sql = "UPDATE account SET money=money+? WHERE username=?;";
        this.jdbcTemplate.update(sql, money, sbReceived.getUsername());
    }
    
    @Override
    public Account query(Account account){
        val sql = "SELECT * FROM account WHERE username=?";
        return this.jdbcTemplate.query(sql,
                (res, index) -> new Account(res.getInt(1),
                        res.getString(2),
                        res.getInt(3)
                ),
            account.getUsername()).get(0);
    }
}

业务逻辑为:

@Service(value = "accountService")
public class AccountServiceImpl implements AccountServiceInterface {
    private AccountDaoInterface accountDao;

    @Autowired
    public void setAccountDao(AccountDaoInterface accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public void transfer(Account from, Account to, int money) throws Throwable {
        this.accountDao.send(money, from);
        if(money >= 1000) throw new Exception("转账超过限额");
        this.accountDao.receive(money, to);
    }

    @Override
    public Account query(Account account){
        return this.accountDao.query(account);
    }
}

测试类:

AccountServiceInterface accountService = (AccountServiceInterface)context.getBean("accountService");
System.out.println(accountService.query(new Account(1, "张三", -1)));
System.out.println(accountService.query(new Account(2, "李四", -1)));
accountService.transfer(
        new Account(1, "张三", -1),
        new Account(2, "李四", -1), 500); // 改成1000之后将会中断转账 
System.out.println(accountService.query(new Account(1, "张三", -1)));
System.out.println(accountService.query(new Account(2, "李四", -1)));

2.3.1 示例①:基于XML的声明式事务管理

<!-- 编写通知:对事务进行增强(通知),需要编写切入点和具体执行事务的细节 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>
    <!-- 给切入点方法添加事务详情:
                name                    :表示方法名称(*表示任意方法名称)
        propagation     :用于设置传播行为
        isolation           :表示隔离级别
        rollback-for    :需要回滚的异常类 -->
    <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.Exception"/>
  </tx:attributes>
</tx:advice>

<!-- aop编写,让Spring自动对目标生成代理,需要使用AspectJ的表达式 -->
<aop:config>
  <!-- 切入点 -->
  <aop:pointcut expression="execution(* MVC.Service.AccountServiceImpl.transfer(..))" id="txPointCut" />
  <!-- 通知器:将切入点与通知整合 -->
  <aop:advisor pointcut-ref="txPointCut" advice-ref="txAdvice" />
</aop:config>


2.3.2 tx:method的属性详解

属性 类型 默认值 说明
propagation propagation枚举 REQUIRED 事务传播属性(详见2.2.2)
isolation isolation枚举 DEFAULT(所用数据库默认级别) 事务隔离级别
read-only boolean false 是否才用优化的只读事务
timeout int -1 超时(秒)
rollbackFor Class[] {} 需要回滚的异常类
rollbackForClassName String[] {} 需要回滚的异常类名
noRollbackFor Class[] {} 不需要回滚的异常类
noRollbackForClassName String[] {} 不需要回滚的异常类名
  • read-only 的值为 true ,则为只读事务,即连接点内无 INSERT 等写操作。
  • 隔离等级共有五级:
  1. DEFAULT :【默认】采用数据库默认隔离级别。
  2. SERIALIZABLE :最严格的级别,事务串行执行,资源消耗最大。
  3. REPEATABLE_READ :保证了一个事务不会修改已经由另一个事务读取但未提交的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
  4. READ_COMMITTED :大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
  5. READ_UNCOMMITTED :保证了读取过程中不会读取到非法数据。隔离级别在于处理多事务的并发问题。

2.3.3 示例②:基于注解的声明式事务管理

在配置文件中开启事务注解的驱动及注册事务管理器:

<!-- 配置事务管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>
<!-- 注册事务管理驱动 -->
<tx:annotation-driven transaction-manager="txManager"/>

修改业务层:

@Service(value = "accountService")
// 添加此注释,参数与 2.3.1 & 2.3.2 小节中一致
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, rollbackFor = {Exception.class})
public class AccountServiceImpl implements AccountServiceInterface {
    private AccountDaoInterface accountDao;

    @Autowired
    public void setAccountDao(AccountDaoInterface accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public void transfer(Account from, Account to, int money) throws Throwable {
        this.accountDao.send(money, from);
        if(money >= 1000) throw new Exception("转账超过限额");
        this.accountDao.receive(money, to);
    }

    @Override
    public Account query(Account account){
        return this.accountDao.query(account);
    }
}

附录 学习笔记①~③代码及工程文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章