整合mybatis&事务管理

1. 整合mybatis

1.1 整合步骤

【第一步】在pom.xml中导入SpringBoot整合mybatis的依赖和mysql驱动

<!--spring boot mybatis起步依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<!--mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

【第二步】在application.yml配置文件中配置连接参数

# 配置数据库的连接信息
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring?useSSl=false
    username: root
    password: 123456

【第三步】定义Mapper接口和方法,在Mapper接口上使用**@Mapper**注解表示该类mybatis的mapper接口

@Mapper//表示该类是mybatis的mapper接口类
public interface UserMapper {
    @Select("select * from tb_user")
    List<User> selectAll();
}

【第四步】在单元测试中调用mapper的方法进行测试

@SpringBootTest
class Spring03ApplicationTests {
    @Autowired
    private UserMapper userMapper;
    @Test
    void contextLoads() {
        //1.调用userMapper的selectAll()方法,查询所有
        List<User> list = userMapper.selectAll();
        //2.遍历打印
        list.forEach(user -> System.out.println(user));
    }
}

1.2 注意事项

问题:很容易在Mapper接口上忘记写@Mapper注解

解决:可以在引导类上使用@MapperScan注解批量加载指定包结构中的所有Mapper接口。那么在mapper接口上就可以不使@Mapper注解

@SpringBootApplication
@MapperScan("com.hsl.mapper")//表示批量加载指定包结构中的所有mapper接口
public class Spring03Application {
    public static void main(String[] args) {      
    	SpringApplication.run(Spring03Application.class, args);
    }
}
//@Mapper//表示该类是mybatis的mapper接口类
public interface UserMapper {
    @Select("select * from tb_user")
    List<User> selectAll();
}

1.3 Mybatis相关配置

问题:java 对象中的属性是驼峰命名法,而数据库的字段名为采用的是下划线命名法,二者并不一致

  • 配置开启驼峰命名自动映射
mybatis:
  configuration:
    map-underscore-to-camel-case: true #开启驼峰命名自动映射
  • 加载xml映射配置文件
    • 映射文件配置别名:type-aliases-package: com.hsl.pojo
    • 加载mybatis核心配置文件:config-location: classpath:mybatis-config.xml
    • 加载mapper目录中所有的Mapper.xml文件:*mapper-locations: classpath:mapper/Mapper.xml
mybatis:
  configuration:
    map-underscore-to-camel-case: true #开启驼峰命名自动映射
    type-aliases-package: com.hsl.pojo #pojo包中所有的类的类名就是别名,在映射配置文件中可以使用别名
    config-location: classpath:mybatis-config.xml # 加载resources目录中mybatis核心配置文件
  	mapper-locations: classpath:mapper/*Mapper.xml #加载mapper目录中userMapper.xml文件。如果映射配置文件和mapper接口同名同位置,则会自动加载,不需要配置

1.4 日志级别配置

ALL   最低等级,用于打开所有日志记录。

TRACE  很低的日志级别,一般不会使用。

DEBUG  主要用于开发与测试过程中打印一些运行信息,不可以用于生产环境。

INFO   记录一些信息型消息比如服务器启动成功,输入的数据,输出的数据等等。

WARN   记录警告信息比如客户端和服务器之间的连接中断,数据库连接丢失等信息。

ERROR 用于记录ERROR和Exception信息。

FATAL   指可能导致程序终止的非常严重的信息。在这种事件之后你的应用很可能会崩溃。

OFF   最高等级,用于关闭所有日志记录。

① 设置整个项目日志级别

# 设置日志级别
logging:
  level:
    root: debug

② 查看运行过程中mapper包下的SQL信息

# 设置日志级别
logging:
  level:
    com.hsl.mapper: debug #mapper包中的所有日志输出级别为debug级别。可以看到sql语句

③ 如果只想看某个类或某个方法的SQL,就需要提供更为具体的类名或方法名

logging:
  level:
    com.hsl.mapper.UserMapper.selectAll: debug

1.5 数据源配置

Spring Boot 使用的连接池是 hikari,可以在 application.yml 中敲入 hikari 提示相关配置.

  • 最大连接数和最小连接数

    yaml文件

#hikari连接池配置信息
    hikari:
      maximum-pool-size: 10 # 最大连接数
      minimum-idle: 3 # 最小连接数

​ properties文件

#hikari连接池配置信息
#最大连接数
spring.datasource.hikari.maximum-pool-size=10
#最小连接数
spring.datasource.hikari.minimum-idle=3
  • 单元测试类中获取以上配置信息
@SpringBootTest//表示该类是一个SpringBoot单元测试类
class Spring03ApplicationTests {
	@Autowired //指定类型是HikariDataSource,否则无法调用特有方法
	private HikariDataSource dataSource;
    @Test
    void contextLoads() {
        System.out.println("最大连接数:" + dataSource.getMaximumPoolSize());
        System.out.println("最小连接数:" + dataSource.getMinimumIdle());
    }

2. 事务管理

2.1 环境搭建

  • 环境准备,初始化表和数据:在 init.sql 中加入
-- 删除tb_account表
drop table if exists tb_account;
-- 创建tb_account表
create table tb_account
(
    id int primary key auto_increment, -- 账户id,主键自增
    name varchar(20) not null, -- 账户名称
    amount double not null -- 金额
);
-- 初始化数据
insert into tb_account values(null,'张三',1000),(null,'李四',1000);

  • pojo
@Data
public class Account {
    private Integer id; //账户id
    private String name; //账户名称
    private double amount; //金额
}
  • mapper
public interface AccountMapper {
    /**
     * 根据id查询账户信息
     * @param id 账户id
     * @return
     */
    @Select("select * from tb_account where id = #{id}")
    Account findById(int id);

    /**
     * 根据id修改账户金额
     * @param id 账户id
     * @param amount 要加或者减的金额
     */
    @Update("update tb_account set amount = amount + #{amount} where id = #{id}")
    void updateAmount(@Param("id") int id,@Param("amount") double amount);
}
  • service
