MyBatis-Plus
是一个非常不错的 MyBatis 的增强工具,为简化开发、提高效率而生。
MyBatis-Plus支持多数据源和动态数据源(dynamic-datasource),以适配不同需求和场景。
基本配置
引入Maven依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
动态数据源配置
spring:
datasource:
dynamic:
primary: master1
strict: true
datasource:
master1:
driverClassName: 驱动
url: URL
username: 用户名
password: 密码
master2:
driverClassName: 驱动
url: URL
username: 用户名
password: 密码
master3:
driverClassName: 驱动
url: URL
username: 用户名
password: 密码
slave1:
driverClassName: 驱动
url: URL
username: 用户名
password: 密码
slave2:
driverClassName: 驱动
url: URL
username: 用户名
password: 密码
AOP切面类
@Component
@Order(value = 0)
@Slf4j
@Aspect
public class DataSourceAspectOnMapper {
@Value("${spring.datasource.dynamic.primary}")
private String defaultDatabase;
/**
* 匹配mapper所在的package所有类(及子类)中所有方法(所有返回类型,所有类型参数)的执行
*/
@Pointcut("execution(* mapper所在的package..*.*(..))")
private void masterAspect() {
}
/**
* 执行Mapper方法前设置数据源
*/
@Before("masterAspect()")
public void beforeMasterDb() throws Exception{
String dataSource = DataSourceUtil.chooseDatasource(defaultDatabase);
DynamicDataSourceContextHolder.push(dataSource);
}
@After("masterAspect()")
public void afterMasterDb() {
DynamicDataSourceContextHolder.poll();
}
}
因为在事务中对Mapper切换数据源无效,需要把所有执行数据库事务的类也放在同一个代码包内,对事务也进行AOP切面,动态设置数据源。源码略。
踩坑
针对数据库表(以表名DEMO为例),自动生成对应的Entity、Mapper、Service等文件后,主要文件示例:
public interface DemoService extends IService<Demo> {
}
@Service
public class DemoServiceImpl extends ServiceImpl<DemoMapper, Demo> implements DemoService {
}
@SpringBootTest
class ApplicationTests {
@Autowired
private DemoService demoService;
@Test
public void batchInsert(){
demo.setGuid(java.util.UUID.randomUUID().toString());
//下面的方法正常
boolean ret = demoService.save(demo);
System.out.println(ret);
//下面的方法出错,提示“表或视图不存在”
boolean ret1 = demoService.saveOrUpdate(demo);
System.out.println(ret1);
}
}
填坑
源码分析
com.baomidou.mybatisplus.extension.service.IService.java 部分源码:
public interface IService<T> {
/**
* 默认批次提交数量
*/
int DEFAULT_BATCH_SIZE = 1000;
/**
* 插入一条记录(选择字段,策略插入)
*
* @param entity 实体对象
*/
default boolean save(T entity) {
return SqlHelper.retBool(getBaseMapper().insert(entity));
}
/**
* TableId 注解存在更新记录,否插入一条记录
*
* @param entity 实体对象
*/
boolean saveOrUpdate(T entity);
}
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.java 部分源码:
/**
* IService 实现类( 泛型:M 是 mapper 对象,T 是实体 )
*/
@SuppressWarnings("unchecked")
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
protected Log log = LogFactory.getLog(getClass());
@Autowired
protected M baseMapper;
/**
* TableId 注解存在更新记录,否插入一条记录
*
* @param entity 实体对象
* @return boolean
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveOrUpdate(T entity) {
if (null != entity) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass);
Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
String keyProperty = tableInfo.getKeyProperty();
Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!");
Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) save(entity) : updateById(entity);
}
return false;
}
}
save方法和saveOrUpdate区别:
save()是在接口中定义的default方法,saveOrUpdate()是在实现类中实现的带事务注解@Transactional的方法。
原因及解决方法
因为源码中saveOrUpdate使用了事务,故在Mapper切面设置数据源无效。
对MyBatis-plus的服务包进行切面,代码如下:
@Component
@Order(value = 0)
@Slf4j
@Aspect
public class DataSourceAspectOnService {
@Value("${spring.datasource.dynamic.primary}")
private String defaultDatabase;
/**
* 匹配service所在的package所有类(及子类)中所有方法(所有返回类型,所有类型参数)的执行
*/
@Pointcut("execution(* service所在的package..*.*(..))")
private void masterAspect() {
}
/**
* 执行Service方法前设置数据源
*/
@Before("masterAspect()")
public void beforeMasterDb() throws Exception{
String dataSource = DataSourceUtil.chooseDatasource(defaultDatabase);
DynamicDataSourceContextHolder.push(dataSource);
}
@After("masterAspect()")
public void afterMasterDb() {
DynamicDataSourceContextHolder.poll();
}
}
此时仍未解决问题,经查是因为DemoServiceImpl未覆盖父类方法saveOrUpdate,切面程序未执行。
@Service
public class DemoServiceImpl extends ServiceImpl<DemoMapper, Demo> implements DemoService {
@Override
public boolean saveOrUpdate(Demo entity) {
return super.saveOrUpdate(entity);
}
}
问题解决。这里不能使用@Pointcut("execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl+.*(..))")进行切面,因为该类同时被master数据源和slave这两类异构的数据源的Service所继承,AOP程序在此处无法确定使用哪个数据源。
本文到此结束,感谢您的观看!!!