整合 MyBatis

Spring Boot 中最简单的数据持久化方案 JdbcTemplate,JdbcTemplate 虽然简单,但是用的并不多,因为它没有 MyBatis 方便,在 Spring+SpringMVC 中整合 MyBatis 步骤还是有点复杂的,要配置多个 Bean,Spring Boot 中对此做了进一步的简化,使 MyBatis 基本上可以做到开箱即用,本文就来看看在 Spring Boot 中 MyBatis 要如何使用。

导入坐标

在pom文件中加入

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

数据源配置

基于上面的但数据源或者多数据源配都可以

注解方式使用

创建实体类
public class Entity {

    private Integer id;

    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Entity{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
注解方式使用
public interface TestMapper {

    @Select("select * from test")
    List<Entity> getAllDatas();

    @Results({
            @Result(property = "id", column = "id"),
            @Result(property = "name", column = "name")
    })
    @Select("select * from test where id=#{id}")
    Entity getById(Long id);

    @Select("select * from test where name like concat('%',#{name},'%')")
    List<Entity> getByName(String name);

    @Insert({"insert into test(name) values(#{name})"})
    @SelectKey(statement = "select last_insert_id()", keyProperty = "id", before = false, resultType = Integer.class)
    Integer addUser(Entity entity);

    @Update("update test set name=#{name} where id=#{id}")
    Integer updateById(Entity entity);

    @Delete("delete from test where id=#{id}")
    Integer deleteById(Integer id);

}

这里是通过全注解的方式来写 SQL,不写 XML 文件。

@Select、@Insert、@Update 以及 @Delete 四个注解分别对应 XML 中的 select、insert、update 以及 delete 标签,@Results 注解类似于 XML 中的 ResultMap 映射文件(getUserById 方法给查询结果的字段取别名主要是向小伙伴们演示下 @Results 注解的用法)。

另外使用 @SelectKey 注解可以实现主键回填的功能,即当数据插入成功后,插入成功的数据 id 会赋值到 user 对象的id 属性上。

配置启动类

TestMapper 创建好之后,还要配置 mapper 扫描,有两种方式,一种是直接在 TestMapper 上面添加 @Mapper 注解,这种方式有一个弊端就是所有的 Mapper 都要手动添加,要是落下一个就会报错,还有一个一劳永逸的办法就是直接在启动类上添加 Mapper 扫描,如下:

@SpringBootApplication
@MapperScan(basePackages = "com.test.demo.mapper")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

好了,做完这些工作就可以去测试 Mapper 的使用了。

测试
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoTest {

    @Autowired
    private TestMapper TestMapper;
    @Test
    public void test() throws SQLException {
        List<Entity>  dataList = TestMapper.getAllDatas();
        System.out.println(dataList);
    }
}

配置方式使用

mapper 接口

当然,开发者也可以在 XML 中写 SQL,例如创建一个 TestMapper,如下:

public interface TestMapper {

    List<Entity> getAllDatas();

    Integer addEntity(Entity entity);

    Integer updateById(Entity entity);

    Integer deleteById(Integer id);

}
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.test.demo.mapper.TestMapper">

    <resultMap type="com.test.demo.entity.Entity" id="entity">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>


    <select id="getAllDatas" parameterType="com.test.demo.entity.Entity" resultMap="entity">
        select * from test
    </select>
    <insert id="addEntity" parameterType="com.test.demo.entity.Entity">
        insert into test(name) values(#{name})
    </insert>
    <update id="updateById" parameterType="com.test.demo.entity.Entity">
        update user set username=#{username},address=#{address} where id=#{id}
    </update>
    <delete id="deleteById" parameterType="int">
        delete from user where id=#{id}
    </delete>
</mapper>

将接口中方法对应的 SQL 直接写在 XML 文件中。

POM文件配置XML编译

java 目录下的 xml 资源在项目打包时会被忽略掉,所以,如果 TestMapper.xml 放在包下,需要在 pom.xml 文件中再添加如下配置,避免打包时 java 目录下的 XML 文件被自动忽略掉:

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
        </resource>
    </resources>
</build>
配置application.properties

此时在 application.properties 中告诉 mybatis 去哪里扫描 mapper:

mybatis.mapper-locations=classpath:mapper/*.xml

如此配置之后,mapper 就可以正常使用了。注意这种方式不需要在 pom.xml 文件中配置文件过滤。

原理分析

在 SSM 整合中,开发者需要自己提供两个 Bean,一个SqlSessionFactoryBean ,还有一个是 MapperScannerConfigurer,在 Spring Boot 中,这两个东西虽然不用开发者自己提供了,但是并不意味着这两个 Bean 不需要了,在 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 类中,我们可以看到 Spring Boot 提供了这两个 Bean,部分源码如下:

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration implements InitializingBean {

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    return factory.getObject();
  }
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }
  @org.springframework.context.annotation.Configuration
  @Import({ AutoConfiguredMapperScannerRegistrar.class })
  @ConditionalOnMissingBean(MapperFactoryBean.class)
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug("No {} found.", MapperFactoryBean.class.getName());
    }
  }
}

从类上的注解可以看出,当当前类路径下存在 SqlSessionFactory、 SqlSessionFactoryBean 以及 DataSource 时,这里的配置才会生效,SqlSessionFactory 和 SqlTemplate 都被提供了。

动态源配置

在很多具体应用场景中,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动态数据源方案进行解决。接下来,我们就来讲解如何实现动态数据源,以及在过程中剖析动态数据源背后的实现原理。

数据源配置类

创建一个数据源配置类,主要做以下几件事情:

  1. 配置 dao,model,xml mapper文件的扫描路径。
  2. 注入数据源配置属性,创建master、slave数据源。
  3. 创建一个动态数据源,并装入master、slave数据源。
  4. 将动态数据源设置到SQL会话工厂和事务管理器。
动态数据源相关配置

当进行数据库操作时,就会通过我们创建的动态数据源去获取要操作的数据源了

数据源还是使用的原来配置的durid的多数据源

@Configuration
public class MybatisConfig {

    @Resource(name = "primaryDatasource")
    private DataSource master;
    @Resource(name = "secondDatasource")
    private DataSource slave;

    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put(DynamicDataSourceContextHolder.MASTER_DB, master);
        dataSourceMap.put(DynamicDataSourceContextHolder.SLAVE_DB, slave);
        // 将 master 数据源作为默认指定的数据源
        dynamicDataSource.setDefaultDataSource(master);
        // 将 master 和 slave 数据源作为指定的数据源
        dynamicDataSource.setDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换
        sessionFactory.setDataSource(dynamicDataSource());
        sessionFactory.setTypeAliasesPackage("com.test.demo.entity");    // 扫描Model
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));    // 扫描映射文件
        return sessionFactory;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        // 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}
动态数据源类

我们上一步把这个动态数据源设置到了SQL会话工厂和事务管理器,这样在操作数据库时就会通过动态数据源类来获取要操作的数据源了。

动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。

所以如果我们需要动态切换数据源,就可以通过以下两种方式定制:

  1. 覆写 determineCurrentLookupKey 方法

    通过覆写 determineCurrentLookupKey 方法,从一个自定义的 DynamicDataSourceContextHolder.getDataSourceKey() 获取数据源key值,这样在我们想动态切换数据源的时候,只要通过 DynamicDataSourceContextHolder.setDataSourceKey(key) 的方式就可以动态改变数据源了。这种方式要求在获取数据源之前,要先初始化各个数据源到 DynamicDataSource 中,我们案例就是采用这种方式实现的,所以在 MybatisConfig 中把master和slave数据源都事先初始化到DynamicDataSource 中。

  2. 覆写 determineTargetDataSource

    因为数据源就是在这个方法创建并返回的,所以这种方式就比较自由了,支持到任何你希望的地方读取数据源信息,只要最终返回一个 DataSource 的实现类即可。比如你可以到数据库、本地文件、网络接口等方式读取到数据源信息然后返回相应的数据源对象就可以了。

DynamicDataSource
/**
 * 动态数据源实现类
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    /**
     * 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
     * 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
     */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
     * 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 设置默认数据源
     *
     * @param defaultDataSource
     */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    /**
     * 设置数据源
     *
     * @param dataSources
     */
    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }
}
数据源上下文

动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。

主要方法介绍

切换数据源

在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。

/**
 * 切换数据源
 * @param key
 */
public static void setDataSourceKey(String key) {
    contextHolder.set(key);
}

重置数据源

将数据源重置回默认的数据源。默认数据源通过 DynamicDataSource.setDefaultDataSource(ds) 进行设置。

/**
 * 重置数据源
 */
public static void clearDataSourceKey() {
    contextHolder.remove();
}
DynamicDataSourceContextHolder
/**
 * 动态数据源上下文
 */
public class DynamicDataSourceContextHolder {
    public static final String MASTER_DB = "master";
    public static final String SLAVE_DB = "slave";

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 将 master 数据源的 key作为默认数据源的 key
         */
        @Override
        protected String initialValue() {
            return MASTER_DB;
        }
    };


    /**
     * 数据源的 key集合,用于切换时判断数据源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 切换数据源
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * 重置数据源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }

    /**
     * 判断是否包含数据源
     *
     * @param key 数据源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 添加数据源keys
     *
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

注解式数据源

到这里,在任何想要动态切换数据源的时候,只要调用 DynamicDataSourceContextHolder.setDataSourceKey(key) 就可以完成了。

POM配置AOP

因为要使用注解的方式所有需要导入aop支持

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建数据源注解

接下来我们实现通过注解的方式来进行数据源的切换,原理就是添加注解(如@DataSource(value=”master”)),然后实现注解切面进行数据源切换。

/**
 * 动态数据源注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    /**
     * 数据源key值 默认master
     *
     * @return
     */
    String value() default DynamicDataSourceContextHolder.MASTER_DB;

}
创建AOP切面

创建一个AOP切面,拦截带 @DataSource 注解的方法,在方法执行前切换至目标数据源,执行完成后恢复到默认数据源。

/**
 * 动态数据源注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    /**
     * 数据源key值 默认master
     *
     * @return
     */
    String value() default DynamicDataSourceContextHolder.MASTER_DB;
}