public interface AccountService {
    /**
     * 转账方法
     * @param fromId 转出账户id
     * @param toId 转入账户id
     * @param amount 转账金额
     */
    void transfer(int fromId,int toId,double amount);
}
@Service //将Bean添加到spring容器中
@Slf4j //用于打印日志信息
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    /**
     * 实现转账业务:
     * 要求:转出账户金额充足才可以进行转账
     *
     * @param fromId 转出账户id
     * @param toId   转入账户id
     * @param amount 转账金额
     */
    @Override
    public void transfer(int fromId, int toId, double amount) throws FileNotFoundException {
        Account fromAccount = accountMapper.findById(fromId);
        //1 根据fromId查询转出账户信息,判断账户余额是否充足
        if (fromAccount.getAmount() >= amount) {
            //2 转出账户余额充足,开始进行转换
            accountMapper.updateAmount(fromId, -amount);    
            accountMapper.updateAmount(toId, amount);
        } else {
            log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
        }
    }
}
  • 测试
@SpringBootTest
@Slf4j//日志打印
class Spring03TransactionalApplicationTests {
    @Autowired
    private AccountService accountService;
	@Test
    void contextLoads(){
        log.info("转账开始...");
        accountService.transfer(1,2,500);
        log.info("转账结束...");
    }
}

结果为:

2.2 问题

如果中间有异常的话,还能照常执行吗

 public void transfer(int fromId, int toId, double amount) throws FileNotFoundException {
        Account fromAccount = accountMapper.findById(fromId);
        //1 根据fromId查询转出账户信息,判断账户余额是否充足
        if (fromAccount.getAmount() >= amount) {
            //2 转出账户余额充足,开始进行转换
            accountMapper.updateAmount(fromId, -amount);    
            //造一个异常
            int num = 1/0;
            accountMapper.updateAmount(toId, amount);
        } else {
            log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
        }
    }

结果报了算数异常:java.lang.ArhslcException: / by zero

表中的数据:张三转出了500,李四没有得到转入

因为执行到 int num = 1/0;方法已经停止运行了,所有没有转入金额

2.3 解决

给需要事务管理的方法上加上@Transactional注解

 @Transactional
 public void transfer(int fromId, int toId, double amount) throws FileNotFoundException {
        Account fromAccount = accountMapper.findById(fromId);
        //1 根据fromId查询转出账户信息,判断账户余额是否充足
        if (fromAccount.getAmount() >= amount) {
            //2 转出账户余额充足,开始进行转换
            accountMapper.updateAmount(fromId, -amount);    
            //造一个异常
            int num = 1/0;
            accountMapper.updateAmount(toId, amount);
        } else {
            log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
        }
    }

重新运行 transfer 发现事务发生了回滚

可以通过日志配置来实现看数据库监测事务的运行情况

#配置springboot日志级别,输出运行过程中的SQL语句
logging:
  level:
    com.hsl.mapper: debug
    org.springframework.jdbc.support.JdbcTransactionManager: debug

其中 spring boot 的 JdbcTransactionManager 中会显示事务的开始、提交、回滚等信息,只需要把它的日志级别调整为 debug 即可

  • 日志解读信息
2022-06-13 11:10:50.290  INFO 1872 --- [           main] .i.Spring03TransactionalApplicationTests : 转账开始...

//创建新事务
2022-06-13 11:10:50.294 DEBUG 1872 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.hsl.service.impl.AccountServiceImpl2.transfer2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

