@DS+@Transactional注解切換數(shù)據(jù)源失效問題及解決
背景
項目中使用了MySQL數(shù)據(jù)庫,并按照功能模塊采用了分庫的策略。因此,一個業(yè)務邏輯類中可能涉及多個MySQL數(shù)據(jù)庫的操作。
我們項目中是采用@DS(“xxx”)來實現(xiàn)數(shù)據(jù)源切換。
- 當注解添加到類上,意味著此類里的方法都使用此數(shù)據(jù)源;
- 當注解添加到方法上時,意味著此方法上使用的數(shù)據(jù)源優(yōu)先級高于其他一切配置;
問題分析
代碼
- 依賴
<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 #設置默認的數(shù)據(jù)源或者數(shù)據(jù)源組,默認值即為master strict: false #嚴格匹配數(shù)據(jù)源,默認false. true未匹配到指定數(shù)據(jù)源時拋異常,false使用默認數(shù)據(jù)源 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; /** * 創(chuàng)建時間 */ @TableField("created_time") private Date createdTime; /** * 創(chuàng)建人 */ @TableField("created_by") private Integer createdBy; /** * 創(chuàng)建人名稱 */ @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> { }
- 業(yè)務代碼
/** * @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注解時,可以正常切換數(shù)據(jù)源
[ { "id": 1, "language": "中文", "languageCode": "chinese", "createdTime": "2023-04-27T18:56:25.000+00:00", "createdBy": 1, "createdByName": "itender" } ]
可以正常切換數(shù)據(jù)源。
- 當方法有@Transactional注解時,切換數(shù)據(jù)源失敗
### 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聲明式事務管理時通過動態(tài)代理實現(xiàn)的。
- @DS注解加到mapper接口、service接口、service方法里都不生效,獲取的還是默認的主數(shù)據(jù)源。猜測是由于spring的aop切面機制導致攔截不到@DS注解,進而不能切換數(shù)據(jù)源,正確的做法是添加到service實現(xiàn)類或者實現(xiàn)類里具體的方法上。
- 在事務方法內(nèi)調(diào)用@DS注解的方法,@DS注解同樣不生效,原因是spring只能攔截到最外層方法的@Transactional注解,此時加載該事務的數(shù)據(jù)源,在事務方法內(nèi)即使調(diào)用了@DS注解的方法,獲取的是外層事務的數(shù)據(jù)源,導致@DS失效。
- 在同一個實現(xiàn)類中,一個非DS注解的常規(guī)方法里調(diào)用@DS注解的方法,同樣存在@DS失效的情況,原因同2,是由spring的aop機制導致的,如果確有這種業(yè)務需要,可以將該DS注解方法定義在不同的類中,通過bean注入的方式調(diào)用,就不會出現(xiàn)這個問題。
解決方案
- 把查詢user的邏輯放到另外一個單獨的業(yè)務邏輯類里面
/** * @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業(yè)務類
@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(); }
測試成功插入一條數(shù)據(jù)。
總結
spring 的@Transactional聲明式事務管理時通過動態(tài)代理實現(xiàn)的。
@DS注解加到mapper接口、service接口、service方法里都不生效,獲取的還是默認的主數(shù)據(jù)源。猜測是由于spring的aop切面機制導致攔截不到@DS注解,進而不能切換數(shù)據(jù)源,正確的做法是添加到service實現(xiàn)類或者實現(xiàn)類里具體的方法上。
在事務方法內(nèi)調(diào)用@DS注解的方法,@DS注解同樣不生效,原因是spring只能攔截到最外層方法的@Transactional注解,此時加載該事務的數(shù)據(jù)源,在事務方法內(nèi)即使調(diào)用了@DS注解的方法,獲取的是外層事務的數(shù)據(jù)源,導致@DS失效。
在同一個實現(xiàn)類中,一個非DS注解的常規(guī)方法里調(diào)用@DS注解的方法,同樣存在@DS失效的情況,原因同2,是由spring的aop機制導致的,如果確有這種業(yè)務需要,可以將該DS注解方法定義在不同的類中,通過bean注入的方式調(diào)用,就不會出現(xiàn)這個問題。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
SpringBoot入坑筆記之spring-boot-starter-web 配置文件的使用
本篇向小伙伴介紹springboot配置文件的配置,已經(jīng)全局配置參數(shù)如何使用的。需要的朋友跟隨腳本之家小編一起學習吧2018-01-01SpringBoot使用CORS實現(xiàn)無縫跨域的方法實現(xiàn)
CORS 是一種在服務端設置響應頭部信息的機制,允許特定的源進行跨域訪問,本文主要介紹了SpringBoot使用CORS實現(xiàn)無縫跨域的方法實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-10-10