@DS+@Transactional注解切换数据源失效解决方案

背景

项目中运用了MySQL数据库,并按照功能模块采用了分库的战略。因而,一个业务逻辑类中或许触及多个MySQL数据库的操作。

咱们项目中是采用@DS(”xxx”)来完成数据源切换。

当注解添加到类上,意味着此类里的办法都运用此数据源; 当注解添加到办法上时,意味着此办法上运用的数据源优先级高于其他一切装备;

问题剖析

代码

  • 依赖
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.28</version>
</dependency>
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.3.1</version>
</dependency>
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.3.1</version>
</dependency>
  • yml装备
spring:
  datasource:
   dynamic:
    primary: master #设置默许的数据源或许数据源组,默许值即为master
    strict: false #严格匹配数据源,默许false. true未匹配到指定数据源时抛异常,false运用默许数据源
    datasource:
     master:
      url: jdbc:mysql://localhost:3306/demo_01?useSSL=false&autoReconnect=true&characterEncoding=utf8
      username: root
      password: xxx
      driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此装备
     slave:
      url: jdbc:mysql://172.23.168.70:3306/dynamic?useSSL=false&autoReconnect=true&characterEncoding=utf8
      username: root
      password: xxx
      driver-class-name: com.mysql.cj.jdbc.Driver
  • 对象实体
/**
 * @author itender
 * @date 2023/4/28 11:01
 * @desc
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_dynamic_template")
public class DynamicTemplateEntity {
​
  @TableId(type = IdType.AUTO)
  private Integer id;
​
  /**
   * 言语
   */
  private String language;
​
  /**
   * 言语编码
   */
  @TableField("language_code")
  private String languageCode;
​
  /**
   * 创立时间
   */
  @TableField("created_time")
  private Date createdTime;
​
  /**
   * 创立人
   */
  @TableField("created_by")
  private Integer createdBy;
​
  /**
   * 创立人称号
  */
  @TableField("created_by_name")
  private String createdByName;
}
​
/**
 * @author itender
 * @date 2023/4/28 10:57
 * @desc
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName("t_user")
public class UserEntity {
​
  /**
   * 主键id
   */
  @TableId(type = IdType.AUTO)
  private Integer id;
​
  /**
   * 用户称号
   */
  private String username;
}
​
  • controller代码
/**
 * @author itender
 * @date 2023/4/28 10:34
 * @desc
 */
@RestController
@RequestMapping("template")
public class DynamicTemplateController {
​
  private final DynamicTemplateService dynamicTemplateService;
​
  @Autowired
  public DynamicTemplateController(DynamicTemplateService dynamicTemplateService) {
    this.dynamicTemplateService = dynamicTemplateService;
   }
​
  @GetMapping
  public List<DynamicTemplateEntity> list() {
    return dynamicTemplateService.list();
   }
​
  @PostMapping
  public Integer add(@RequestBody DynamicTemplateEntity template) {
    return dynamicTemplateService.add(template);
   }
}
  • service
/**
 * @author itender
 * @date 2023/4/28 10:36
 * @desc
 */
public interface DynamicTemplateService {
​
  /**
   * 查询模板调集
   *
   * @return
   */
  List<DynamicTemplateEntity> list();
​
  /**
   * 添加模板
   *
   * @param template
   * @return
   */
  Integer add(DynamicTemplateEntity template);
}
  • mapper
/**
 * @author itender
 * @date 2023/4/28 11:09
 * @desc
 */
@DS("slave")
@Mapper
@Repository
public interface DynamicTemplateMapper extends BaseMapper<DynamicTemplateEntity> {
}
/**
 * @author itender
 * @date 2023/4/28 11:08
 * @desc
 */
@Mapper
@Repository
@DS("master")
public interface UserMapper extends BaseMapper<UserEntity> {
}
  • 业务代码
/**
 * @author itender
 * @date 2023/4/28 11:15
 * @desc
 */