//打开数据库连接
2022-06-13 11:10:50.294 DEBUG 1872 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689] for JDBC transaction

// 切换数据库连接为手动提交事务
2022-06-13 11:10:50.296 DEBUG 1872 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689] to manual commit

// 查询 转账账户余额
2022-06-13 11:10:50.322 DEBUG 1872 --- [           main] c.hsl.mapper.AccountMapper.findById  : ==>  Preparing: select * from tb_account where id = ?
2022-06-13 11:10:50.347 DEBUG 1872 --- [           main] c.hsl.mapper.AccountMapper.findById  : ==> Parameters: 1(Integer)
2022-06-13 11:10:50.373 DEBUG 1872 --- [           main] c.hsl.mapper.AccountMapper.findById  : <==      Total: 1

// 修改 转账账户减钱
2022-06-13 11:10:50.375 DEBUG 1872 --- [           main] c.i.mapper.AccountMapper.updateAmount    : ==>  Preparing: update tb_account set amount = amount + ? where id = ?
2022-06-13 11:10:50.376 DEBUG 1872 --- [           main] c.i.mapper.AccountMapper.updateAmount    : ==> Parameters: -500.0(Double), 1(Integer)
2022-06-13 11:10:50.377 DEBUG 1872 --- [           main] c.i.mapper.AccountMapper.updateAmount    : <==    Updates: 1

// 回滚事务
2022-06-13 11:10:50.377 DEBUG 1872 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction rollback
2022-06-13 11:10:50.377 DEBUG 1872 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689]

// 归还数据库连接
2022-06-13 11:10:50.517 DEBUG 1872 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2a066689] after transaction

一旦我们进行了事务管理,我们从容器中获取到的service就不是目标对象了,而是代理对象,其内部通过AOP的方式对目标对象进行了增强,代理对象内部使用JdbcTransactionManager对象在切点方法执行前后进行事务管理

2.4 初始化库

每次执行测试之后,数据库表中的数据都会发生变化,为了方便测试,需要手动恢复表中的数据.

解决思路:把 DDL 和 DML 的 sql 分别存储于 resources/schema.sql 和 resources/data.sql 这两个文件中,然后 application.properties 中配置

  • schema.sql
-- 删除tb_account表
drop table if exists tb_account;
-- 创建tb_account表
create table tb_account
(
    id int primary key auto_increment, -- 账户id,主键自增
    name varchar(20) not null, -- 账户名称
    amount double not null -- 金额
);
  • data.sql
-- 初始化数据
insert into tb_account values(null,'张三',1000),(null,'李四',1000);

配置文件配置

  • application.properties
spring.datasource.initialization-mode=always
  • application.yml
  sql:
    init:
      mode: always # 总是初始化关系型数据库

2.5 进阶

@Transactional 注解,它其中有很多配置,称为事务属性,摘录如下

属性名称含义补充说明
propagation传播行为-
isolation隔离级别-
timeout超时时间当规定时间内,事务仍未完成时,会回滚并结束事务
readOnly是否只读某些数据库驱动会对只读事务做优化
rollbackFor遇到什么异常回滚事务-
noRollbackFor遇到什么异常不回滚事务-

1) 事务失效(器异常所致)

将异常缓存编译期异常,程序抛出异常,运行之后发现事务仍然提交。

  @Override
    @Transactional
    public void transfer2(int fromId, int toId, double amount) throws FileNotFoundException {
        Account fromAccount = accountMapper.findById(fromId);
        //1 根据fromId查询转出账户信息,判断账户余额是否充足
        if (fromAccount.getAmount() >= amount) {
            //2 转出账户余额充足,开始进行转换
            accountMapper.updateAmount(fromId, -amount);
            //造一个异常
            //int num = 1 / 0;
            new FileInputStream("");//抛出了编译异常
            accountMapper.updateAmount(toId, amount);
        } else {
            log.info("余额仅剩{},无法进行转账操作!", fromAccount.getAmount());
        }
    }
  • 原因:

    • Spring 默认只有抛出运行时异常(即 RuntimeException 及子类)或 Error 及子类时,才会回滚事务
    • 如果抛出的是其它编译时异常仍然会提交事务
  • 解决办法:配置 rollbackFor = Exception.class

@Transactional(rollbackFor = Exception.class)

2) 事务失效(捕获异常所致)

在业务层被事务管理的方法内的异常不能捕获, 如果try…catch…最终事务还是提交

  • 原因:

    • 事务提交、回滚,都是代理对象调用事务通知来完成的,如果代理对象不知道出现了异常,也就没有机会去执行回滚
    • 自己 try-catch 异常,意味着代理对象认为【没有发生异常】,因此也会提交事务
  • 解决办法:业务方法内不要捕获异常、或者将捕获的异常重新抛出

