SpringBoot(十二):在多数据源下整合Mybatis

郎家岭伯爵 2023年07月21日 742次浏览

前言

SpringBoot 整合多数据源的 Mybatis

实现

创建一个新的 SpringBoot 项目。

本项目以连接两个数据源为例(数据源为本地 MySQL 中两个不同的数据库),大家按照实际需求进行连接。

pom.xml

pom.xml 文件引入以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.6</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0.1-android</version>
    <scope>compile</scope>
</dependency>

注:

  • 由于本项目连接的是两个 MySQL 数据源,如果要连接其它类型的数据库需要导入对应的依赖。

application.properties

application.properties 中添加如下配置项:

# 数据源1
spring.datasource.datasource1.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.datasource1.username=root
spring.datasource.datasource1.password=
spring.datasource.datasource1.driver-class-name=com.mysql.cj.jdbc.Driver

# 数据源2
spring.datasource.datasource2.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.datasource2.username=root
spring.datasource.datasource2.password=
spring.datasource.datasoutce2.driver-class-name=com.mysql.cj.jdbc.Driver

# 开启MyBatis的SQL语句输出
logging.level.com.langjialing.mybatismultisourcedemo.mapper.*=debug

Config配置类

我们把数据源的连接配置均写在 Config 配置类中,包括 Mapper 接口类和 Mapper 文件。

Datasource1Config.java

第一个数据源的配置类:

package com.langjialing.mybatismultisourcedemo.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/20 13:52
 */
@Configuration
@MapperScan(basePackages = {Datasource1Config.PACKAGE},
        sqlSessionFactoryRef = "datasource1SessionFactory")
public class Datasource1Config {

    static final String PACKAGE = "com.langjialing.mybatismultisourcedemo.mapper.datasource1";
    static final String MAPPER_LOCATION = "classpath:mapper/datasource1/*.xml";
    static final String CONFIG_LOCATION = "classpath:mybatis-config.xml";

    @Bean(name = "datasource1DruidDatasource")
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DruidDataSource datasource1DruidDatasource() {
        return new DruidDataSource();
    }

