基于SpringBoot的MyBatisPlus+Druid+AbstractRoutingDataSource的动态数据源实现方案

本文介绍了如何在SpringBoot应用中结合MyBatisPlus和Druid实现动态数据源切换。通过AbstractRoutingDataSource和ThreadLocal管理数据源ID,动态创建和切换数据源,确保在不同线程中能正确选择所需的数据源。同时,文章详细展示了配置、实体、Mapper、Service和Controller层的实现,以及解决动态数据源后MyBatisPlus字段填充失效的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 概述

动态数据源是指在程序运行时可以动态切换数据源的技术。当我们具有相同的数据库结构、相同的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等等;
image.png
另一部分为我们需要进行切换的库,他们具有相同的数据库表结构,存储我们的业务数据,示例中只列举了两个业务库,实际可自由扩展,由代码端进行数据库的创建、表的初始化、数据源记录的添加。
image.png

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 项目结构

image.png

二 实现效果

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;
    }
}

image.png

三 具体实现

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来存放我们需要切换的数据源;
image.png
AbstractRoutingDataSourcegetConnection()方法进行了特殊处理,通过方法determineCurrentLookupKey()返回的lookupKey来决定使用哪个数据源来返回数据库连接对象。因此示例中我们对determineCurrentLookupKey()方法进行了重写,来实现我们想要的效果(从Holder中拿到数据源key进行切换)。
image.png
image.png

五 一些“坑”

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(字段填充)!回过头来看一下三-3DruidDBConfig(DruidDB配置类)中的SqlSessionFactorybean要如何修改:

    @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;

这个示例在原本非动态数据源的项目中可以正常生效,但添加动态数据源支持后,此字段填充配置无法生效。首先来看一下官方贴出的注意事项:
image.png

我们知道,SqlSessionFactory负责将 MyBatis的Configuration对象的信息与连接池和数据源相关联,从而创建 SqlSession 的单个实例进而执行数据库操作。换句话说,也就是需要在SqlSessionFactory中指定数据源与MyBatis的配置,比如Mapper路径、MetaObjectHandler(字段填充)!回过头来看一下 三-3 DruidDBConfig(DruidDB配置类)中的SqlSessionFactorybean要如何修改:…

回过头再看我在五-1问题排查中说的这句话,结合字段填充器的本质是entity的属性设置值,我们可以推断出来实体行为受到了影响,也就是MyBatis配置产生的影响。回过头来看一下 三-3 DruidDBConfig(DruidDB配置类)中的SqlSessionFactorybean针对于字段填充失效要如何修改:

    @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实现动态数据源切换》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值