一 概述
动态数据源是指在程序运行时可以动态切换数据源的技术。当我们具有相同的数据库结构、相同的Java实体映射,想要在上层进行数据源的切换(数据库层面)来查询不同的数据库时,便可采用动态数据源技术。
网络上也有很多的动态数据源的实现方案,大多是在项目配置文件中提前声明好主数据源、从数据源1、从数据源2(一主多从),但这种方式不是我想要的,我希望能够在代码端进行数据源的创建、初始化、查询、切换。
此文章将会利用SpringBoot的AbstractRoutingDataSource
+DruidDataSource
+MyBatisPlus
进行动态数据源的实现与说明。
1 版本说明
- Java - 8
- SpringBoot - 2.6.14
- MyBatsiPlus(mybatis-plus-boot-starter) - 3.5.3
- druid(alibaba) - 1.1.21
- 数据库 - pg(不重要)
2 关键依赖
<!-- mybatis-plus-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<!-- 此外还用到Lombok、Hutool 等工具包,用于简化代码-->
3 数据库与测试数据说明
有两部分数据库,一部分为系统数据源库,存储需要动态切换的数据源信息,例如数据库的host、port、db、user、password等等;
另一部分为我们需要进行切换的库,他们具有相同的数据库表结构,存储我们的业务数据,示例中只列举了两个业务库,实际可自由扩展,由代码端进行数据库的创建、表的初始化、数据源记录的添加。
4 实体
@Data
@TableName(value = "sys_datasource", autoResultMap = true)
public class DataSourceEntity implements Serializable {
private static final long serialVersionUID = 1L;
public static final String DriverClassName = "org.postgresql.Driver";
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "host")
private String host;
@TableField(value = "port")
private Integer port;
@TableField(value = "database")
private String database;
@TableField(value = "user")
private String user;
@TableField(value = "password")
private String password;
public String getUrl(){
return StrUtil.format("jdbc:postgresql://{}:{}/{}", host,port,database);
}
}
@Data
@TableName("data_people", autoResultMap = true)
public class PeopleEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "name")
private String name;
@TableField(value = "age")
private Integer age;
}
5 项目结构
二 实现效果
PeopleController中定义查询接口,同时接口需要传入数据源id参数,来查询指定数据源中的数据。
@RestController
@RequestMapping("/people")
@AllArgsConstructor
@Slf4j
public class PeopleController {
private final DataSourceService dataSourceService;
private final PeopleService peopleService;
@GetMapping("/list/{dataSourceId}")
public Object list(@PathVariable(value = "dataSourceId") Integer dataSourceId) {
// 切换到指定数据源
if (!dataSourceService.changeDataSource(dataSourceId)) {
return "指定数据源失败";
}
List<PeopleEntity> list = peopleService.list();
// 切换回原有数据源
dataSourceService.restoreDataSource();
return list;
}
}
三 具体实现
1 DataSourceIdHolder - 数据源id存放类
就像是之前做动态表名一样,因为接口进来后是在不同的线程中,需要有一个地方存放当前线程要查询的数据源id,所以使用ThreadLocal来做。ThreadLocal此处存储Integer整形数据,即数据源记录的id(整形自增id)。
此处也需要考虑避免内存泄漏的问题,需要确保set后remove即可,考虑到不希望在别的地方出现太多DataSourceIdHolder的引用,此步骤交给了DataSource的Service层来包装。
/**
* 数据源id存放类
*/
@Slf4j
public class DataSourceIdHolder {
/**
* 线程级别的私有变量
*/
private static final ThreadLocal<Integer> DATASOURCEID_HOLDER = new ThreadLocal<>();
/**
* 指定数据源
*/
public static void setDataSource(Integer dataSourceId) {
DATASOURCEID_HOLDER.set(dataSourceId);
log.info("数据源Holder-设置数据源. threadId:[{}] dataSourceId:[{}]", Thread.currentThread().getId(), dataSourceId);
}
/**
* 获取数据源
*
* @return
*/
public static Integer getDataSource() {
Integer dataSourceId = DATASOURCEID_HOLDER.get();
log.info("数据源Holder-获取数据源. threadId:[{}] dataSourceId:[{}]", Thread.currentThread().getId(), dataSourceId);
return dataSourceId;
}
/**
* 删除数据源
*/
public static void removeDataSource() {
DATASOURCEID_HOLDER.remove();
log.info("数据源Holder-删除数据源. threadId:[{}] dataSourceId:[{}]", Thread.currentThread().getId(), DATASOURCEID_HOLDER.get());
}
}
2 DynamicRoutingDataSource - 动态路由数据源
AbstractRoutingDataSource(SpringBoot体系中的)是动态数据源实现的核心,它以Map<Object(数据源key),Object(DataSource数据源)>的形式存储了我们定义的多个数据源,它通过determineCurrentLookupKey()
方法返回一个key值,这个key值与上述Map中进行关联,来决定采用哪个数据源来返回数据库Connection对象进行后续的数据库操作,但本质上他也是一个数据源,实现了DataSource
接口。
我们通过重写它的determineCurrentLookupKey()
方法,从我们第一步“DataSourceIdHolder - 数据源id存放类”中来拿到要切换的数据源,来实现数据源的切换。
/**
* 动态路由数据源
*/
@Slf4j
@Data
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 存储我们注册的数据源 <Integer(DataSourceId),DruidDataSource>
*/
private volatile Map<Object, Object> customDataSources;
@Override
protected Object determineCurrentLookupKey() {
// 指定本次查询要使用的数据源LookupKey
// (1)LookupKey与dataSourceId为一对一的关系
// (2)dataSourceId来自DataSourceIdHolder(ThreadLocal<Integer>)
// (3)当此方法返回null时采用默认的数据源(即主数据源)
// 判断DataSourceIdHolder中是否指定数据源
Integer dataSourceId = DataSourceIdHolder.getDataSource();
boolean isSetDataSourceInHolder = dataSourceId != null && dataSourceId > 0;
if (!isSetDataSourceInHolder) {
// 未指定返回null(后续查询使用的数据源采用默认数据源)
log.info("LookupKey未指定,采用默认数据源. Holder中未指定数据源.");
return null;
}
// 判断已经创建的数据源中是否包含指定的数据源id
boolean isExistDataSourceInCustom = this.customDataSources.containsKey(dataSourceId);
if (isExistDataSourceInCustom) {
log.info("LookupKey已指定. dataSourceIdFromHolder:[{}]", dataSourceId);
return dataSourceId;
} else {
log.info("LookupKey已指定,但数据源未注册,采用默认数据源. dataSourceId:[{}]", dataSourceId);
return null;
}
}
/**
* 检查数据源是否已经创建,如果不存在则创建
*/
public boolean checkOrCreateDataSource(DataSourceEntity dataSourceEntity) {
if (this.customDataSources == null)
this.customDataSources = new HashMap<>();
boolean result = false;
Integer dataSourceId = dataSourceEntity.getId();
boolean isRegistered = this.customDataSources.containsKey(dataSourceId);
if (isRegistered) {
//检查之前创建的数据源现在是否连接正常
boolean isHealthy = checkDruidDataSource((DruidDataSource) this.customDataSources.get(dataSourceId));
if (!isHealthy) {
log.warn("数据源非健康状态,删除后重新创建数据源...");
result = delete(dataSourceId) && create(dataSourceEntity);
}
} else {
log.info("数据源未创建,创建数据源...");
result = create(dataSourceEntity);
}
return result;
}
/**
* 创建数据源
*
* @param dataSourceEntity 数据源实体
*/
private boolean create(DataSourceEntity dataSourceEntity) {
Integer dataSourceId = dataSourceEntity.getId();
String dataSourceName = getDataSourceName(dataSourceId);
// 检查数据源链接配置
if (!this.checkConnectionParameters(dataSourceEntity.getUrl(), dataSourceEntity.getUser(), dataSourceEntity.getPassword()))
return false;
// 构建Druid数据源
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setName(dataSourceName);
druidDataSource.setDriverClassName(DataSourceEntity.DriverClassName);
druidDataSource.setUrl(dataSourceEntity.getUrl());
druidDataSource.setUsername(dataSourceEntity.getUser());
druidDataSource.setPassword(dataSourceEntity.getPassword());
druidDataSource.setMaxActive(20);
druidDataSource.setMinIdle(5);
druidDataSource.setMaxWait(6000); //获取连接最大等待时间,单位毫秒
druidDataSource.setTestOnBorrow(true); //申请连接时执行validationQuery检测连接是否有效,防止取到的连接不可用
druidDataSource.setValidationQuery("select 1");
// Druid数据源初始化
try {
druidDataSource.init();
} catch (SQLException e) {
log.error("创建数据源-数据源初始化失败. id:[{}] name:[{}} 异常信息:[{}]", dataSourceId, dataSourceName, e.getMessage());
return false;
}
// 记录到自定义数据源Map中
this.customDataSources.put(dataSourceId, druidDataSource);
// 将map赋值给父类的TargetDataSources
super.setTargetDataSources(this.customDataSources);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
log.info("创建数据源-成功. 数据源id:[{}] name:[{}} ", dataSourceId, dataSourceName);
return true;
}
/**
* 删除数据源
*
* @param dataSourceId 数据源id
*/
private boolean delete(Integer dataSourceId) {
Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
for (DruidDataSource dataSource : druidDataSourceInstances) {
String dataSourceName = this.getDataSourceName(dataSourceId);
if (!dataSource.getName().equals(dataSourceName))
continue;
//从实例中移除当前dataSource
DruidDataSourceStatManager.removeDataSource(dataSource);
// 自定义的数据源记录中删除
this.customDataSources.remove(dataSourceId);
// 将map赋值给父类的TargetDataSources
super.setTargetDataSources(this.customDataSources);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
log.info("删除数据源-成功. dataSourceId:[{}] dataSourceName:[{}]", dataSourceId, dataSourceName);
return true;
}
log.warn("删除数据源-失败,未找到指定数据源. dataSourceId:[{}]", dataSourceId);
return false;
}
/**
* 根据数据源id(Integer)获取数据源name
* <p>
* 格式采用:ds_datasourceId
*
* @param dataSourceId
* @return
*/
private String getDataSourceName(Integer dataSourceId) {
if (dataSourceId == null || dataSourceId <= 0)
throw new IllegalArgumentException("dataSourceId不合法,请确保dataSourceId非空且大于0");
return StrUtil.format("ds_{}", dataSourceId);
}
/**
* 检查数据库链接配置
* <p>
* 逻辑:通过数据库连接配置参数,看能够正确构建连接对象
*
* @param url
* @param user
* @param password
* @return
*/
private boolean checkConnectionParameters(String url, String user, String password) {
if (StrUtil.isEmpty(url) || StrUtil.isEmpty(user) || StrUtil.isEmpty(password))
throw new IllegalArgumentException("数据库链接信息不可为空,请检查url、user、password参数.");
boolean isHealthy = true;
Connection connection = null;
// 通过尝试获取数据库链接校验数据库配置信息是否有误
try {
connection = DriverManager.getConnection(url, user, password);
isHealthy = connection != null;
} catch (SQLException exception) {
isHealthy = false;
log.error("数据源检查-失败,配置有错误,无法正确获取链接. url:[{}] user:[{}] password:[{}]", url, user, password);
} finally {
if (isHealthy)
try {
connection.close();
} catch (SQLException exception) {
log.warn("数据源检查-警告,检查后无法关闭链接. url:[{}] user:[{}] password:[{}] exception:[{}]", url, user, password, exception.getMessage());
}
}
return isHealthy;
}
/**
* 检查Druid数据源
* <p>
* 逻辑:通过数据源获取内部连接对象,看能够正确获取连接对象
*
* @param druidDataSource
* @return
*/
private boolean checkDruidDataSource(DruidDataSource druidDataSource) {
boolean isHealthy = true;
DruidPooledConnection connection = null;
try {
connection = druidDataSource.getConnection();
} catch (SQLException exception) {
//抛异常了说明连接失效,则删除现有连接
log.error("数据源健康检查,获取连接对象失败. 异常信息:[{}]", exception.getMessage());
isHealthy = false;
} finally {
//如果连接正常关闭连接
if (connection != null) {
try {
connection.close();
} catch (SQLException exception) {
log.warn("数据源健康检查,关闭连接对象失败. 异常信息:[{}]", exception.getMessage());
}
}
}
return isHealthy;
}
3 DruidDBConfig - DruidDB配置类
此配置类中我们定义了四个bean:
- mainDataSource(DataSource)
- 此bean作为主数据源,通过我们在配置文件中的配置进行构建;
- 返回DruidDataSource的实例(数据库连接池的管理);
- dynamicDataSource(DynamicRoutingDataSource)
- 此bean作为动态数据源;
- 将上一步定义的mainDataSource设置为默认数据源与目标数据源;
- 默认数据源是我们未指定数据源时采用的数据源;
- 目标数据源是 三-2 中determineCurrentLookupKey()中决定要采用哪个的数据源集合,本实例的另外两个数据库(数据源)就存放在此;
- sqlSessionFactory(SqlSessionFactory)
SqlSessionFactory
负责将 MyBatis的Configuration
对象的信息与连接池和数据源相关联,从而创建 SqlSession 的单个实例;- 此处有坑,放在后面讲
- transactionManager(DataSourceTransactionManager)
- 事务管理
/**
* DruidDB配置类
*/
@Configuration
@Slf4j
public class DruidDBConfig {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
// 连接池连接信息
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
/**
* 默认数据源bean
*
* @return
* @throws SQLException
*/
@Bean
@Primary
@Qualifier("mainDataSource")
public DataSource dataSource() throws SQLException {
DruidDataSource datasource = new DruidDataSource();
// 基础连接信息
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
// 连接池连接信息
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
//是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
datasource.setPoolPreparedStatements(false);
datasource.setMaxPoolPreparedStatementPerConnectionSize(20);
//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
datasource.setTestOnBorrow(true);
//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
datasource.setTestWhileIdle(true);
//用来检测连接是否有效的sql
// datasource.setValidationQuery("select 1 from dual");
datasource.setValidationQuery("select 1");
//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
datasource.setTimeBetweenEvictionRunsMillis(60000);
//配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000
datasource.setMinEvictableIdleTimeMillis(180000);
datasource.setKeepAlive(true);
return datasource;
}
/**
* 自定义的动态数据源bean
*
* @return
* @throws SQLException
*/
@Bean(name = "dynamicDataSource")
@Qualifier("dynamicDataSource")
public DynamicRoutingDataSource dynamicDataSource() throws SQLException {
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
//配置缺省的数据源
dynamicDataSource.setDefaultTargetDataSource(dataSource());
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//额外数据源配置 TargetDataSources
targetDataSources.put("mainDataSource", dataSource());
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//用mybatis的这里会有点区别,mybatis用的是SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 此处有坑,放在后面讲
return sqlSessionFactoryBean.getObject();
}
/**
* 将动态数据加载类添加到事务管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicRoutingDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
4 DataSource部分的Mapper与Service层
DataSourceService层包含两个接口定义,用于在其它Controller层中进行数据源的指定,一个用于指定数据源,另外一个用于恢复默认数据源。第二个接口void restoreDataSource()
便是对DataSourceIdHolder
在当前线程中存储的数据源id值的清除。
@Mapper
public interface DataSourceMapper extends BaseMapper<DataSourceEntity> {
}
public interface DataSourceService {
/**
* 设置为指定数据源
*
* @param dataSourceId
* @return
*/
boolean changeDataSource(Integer dataSourceId);
/**
* 恢复默认数据源
*/
void restoreDataSource();
}
@Service
@Slf4j
@AllArgsConstructor
public class DataSourceServiceImpl implements DataSourceService {
private final DataSourceMapper dataSourceMapper;
private final DynamicRoutingDataSource dynamicRoutingDataSource;
@Override
public boolean changeDataSource(Integer dataSourceId) {
//切到默认数据源,查询所有的数据源配置
DataSourceIdHolder.removeDataSource();
// TODO:高频查询,按需引入Redis
DataSourceEntity dataSourceEntity = dataSourceMapper.selectById(dataSourceId);
if (dataSourceEntity == null) {
log.warn("未找到指定数据源. dataSourceId:[{}]", dataSourceId);
return false;
}
log.info("已找到数据源,dataSourceId:[{}]", dataSourceId);
//判断连接是否存在,不存在就创建
boolean checkOrCreateResult = dynamicRoutingDataSource.checkOrCreateDataSource(dataSourceEntity);
if (checkOrCreateResult) {
// DataSourceIdHolder中记录使用的数据源Id,供下次查询时使用指定数据源
DataSourceIdHolder.setDataSource(dataSourceId);
}
return checkOrCreateResult;
}
@Override
public void restoreDataSource() {
//切回主数据源
DataSourceIdHolder.removeDataSource();
}
}
People部分的Mapper与Service层没有什么内容,是很薄的一层,采用MyBatisPlus的BaseMapper、IService、ServiceImpl进行扩展即可。
5 Controller - 在本层进行数据源的切换
@RestController
@RequestMapping("/people")
@AllArgsConstructor
@Slf4j
public class PeopleController {
private final DataSourceService dataSourceService;
private final PeopleService peopleService;
@GetMapping("/list/{dataSourceId}")
public Object list(@PathVariable(value = "dataSourceId") Integer dataSourceId) {
// 切换指定数据源
if (!dataSourceService.changeDataSource(dataSourceId)) {
return "指定数据源失败";
}
// 优雅一些的写法,采用Assert断言工具类确保切换数据源成功,在外部再增加全局异常捕获,对此类异常统一处理
// Assert.isTrue(dataSourceService.changeDataSource(dataSourceId),"指定数据源失败");
List<PeopleEntity> list = peopleService.list();
// 恢复默认数据源(当前线程结束前清空ThreadLocal中存放的值)
dataSourceService.restoreDataSource();
return list;
}
}
四 AbstractRoutingDataSource源码关键部分解读
AbstractRoutingDataSource是Spring框架中提供的一个抽象类,用于实现动态数据源。它继承自javax.sql.DataSource接口,并重写了getConnection()方法,该方法会根据当前线程绑定的数据源key来获取相应的数据源,并返回一个连接。
AbstractRoutingDataSource
中有Map<Object, Object> targetDataSources
来存放我们需要切换的数据源;
AbstractRoutingDataSource
中getConnection()
方法进行了特殊处理,通过方法determineCurrentLookupKey()
返回的lookupKey
来决定使用哪个数据源来返回数据库连接对象。因此示例中我们对determineCurrentLookupKey()
方法进行了重写,来实现我们想要的效果(从Holder中拿到数据源key进行切换)。
五 一些“坑”
1 为什么添加动态数据源后Mapper中定义的自定义查询方法调用提示无效绑定
上述示例是用作预研,都是最简单的CRUD测试,但实际项目中会在Mapper中添加我们自定义的查询方法与结果映射,添加动态数据源后调用Mapper中自定义的方法便报错提示Invalid bound statement (not found)
的错误。这类问题出现时,往往我们会检查下面内容:
- mapper.xml中的方法标签id与mapper接口中的方法名是否一致;
- mapper.xml中头部的namespace是否与mapper接口所在的路径一致;
- spring配置文件中mybatis(mybatis-plus)配置中对mapper.xml文件路径配置是否正确;
- Mapper是否使用@Mapper注解声明,或启动类是否添加Mapper的扫描路径@MapperScan
- …
当这些都检查完了发现依然不行,就需要去深入检查一下是否是动态数据源的引入出现的问题。排查过程不再赘述了,直接来看问题点。
我们知道,SqlSessionFactory
负责将 MyBatis的Configuration
对象的信息与连接池和数据源相关联,从而创建 SqlSession 的单个实例进而执行数据库操作。换句话说,也就是需要在SqlSessionFactory
中指定数据源与MyBatis的配置,比如Mapper
路径、MetaObjectHandler
(字段填充)!回过头来看一下三-3
中DruidDBConfig
(DruidDB配置类)中的SqlSessionFactory
bean要如何修改:
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//用mybatis的这里会有点区别,mybatis用的是SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//用mybatis的这里会有点区别,mybatis用的是SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 指定Mapper文件的位置
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
通过更改可以看到,我们给sqlSessionFactoryBean
指定了Mapper文件的路径。
在这里,我们使用了Spring提供的PathMatchingResourcePatternResolver类来获取指定路径下的所有Mapper文件,并将其设置到MybatisSqlSessionFactoryBean对象中。 具体来说,代码中的"classpath*:/mapper/.xml"表示从classpath中查找所有以.xml结尾的文件,并且文件路径中包含/mapper/的文件。其中,classpath:表示在所有的classpath中查找,包括jar包中的classpath;/mapper/表示在mapper目录下查找;*.xml表示查找以.xml结尾的文件。例如,如果MyBatis的Mapper文件位于/src/main/resources/mapper/目录下,那么这行代码就会查找该目录下所有以.xml结尾的文件。
2 为什么添加动态数据源后MyBatisPlus的MetaObjectHandler进行字段填充失效
通常我们会给Entity(数据库表)添加创建时间、更新时间等字段,我们希望在创建一条记录时,指定当前时间为创建时间,在更新这条记录时,指定更新时间为当前时间。在MyBatisPlus项目中,我们可以自定义元对象填充处理类实现MyBatisPlus的MetaObjectHandler
接口,对我们需要处理的字段在插入或者更新时进行处理。
基于MyBatisPlus实现针对于创建时间、更新时间字段进行填充,具体示例实现如下:
(1)第一步,编写自定义字段填充处理器,实现MyBatisPlus的MetaObjectHandler
接口,添加具体操作
@Slf4j
@Component
public class TimeFieldFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
// MetaObjectHandler提供的默认方法的策略均为:如果属性有值则不覆盖,如果填充值为null则不填充
// this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 更新时间字段采用此方式赋值,不管是否已经有值
this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject);
}
}
(2)第二步,在要进行处理的Entity中的属性上添加注解
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
这个示例在原本非动态数据源的项目中可以正常生效,但添加动态数据源支持后,此字段填充配置无法生效。首先来看一下官方贴出的注意事项:
我们知道,
SqlSessionFactory
负责将 MyBatis的Configuration
对象的信息与连接池和数据源相关联,从而创建 SqlSession 的单个实例进而执行数据库操作。换句话说,也就是需要在SqlSessionFactory
中指定数据源与MyBatis的配置,比如Mapper
路径、MetaObjectHandler
(字段填充)!回过头来看一下 三-3DruidDBConfig
(DruidDB配置类)中的SqlSessionFactory
bean要如何修改:…
回过头再看我在五-1
问题排查中说的这句话,结合字段填充器的本质是entity的属性设置值,我们可以推断出来实体行为受到了影响,也就是MyBatis配置产生的影响。回过头来看一下 三-3
DruidDBConfig
(DruidDB配置类)中的SqlSessionFactory
bean针对于字段填充失效要如何修改:
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//用mybatis的这里会有点区别,mybatis用的是SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//用mybatis的这里会有点区别,mybatis用的是SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 指定Mapper文件的位置
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml"));
// 构建MyBatisPlus全局配置对象,用于指定元对象字段填充
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new TimeFieldFillHandler());
sqlSessionFactoryBean.setGlobalConfig(globalConfig);
return sqlSessionFactoryBean.getObject();
}
这里我们构建了一个MyBatisPlus的全局配置对象,配置指定了MetaObjectHandler
。到此位置,问题就得到解决了。
六 参考与引用
MyBatisPlus - 自动填充功能
知乎 - 但偏偏雨渐渐 - 《SpringBoot+Mybatis-Plus实现动态数据源切换》