    @Bean(name = "datasource1SessionFactory")
    @Primary
    public SqlSessionFactory datasource1SessionFactory(@Qualifier("datasource1DruidDatasource") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(MAPPER_LOCATION));
        sessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource(CONFIG_LOCATION));
        sessionFactory.setObjectWrapperFactory(new MapWrapperFactory());
        return sessionFactory.getObject();
    }

    @Bean(name = "datasource1TransactionManager")
    public DataSourceTransactionManager datasource1TransactionManager() {
        return new DataSourceTransactionManager(datasource1DruidDatasource());
    }

    @Bean(name = "datasource1JdbcTemplate")
    public JdbcTemplate datasource1JdbcTemplate(@Qualifier("datasource1DruidDatasource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
  • @Configuration:这个注解标识这是一个配置类,告诉 SpringBoot 在启动时要读取该类中的配置。
  • @MapperScan:这个注解用于扫描指定包下的 MyBatis Mapper 接口,并注册这些 Mapper 接口的实现到 Spring 容器中。在这里,@MapperScan 扫描了包路径为 com.langjialing.mybatismultisourcedemo.mapper.datasource1Mapper 接口,将其注册到 Spring 容器中,并指定使用名为 datasource1SessionFactory 的 SqlSessionFactory
  • @Bean:这个注解用于标识一个方法产生一个 Spring Bean 对象,并将该对象注册到 Spring 容器中。在这里,@Bean 注解用于标识四个方法分别创建了不同的 Bean 实例。这些 Bean 包括 数据源(DataSource)SqlSessionFactory事务管理器(DataSourceTransactionManager)JdbcTemplate。其中,SqlSessionFactory 对象通过 SqlSessionFactoryBean 配置了 Mapper 文件的路径、MyBatis 配置文件的路径以及自定义的对象包装工厂。
  • @ConfigurationProperties(prefix = "spring.datasource.datasource1"):这个注解用于读取以 spring.datasource.datasource1 为前缀的配置属性,并将它们绑定到 datasource1DruidDatasource() 方法创建的 DruidDataSource 对象中。
  • @Qualifier("datasource1DruidDatasource"):这个注解用于指定依赖注入时所使用的 bean 名称。在这里,datasource1SessionFactory()datasource1JdbcTemplate() 方法中,通过 @Qualifier 注解指定使用名为 datasource1DruidDatasourceDataSource Bean
  • @Primary:这个注解用于解决多个相同类型的 Bean 注入时的歧义性。在这里,datasource1SessionFactory() 方法上使用了 @Primary 注解,表示当存在多个 SqlSessionFactory Bean 时,优先使用该方法所返回的 SqlSessionFactory Bean

Datasource2Config.java

第二个数据源的配置:

package com.langjialing.mybatismultisourcedemo.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/20 13:52
 */
@Configuration
@MapperScan(basePackages = {Datasource2Config.PACKAGE},
        sqlSessionFactoryRef = "datasource2SessionFactory")
public class Datasource2Config {

    static final String PACKAGE = "com.langjialing.mybatismultisourcedemo.mapper.datasource2";
    static final String MAPPER_LOCATION = "classpath:mapper/datasource2/*.xml";
    static final String CONFIG_LOCATION = "classpath:mybatis-config.xml";

    @Bean(name = "datasource2DruidDatasource")
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DruidDataSource dataSource2DruidDatasource() {
        return new DruidDataSource();
    }

    @Bean(name = "datasource2SessionFactory")
    @Primary
    public SqlSessionFactory datasource2SessionFactory(@Qualifier("datasource2DruidDatasource") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(MAPPER_LOCATION));
        sessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource(CONFIG_LOCATION));
        return sessionFactory.getObject();
    }

    @Bean(name = "datasource2TransactionManager")
    public DataSourceTransactionManager datasource2TransactionManager() {
        return new DataSourceTransactionManager(dataSource2DruidDatasource());
    }

    @Bean(name = "datasource2JdbcTemplate")
    public JdbcTemplate datasource2JdbcTemplate(@Qualifier("datasource2DruidDatasource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

其它配置类

在第一个数据源的配置类中有一行代码 sessionFactory.setObjectWrapperFactory(new MapWrapperFactory());,通过设置这个属性,可以来处理特定类型的查询结果对象,或者实现特定的属性访问规则。但不设置也是可以正常使用的

MapWrapperFactory.java

package com.langjialing.mybatismultisourcedemo.config;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;

import java.util.Map;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/21 9:56
 */
public class MapWrapperFactory implements ObjectWrapperFactory {
    @Override
    public boolean hasWrapperFor(Object object) {
        return object != null && object instanceof Map;
    }

    @Override
    public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
        return new CameCaseMapWrapper(metaObject, (Map) object);
    }
}
  • hasWrapperFor(Object object) 方法用于判断给定对象是否需要用该 ObjectWrapperFactory 进行处理。在这个实现中,它判断给定的对象是否为非空,并且是 Map 类型的对象。如果是,返回 true,表示该 ObjectWrapperFactory 可以处理这个对象。

  • getWrapperFor(MetaObject metaObject, Object object) 方法在需要处理该对象时被调用,用于创建相应的 ObjectWrapper。在这个实现中,它返回了一个自定义的 CameCaseMapWrapper 对象,它继承自 MapWrapper

CameCaseMapWrapper.java

package com.langjialing.mybatismultisourcedemo.config;

import com.google.common.base.CaseFormat;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.wrapper.MapWrapper;

import java.util.Map;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/21 9:57
 */
public class CameCaseMapWrapper extends MapWrapper {

    public CameCaseMapWrapper(MetaObject metaObject, Map<String, Object> map) {
        super(metaObject, map);
    }

    @Override
    public String  findProperty(String name, boolean useCamelCaseMapping) {
        if (useCamelCaseMapping) {
            return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
        }
        return name;
    }
}
  • CameCaseMapWrapper 继承自 MapWrapper,它通过调用父类的构造方法,传入 metaObjectmap 来初始化。

  • findProperty(String name, boolean useCamelCaseMapping)MapWrapper 中的一个方法,用于查找属性名。在这个实现中,useCamelCaseMapping 参数表示是否启用驼峰命名映射。如果启用了驼峰命名映射(useCamelCaseMappingtrue),则将会对属性名进行转换。

  • findProperty 方法中,如果启用了驼峰命名映射,它使用 GuavaCaseFormat 类将数据库下划线命名风格转换为 Java 驼峰命名风格。具体来说,它将字符串 name 从数据库下划线命名风格转换为 Java 驼峰命名风格。例如,对于数据库列名 first_name,在启用了驼峰命名映射后,findProperty 方法会将它转换为 firstName,以符合 Java 的命名约定。

Mapper接口类

Datasource1Mapper.java

第一个数据源的 Mapper 接口类:

package com.langjialing.mybatismultisourcedemo.mapper.datasource1;

import com.langjialing.mybatismultisourcedemo.entity.User;

import java.util.List;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/20 13:56
 */
public interface Datasource1Mapper {

    /**
     * 获取所有用户。
     * @return 所有用户。
     */
    List<User> getAll();

    /**
     * 根据用户ID获取用户。
     * @param id 用户ID。
     * @return 用户。
     */
    User getOne(Long id);

    /**
     * 新增用户。
     * @param user 用户。
     */
    void insert(User user);

    /**
     * 更新用户。
     * @param user 用户。
     */
    void update(User user);

    /**
     * 根据用户ID删除用户。
     * @param id 用户ID。
     */
    void delete(Long id);
}

Datasource2Mapper.java

第二个数据源的 Mapper 接口类 :

package com.langjialing.mybatismultisourcedemo.mapper.datasource2;

import com.langjialing.mybatismultisourcedemo.entity.User;

import java.util.List;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/20 13:57
 */
public interface Datasource2Mapper {

    /**
     * 获取所有用户。
     * @return 所有用户。
     */
    List<User> getAll();

    /**
     * 根据用户ID获取用户。
     * @param id 用户ID。
     * @return 用户。
     */
    User getOne(Long id);

    /**
     * 新增用户。
     * @param user 用户。
     */
    void insert(User user);

    /**
     * 更新用户。
     * @param user 用户。
     */
    void update(User user);

    /**
     * 根据用户ID删除用户。
     * @param id 用户ID。
     */
    void delete(Long id);
}

Mapper配置文件

Datasource1Mapper.xml

第一个数据源的 Mapper 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.langjialing.mybatismultisourcedemo.mapper.datasource1.Datasource1Mapper">
    <resultMap id="BaseResultMap" type="com.langjialing.mybatismultisourcedemo.entity.User">
        <id column="user_id" property="userId"/>
        <result column="user_name" property="userName"/>
        <result column="age" property="age"/>
        <result column="password" property="password"/>
    </resultMap>

    <sql id="Base_Column_List">
        user_id, user_name, age, password
    </sql>

    <select id="getAll" resultType="com.langjialing.mybatismultisourcedemo.entity.User">
        select
        <include refid="Base_Column_List" />
        from `user`
    </select>

    <select id="getOne" parameterType="java.lang.Long" resultType="com.langjialing.mybatismultisourcedemo.entity.User">
        SELECT
        <include refid="Base_Column_List" />
        FROM `user`
        WHERE user_id = #{id}
    </select>

    <insert id="insert" parameterType="com.langjialing.mybatismultisourcedemo.entity.User">
        insert into
            `user`
            (user_name,age,password)
        values
            (#{userName},#{age},#{password});
    </insert>
    <update id="update" parameterType="com.langjialing.mybatismultisourcedemo.entity.User">
        update
        `user`
        set
        <if test="userName != null">user_name=#{userName},</if>
        <if test="password != null">password=#{password},</if>
        age=#{age}
        where user_id=#{userId}
    </update>
    <delete id="delete">
        delete from
            `user`
        where
            user_id=#{id}
    </delete>
</mapper>

Datasource2Mapper.xml

第二个数据源的 Mapper 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.langjialing.mybatismultisourcedemo.mapper.datasource2.Datasource2Mapper">
    <resultMap id="BaseResultMap" type="com.langjialing.mybatismultisourcedemo.entity.User">
        <id column="user_id" property="userId"/>
        <result column="user_name" property="userName"/>
        <result column="age" property="age"/>
        <result column="password" property="password"/>
    </resultMap>

    <sql id="Base_Column_List">
        user_id, user_name, age, password
    </sql>

    <select id="getAll" resultType="com.langjialing.mybatismultisourcedemo.entity.User">
        select
        <include refid="Base_Column_List" />
        from `user`
    </select>

    <select id="getOne" parameterType="java.lang.Long" resultType="com.langjialing.mybatismultisourcedemo.entity.User">
        SELECT
        <include refid="Base_Column_List" />
        FROM `user`
        WHERE user_id = #{id}
    </select>

    <insert id="insert" parameterType="com.langjialing.mybatismultisourcedemo.entity.User">
        insert into
            `user`
            (user_name,age,password)
        values
            (#{userName},#{age},#{password});
    </insert>
    <update id="update" parameterType="com.langjialing.mybatismultisourcedemo.entity.User">
        update
        `user`
        set
        <if test="userName != null">user_name=#{userName},</if>
        <if test="password != null">password=#{password},</if>
        age=#{age}
        where user_id=#{userId}
    </update>
    <delete id="delete">
        delete from
            `user`
        where
            user_id=#{id}
    </delete>
</mapper>

Mybatis-config.xml

Mybatis-config 配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 设置全局配置项 -->
    <settings>
        <!-- 开启驼峰命名自动映射 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启自动映射的时候将未知的列映射到属性 -->
        <setting name="callSettersOnNulls" value="true"/>
    </settings>

    <!-- 设置类型别名 -->
    <typeAliases>
<!--        <typeAlias type="com.example.model.User" alias="User"/>-->
        <!-- 可以添加其他类型别名 -->
    </typeAliases>

    <!-- 设置插件(可选) -->
    <!--
    <plugins>
        <plugin interceptor="com.example.mybatis.MyPlugin">
            <property name="someProperty" value="100"/>
        </plugin>
    </plugins>
    -->

    <!-- 设置映射器(Mapper)的位置 -->
    <mappers>
<!--        <mapper resource="com/example/mapper/UserMapper.xml"/>-->
        <!-- 可以添加其他Mapper文件的位置 -->
    </mappers>

</configuration>

注:

  • 这个文件不加也是可以的,这样会加载默认配置。但不加这个文件的话要注意把 Datasource1Config.javaDatasource2Config.java 中的相关配置也去掉。

User实体类

package com.langjialing.mybatismultisourcedemo.entity;

import lombok.Builder;
import lombok.Data;
import lombok.experimental.Tolerate;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/20 14:33
 */
@Data
@Builder
public class User {
    private Integer userId;
    private Integer age;
    private String userName;
    private String password;

    @Tolerate
    User() {}
}

Controller层

在 Controller 层添加接口验证多数据源:

package com.langjialing.mybatismultisourcedemo.controller;

import com.langjialing.mybatismultisourcedemo.entity.User;
import com.langjialing.mybatismultisourcedemo.mapper.datasource1.Datasource1Mapper;
import com.langjialing.mybatismultisourcedemo.mapper.datasource2.Datasource2Mapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author 郎家岭伯爵
 * @time 2023/7/20 14:37
 */
@RestController
public class DatasourceController {
    @Resource
    private Datasource1Mapper datasource1Mapper;

    @Resource
    private Datasource2Mapper datasource2Mapper;

    @GetMapping("/datasource1/getAll")
    public List<User> getAllFromDatasource1() {
        return datasource1Mapper.getAll();
    }

    @GetMapping("/datasource2/getAll")
    public List<User> getAllFromDatasource2() {
        return datasource2Mapper.getAll();
    }
}

注:

  • 这里我们只写了一个 getAll() 方法,其它方法的使用与上一篇文章中的 Controller 是一样的,不再赘述。

测试多数据源

启动项目后,调用 Controller 层提供的接口。

第一个数据源

第二个数据源

console日志打印

总结

SpringBoot 整合多数据源的 Mybatis