@Service
public class DynamicTemplateServiceImpl implements DynamicTemplateService {
    private final DynamicTemplateMapper dynamicTemplateMapper;
    private final UserMapper userMapper;
    private final UserService userService;
    @Autowired
    public DynamicTemplateServiceImpl(DynamicTemplateMapper dynamicTemplateMapper, UserMapper userMapper, UserService userService) {
        this.dynamicTemplateMapper = dynamicTemplateMapper;
        this.userMapper = userMapper;
        this.userService = userService;
    }
    @Override
    public List<DynamicTemplateEntity> list() {
        List<DynamicTemplateEntity> templateList = dynamicTemplateMapper.selectList(new QueryWrapper<>());
        if (CollectionUtils.isEmpty(templateList)) {
            return Lists.newArrayList();
        }
        List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
        if (CollectionUtils.isEmpty(userList)) {
            return templateList;
        }
        Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
        templateList.forEach(template -> template.setCreatedByName(userMap.get(template.getCreatedBy())));
        return templateList;
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Integer add(DynamicTemplateEntity template) {
        List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
        if (CollectionUtils.isEmpty(userList)) {
            template.setCreatedByName("");
        }
        Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
        template.setCreatedByName(userMap.get(template.getCreatedBy()));
        template.setCreatedTime(new Date());
        dynamicTemplateMapper.insert(template);
        return template.getId();
    }
}

测试

  • 当办法没有@Transactional注解时,能够正常切换数据源
[    {        "id": 1,        "language": "中文",        "languageCode": "chinese",        "createdTime": "2023-04-27T18:56:25.000+00:00",        "createdBy": 1,        "createdByName": "itender"    }]

能够正常切换数据源。

  • 当办法有@Transactional注解时,切换数据源失利
### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
### The error may exist in com/itender/threadpool/mapper/DynamicTemplateMapper.java (best guess)
### The error may involve com.itender.threadpool.mapper.DynamicTemplateMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO t_dynamic_template  ( language, language_code, created_time, created_by, created_by_name )  VALUES  ( ?, ?, ?, ?, ? )
### Cause: java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist] with root cause
java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist

剖析

  • spring 的@Transactional声明式业务管理时经过动态署理完成的。
  • @DS注解加到mapper接口、service接口、service办法里都不收效,获取的仍是默许的主数据源。猜测是因为spring的aop切面机制导致阻拦不到@DS注解,从而不能切换数据源,正确的做法是添加到service完成类或许完成类里详细的办法上。
  • 在业务办法内调用@DS注解的办法,@DS注解相同不收效,原因是spring只能阻拦到最外层办法的@Transactional注解,此刻加载该业务的数据源,在业务办法内即便调用了@DS注解的办法,获取的是外层业务的数据源,导致@DS失效。
  • 在同一个完成类中,一个非DS注解的惯例办法里调用@DS注解的办法,相同存在@DS失效的状况,原因同2,是由spring的aop机制导致的,假如确有这种业务需要,能够将该DS注解办法定义在不同的类中,经过bean注入的办法调用,就不会呈现这个问题。

解决方案

  • 把查询user的逻辑放到别的一个独自的业务逻辑类里边
/**
 * @author itender
 * @date 2023/4/28 14:25
 * @desc
 */
public interface UserService {
    /**
     * 查询用户调集
     *
     * @return
     */
    List<UserEntity> list();
}
/**
 * @author itender
 * @date 2023/4/28 14:27
 * @desc
 */
@Service
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;
    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
    @DS("master")
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    @Override
    public List<UserEntity> list() {
        return userMapper.selectList(new QueryWrapper<>());
    }
}
  • 修改template业务类
@DS("slave")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public Integer add(DynamicTemplateEntity template) {
    // List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
    List<UserEntity> userList = userService.list();
    if (CollectionUtils.isEmpty(userList)) {
        template.setCreatedByName("");
    }
    Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
    template.setCreatedByName(userMap.get(template.getCreatedBy()));
    template.setCreatedTime(new Date());
    dynamicTemplateMapper.insert(template);
    return template.getId();
}

测试成功插入一条数据。

总结

  • spring 的@Transactional声明式业务管理时经过动态署理完成的。
  • @DS注解加到mapper接口、service接口、service办法里都不收效,获取的仍是默许的主数据源。猜测是因为spring的aop切面机制导致阻拦不到@DS注解,从而不能切换数据源,正确的做法是添加到service完成类或许完成类里详细的办法上。
  • 在业务办法内调用@DS注解的办法,@DS注解相同不收效,原因是spring只能阻拦到最外层办法的@Transactional注解,此刻加载该业务的数据源,在业务办法内即便调用了@DS注解的办法,获取的是外层业务的数据源,导致@DS失效。
  • 在同一个完成类中,一个非DS注解的惯例办法里调用@DS注解的办法,相同存在@DS失效的状况,原因同2,是由spring的aop机制导致的,假如确有这种业务需要,能够将该DS注解办法定义在不同的类中,经过bean注入的办法调用,就不会呈现这个问题。