3) 事务属性 - propagation

两个受事务控制的业务方法之间进行调用时,事务是合二为一、还是另开新事务,这可以通过传播行为进行控制,对其中最常用的 REQUIRED 和 REQUIRES_NEW 说明如下

@Service
public class ServiceA {

    private static final Logger log = LoggerFactory.getLogger(ServiceA.class);

    @Autowired
    private ServiceB b;

    @Transactional(propagation = 传播行为)
    public void foo() {
        log.debug("foo");
        b.bar();
    }
}

@Service
public class ServiceB {
    private static final Logger log = LoggerFactory.getLogger(ServiceB.class);

    @Transactional(propagation = 传播行为)
    public void bar() {
        log.debug("bar");
    }
}

ServiceA.foo() 调用 ServiceB.bar(),其传播行为组合测试如下
测试1

  • foo - @Transactional(propagation = Propagation.REQUIRED)
  • bar - @Transactional(propagation = Propagation.REQUIRED)
  • 结论 foo bar 同一事务

测试2

  • foo - @Transactional(propagation = Propagation.REQUIRED)
  • bar - @Transactional(propagation = Propagation.REQUIRES_NEW)
  • 结论 foo bar 不同事务

测试3

  • foo - @Transactional(propagation = Propagation.REQUIRED)
  • bar - @Transactional(propagation = Propagation.REQUIRES_NEW)
  • bar - 出现异常、foo 不捕获
  • 结论 bar 回滚,foo 也会回滚

测试4

  • foo - @Transactional(propagation = Propagation.REQUIRED)
  • bar - @Transactional(propagation = Propagation.REQUIRES_NEW)
  • bar - 出现异常、foo 捕获
  • 结论 bar 回滚,foo 因为捕获了异常,因此会提交
其它传播行为
属性名称含义补充说明
REQUIRED需要事务,有则加入,无则创建新事务
REQUIRES_NEW需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在独立的连接中运行 SQL结合 Hibernate、JPA 时有用,配在查询方法上
NOT_SUPPORTED不支持事务,不加入,在独立的连接中运行 SQL-
MANDATORY必须有事务,否则抛异常-
NEVER必须没事务,否则抛异常-
NESTED嵌套事务仅对 DataSourceTransactionManager 有效

4) 传播行为失效

@Service
public class ServiceC {

    private static final Logger log = LoggerFactory.getLogger(ServiceC.class);

    @Transactional(propagation = Propagation.REQUIRED)
    public void foo() {
        log.debug("foo");
        bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void bar() {
        log.debug("bar");
    }
}

在 foo 方法内调用 bar 方法,因为两个方法都配置了 @Transactional,现在测试 foo 方法,根据配置,期望有两个事务

@SpringBootTest
public class TransactionalTests {

    // ...

    @Autowired
    private ServiceC serviceC;

    @Test
    void test5() throws Exception {
        serviceC.foo();
    }
}

观察发现,自始至终只有一个事务,并没有像预期的在 bar 方法调用时由 @Transactional(propagation = Propagation.REQUIRES_NEW) 创建一个新事务

原因:
这实际就是前面讲代理时的通知失效问题,同类的两个方法调用不走代理

解决方法1:

引导类中新增 @EnableAspectJAutoProxy(exposeProxy = true),同时添加 aop 依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

调用时修改为

@Service
public class ServiceC {

    private static final Logger log = LoggerFactory.getLogger(ServiceC.class);

    @Transactional(propagation = Propagation.REQUIRED)
    public void foo() {
        log.debug("foo");
        ((ServiceC) AopContext.currentProxy()).bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void bar() {
        log.debug("bar");
    }
}

解决方法2:

自己注入自己(其实注入了代理),其它不用动

@Service
public class ServiceC {

    private static final Logger log = LoggerFactory.getLogger(ServiceC.class);

    @Autowired
    private ServiceC serviceC;

    @Transactional(propagation = Propagation.REQUIRED)
    public void foo() {
        log.debug("foo");
        serviceC.bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void bar() {
        log.debug("bar");
    }
}

ebug(“foo”);
((ServiceC) AopContext.currentProxy()).bar();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bar() {
    log.debug("bar");
}

}


解决方法2:

自己注入自己(其实注入了代理),其它不用动

```java
@Service
public class ServiceC {

    private static final Logger log = LoggerFactory.getLogger(ServiceC.class);

    @Autowired
    private ServiceC serviceC;

    @Transactional(propagation = Propagation.REQUIRED)
    public void foo() {
        log.debug("foo");
        serviceC.bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void bar() {
        log.debug("bar");
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.han、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值