Spring Boot 学习笔记 04——访问数据库

609 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

MyBatis 框架

MyBatis 的官方定义为:MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生 Map 使用简单的 XML 或注解,将接口和 Java 的 POJO (Plain Old Java Object, 普通的Java对象) 映射成数据库中的记录。

MyBatis 的配置文件包括两个大的部分,一是基础配置文件,一个是映射文件。在 MyBatis 中也可以使用注解来实现映射,只是由于功能和可读性的限制,在实际的企业中使用得比较少。MyBatis 社区为了整合 Spring 自己开发了相应的开发包,因此在 Spring Boot 中,我们可以依赖 MyBatis 社区提供的 starter。例如,在Maven中加入依赖的包,如代码所示:

<!-- 引入关于 MyBatis 的 starter -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.1</version>
</dependency>

详见专栏:MyBatis 学习笔记

数据库事务处理

执行 SQL 事务流程

执行 SQL 事务流程

Spring 数据库事务约定

Spring 数据库事务约定

当 Spring 的上下文开始调用被 @Transactional 标注的类或者方法时,Spring 就会产生 AOP 的功能。请注意事务的底层需要启用 AOP 功能,这是 Spring 事务的底层实现,后面我们会看到一些陷阱。那么当它启动事务时,就会根据事务定义器内的配置去设置事务,首先是根据传播行为去确定事务的策略,然后是隔离级别、超时时间、只读等内容的设置,只是这步设置事务并不需要开发者完成,而是 Spring 事务拦截器根据 @Transactional 配置的内容来完成的。

在上述场景中,Spring 通过对注解 @Transactional 属性配置去设置数据库事务,跟着 Spring 就会开始调用开发者编写的业务代码。执行开发者的业务代码,可能发生异常,也可能不发生异常。在 Spring 数据库事务的流程中,它会根据是否发生异常采取不同的策略。

如果都没有发生异常,Spring 数据库拦截器就会帮助我们提交事务,这点也并不需要我们干预。如果发生异常,就要判断一次事务定义器内的配置,如果事务定义器已经约定了该类型的异常不回滚事务就提交事务,如果没有任何配置或者不是配置不回滚事务的异常,则会回滚事务,并且将异常抛出,这步也是由事务拦截器完成的。

无论发生异常与否,Spring 都会释放事务资源,这样就可以保证数据库连接池正常可用了,这也是由 Spring 事务拦截器完成的内容。 从流程中我们可以看到开发者在整个流程中只需要完成业务逻辑即可,其他的使用 Spring 事务机制和其配置即可,这样就可以把 try... catch... finally...、数据库连接管理和事务提交回滚的代码交由 Spring 拦截器完成,而只需要完成业务代码即可,所以经常看到如下所示的简洁代码。

// ......
public class UserServiceImpl implements UserService {
  
  @Autowired
  private UserDao userDao = null;
  
  @Override
  @Transactional
  public int insertUser(User user) {
    return userDao.insertUser(user);
  }
  
  // ......
}

隔离级别

  1. 未提交读(read uncommitted),允许一个事务读取另外一个事务没有提交的数据。优点在于并发能力高,最大坏处是出现脏读。实际应用中采用的不多。
  2. 读写提交(read committed),指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。不足之处为出现不可重复读。
  3. 可重复读(repeatable read),目标是克服读写提交中出现的不可重复读现象。不足之处为出现幻读(指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的数据行)。
  4. 串行化(serializable),要求所有的 SQL 都会按顺序执行。
隔离级别和可能发生的现象

对于 Oracle 默认的隔离级别为读写提交, MySQL 则是可重复读。

传播行为

在 Spring 中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为

传播行为含义
PROPAGATION_REQUIRED表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
PROPAGATION SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行
PROPAGATION_MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager
PROPAGATION_NOT_SUPPORTED表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager 的话,则需要访问 TransactionManager
PROPAGATON_NEVER表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATON_NIESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回流。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务

测试 REQUIRED、REQUIRES_NEW 和 NESTED 3种常用传播行为

  • REQUIRED 内外方法同一个事务。内部抛异常,外部回滚。外部抛异常,内部回滚。
  • REQUIRES_NEW 内外方法不是同一个事务。内部抛异常,外部不回滚。外部抛异常,内部不回滚。
  • NESTED 内外方法是嵌套事务。内部抛异常,外部不回滚。外部抛异常,内部回滚。

以 REQUIRED 举例:

UserBatchServiceImpl.java

package com.springboot.chapter6.service.impl;

/**** imports ****/

@Service
public class UserBatchServiceImpl implements UserBatchService {
  @Autowired
  private UserService userService = null;
  @Override
  @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
  public int insertUsers(List<User> userList) {
    int count = 0;
    for(User user : userList) {
      // 调用子方法,将使用 @Transactional 定义的传播行为
      count += userService.insertUser(user);
    }
    return count;
  }
}

UserController.java

@Autowired
private UserBatchService userBatchService = null;

@RequestMapping("/insertUsers")
@ResponseBody
public Map<String, Object> insertUsers(String userName1, String note1, String userName2, String note2) {
  User user1 = new User();
  user1.setUserName(userName1);
  user1.setNote(note1);
  User user2 = new User();
  user2.setUserName(userName2);
  user2.setNote(note2);

  List<User> userList = new ArrayList<>();
  userList.add(user1);
  userList.add(user2);
  // 结果会回填主键,返回插入条数
  int inserts = userBatchService.insertUsers(userList);
  Map<String, Object> result = new HashMap<>();
  result.put("success", inserts > 0);
  result.put("user", userList);
  return result;
}

浏览器地址栏输入 http://localhost:8080/user/insertUsers?userName1=username_1&note1=note_1&userName2=username_2&note2=note_2,观察后台日志:

Participating in existing transaction

Creating a new SqlSession

......

Releasing transactional SqlSession [...]

Participating in existing transaction

Creating a new SqlSession

......

Releasing transactional SqlSession [...]

接着修改 insertUser 方法,分别测试 REQUIRES_NEW 和 NESTED,输出日志不过多展示。

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
public int insertUser(User user) {
  return userDao.insertUser(user);
}

/**********/

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.NESTED)
public int insertUser(User user) {
  return userDao.insertUser(user);
}