1 - 1.Mybatis+xml整合篇

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

本文将通过实例方式,介绍下如何整合SpringBoot + Mybatis,构建一个支持CRUD的demo工程

I. 环境

本文使用SpringBoot版本为 2.2.1.RELEASE, mybatis版本为1.3.2,数据库为mysql 5+

1. 项目搭建

推荐是用官方的教程来创建一个SpringBoot项目; 如果直接创建一个maven工程的话,将下面配置内容,拷贝到你的pom.xml

  • 主要引入的是mybatis-spring-boot-starter,可以减少令人窒息的配置
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot-local</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/libs-milestone-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/libs-release-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

2. 配置信息

application.yml 配置文件中,加一下db的相关配置

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password:

接下来准备一个测试表(依然借用之前db操作系列博文中的表结构),用于后续的CURD;表结果信息如下

DROP TABLE IF EXISTS `money`;

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '有多少钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

II. 实例整合

本文将介绍一下传统的xml使用姿势,手动的添加PO, DAO, Mapper.xml;至于Generator来自动生成的case,后面通过图文的方式进行介绍

1. PO

创建表对应的PO对象: MoneyPo

@Data
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

知识点注意,上面的 createAt 与 表中的create_at,一个驼峰一个下划线,他们是怎么映射的呢?且看后文

2. DAO接口

表的操作接口,下面简单的写了四个接口,分别对应CRUID四种操作

@Mapper
public interface MoneyMapper {

    int savePo(@Param("po") MoneyPo po);

    List<MoneyPo> findByName(@Param("name") String name);

    int addMoney(@Param("id") int id, @Param("money") int money);

    int delPo(@Param("id") int id);
}

重点观察下上面接口的两个注解

  • @Mapper:声明这个为mybatis的dao接口,spring扫描到它之后,会自动生成对应的代理类
    • 使用这个注解之后,可以不再启动类上加上@MapperScan; 当然加上@MapperScan之后,也可以不用这个注解
  • @Param: 主要传递到xml文件中,方便参数绑定

这里简单说一下几种常见的参数传递方式

a. 单参数传递

如果只有一个基本类型的参数,可以直接使用参数名的使用方式

MoneyPo findById(int id);

对应的xml文件如下(先忽略include 与 resultMap), 可以直接用参数名

<select id="findById" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money where id=#{id}
</select>

b. 多参数默认传递

当接口定义有多个参数时,就不能直接使用参数名了,使用 arg0, arg1… (或者 param1, param2…)

实例如下

List<MoneyPo> findByNameAndMoney(String name, Integer money);

对应的xml

<select id="findByNameAndMoney" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
--         from money where name=#{param1} and money=#{param2}
    from money where name=#{arg0} and money=#{arg1}
</select>

c. @Param方式

就是上面case中的方式,xml中的参数就是注解的value;就不给演示了(后续的xml中可以看到使用姿势)

d. Map传参

接口定义一个Map<String, Object> 类型的参数,然后在xml中,就可以使用key的值来表明具体选中的是哪一个参数

List<MoneyPo> findByMap(Map<String, Object> map);

对应的xml如下,关于标签的用法主要是mybatis的相关知识点,这里不详细展开

<select id="findByMap" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money
    <trim prefix="WHERE" prefixOverrides="AND | OR">
        <if test="id != null">
            id = #{id}
        </if>
        <if test="name != null">
            AND name=#{name}
        </if>
        <if test="money != null">
            AND money=#{money}
        </if>
    </trim>
</select>

e. POJO传参

参数为一个POJO对象,实际使用中,通过成员名来确定具体的参数

List<MoneyPo> findByPo(MoneyPo po);

对应的xml如下,需要添加参数parameterType 指定POJO的类型

此外请额外注意下面的参数使用姿势和后面savePo接口对应的实现中参数的引用区别

<select id="findByPo" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" resultMap="BaseResultMap">
        select
    <include refid="money_po"/>
    from money
    <trim prefix="WHERE" prefixOverrides="AND | OR">
        <if test="id != null">
            id = #{id}
        </if>
        <if test="name != null">
            AND name=#{name}
        </if>
        <if test="money != null">
            AND money=#{money}
        </if>
    </trim>
</select>

3. xml实现

上面的Mapper接口中定义接口,具体的实现需要放在xml文件中,在我们的实例case中,xml文件放在 resources/sqlmapper目录下

文件名为money-mapper.xml, 没有什么特别的要求

<?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.git.hui.boot.mybatis.mapper.MoneyMapper">

    <resultMap id="BaseResultMap" type="com.git.hui.boot.mybatis.entity.MoneyPo">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="money" property="money" jdbcType="INTEGER"/>
        <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
        <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
        <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
    </resultMap>
    <sql id="money_po">
      id, name, money, is_deleted, create_at, update_at
    </sql>

    <insert id="savePo" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" useGeneratedKeys="true"
            keyProperty="po.id">
      INSERT INTO `money` (`name`, `money`, `is_deleted`)
      VALUES
	  (#{po.name}, #{po.money}, #{po.isDeleted});
    </insert>

    <update id="addMoney" parameterType="java.util.Map">
        update money set money=money+#{money} where id=#{id}
    </update>

    <delete id="delPo" parameterType="java.lang.Integer">
        delete from money where id = #{id,jdbcType=INTEGER}
    </delete>

    <select id="findByName" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="money_po"/>
        from money where name=#{name}
    </select>
</mapper>

在上面的xml文件中,除了四个接口对应的实现之外,还定义了一个resultMapsql

  • sql 标签定义通用的sql语句片段,通过<include refid="xxx"/>方式引入,避免写重复代码
  • resultMap: 定义表中数据与POJO成员的映射关系,比如将下划线的命名映射成驼峰

4. mybatis配置

上面基本上完成了整合工作的99%, 但是还有一个问题没有解决,mapper接口如何与xml文件关联起来?

  • xml文件中的mapper标签的namespace指定了具体的mapper接口, 表明这个xml文件对应的这个mapper

但是对于spring而言,并不是所有的xml文件都会被扫描的,毕竟你又不是 web.xml 这么有名(为什么web.xml就这么特殊呢😝, 欢迎查看我的Spring MVC之基于xml配置的web应用构建

为了解决xml配置扫描问题,请在 application.yml 文件中添加下面这一行配置

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

5. 测试

接下来简单测试一下上面的四个接口,看是否可以正常工作

启动类

@SpringBootApplication
public class Application {

    public Application(MoneyRepository repository) {
        repository.testMapper();
    }

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

测试类

@Repository
public class MoneyRepository {
    @Autowired
    private MoneyMapper moneyMapper;

    private Random random = new Random();

    public void testMapper() {
        MoneyPo po = new MoneyPo();
        po.setName("mybatis user");
        po.setMoney((long) random.nextInt(12343));
        po.setIsDeleted(0);

        moneyMapper.savePo(po);
        System.out.println("add record: " + po);
        moneyMapper.addMoney(po.getId(), 200);
        System.out.println("after addMoney: " + moneyMapper.findByName(po.getName()));
        moneyMapper.delPo(po.getId());
        System.out.println("after delete: " + moneyMapper.findByName(po.getName()));
    }
}

输出结果

II. 其他

0. 项目

2 - 2.Mybatis+注解整合篇

上一篇博文介绍了SpringBoot整合mybatis的过程,但是xml的方式,总感觉让人有点蛋疼;本文将介绍一种noxml的使用姿势,纯用注解的方式来支持CURD

I. 环境

本文使用SpringBoot版本为 2.2.1.RELEASE, mybatis版本为1.3.2,数据库为mysql 5+

1. 项目搭建

推荐是用官方的教程来创建一个SpringBoot项目; 如果直接创建一个maven工程的话,将下面配置内容,拷贝到你的pom.xml

  • 主要引入的是mybatis-spring-boot-starter,可以减少令人窒息的配置
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot-local</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/libs-milestone-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/libs-release-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

2. 配置信息

application.yml 配置文件中,加一下db的相关配置

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password:

接下来准备一个测试表(依然借用之前db操作系列博文中的表结构),用于后续的CURD;表结果信息如下

DROP TABLE IF EXISTS `money`;

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '有多少钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

II. 实例整合

在前一篇的基础上进行扩展,重点在于干掉了xml文件,在DAO接口上通过注解来实现CURD

1. PO

创建表对应的PO对象: MoneyPo

@Data
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

2. DAO接口

表的操作接口,下面简单的写了四个接口,分别对应CRUID四种操作

@Mapper
public interface MoneyMapper {

    // 支持主键写回到po

    @Options(useGeneratedKeys = true, keyProperty = "po.id", keyColumn = "id")
    @Insert("insert into money (name, money, is_deleted) values (#{po.name}, #{po.money}, #{po.isDeleted})")
    int savePo(@Param("po") MoneyPo po);

    @Select("select * from money where name=#{name}")
    @Results({@Result(property = "id", column = "id", id = true, jdbcType = JdbcType.INTEGER),
            @Result(property = "name", column = "name", jdbcType = JdbcType.VARCHAR),
            @Result(property = "money", column = "money", jdbcType = JdbcType.INTEGER),
            @Result(property = "isDeleted", column = "is_deleted", jdbcType = JdbcType.TINYINT),
            @Result(property = "createAt", column = "create_at", jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP)})
    List<MoneyPo> findByName(@Param("name") String name);

    @Update("update money set money=money+#{money} where id=#{id}")
    int addMoney(@Param("id") int id, @Param("money") int money);

    @Delete("delete from money where id = #{id,jdbcType=INTEGER}")
    int delPo(@Param("id") int id);

    @Select("<script> select * from money " +
            "<trim prefix=\"WHERE\" prefixOverrides=\"AND | OR\">" +
            "   <if test=\"id != null\">" +
            "       id = #{id}" +
            "   </if>" +
            "   <if test=\"name != null\">" +
            "       AND name=#{name}" +
            "   </if>" +
            "   <if test=\"money != null\">" +
            "       AND money=#{money}" +
            "   </if>" +
            "</trim>" +
            "</script>")
    @Results({@Result(property = "id", column = "id", id = true, jdbcType = JdbcType.INTEGER),
                @Result(property = "name", column = "name", jdbcType = JdbcType.VARCHAR),
                @Result(property = "money", column = "money", jdbcType = JdbcType.INTEGER),
                @Result(property = "isDeleted", column = "is_deleted", jdbcType = JdbcType.TINYINT),
                @Result(property = "createAt", column = "create_at", jdbcType = JdbcType.TIMESTAMP),
                @Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP)})
    List<MoneyPo> findByPo(MoneyPo po);
}

从mapper的实现上,也可以看出来,通过 @Insert, @Select, @Update, @Delete 四个注解来实现CURD,使用上面这种方式时,有几个点需要注意

  • insert: 当我们希望插入的主键写回到PO时,可以配置@Options(useGeneratedKeys = true, keyProperty = "po.id", keyColumn = "id")
  • 动态sql: 在注解中,通过<script>来包装动态sql
  • @Results 实现<resultMap>的映射关系

5. 测试

接下来简单测试一下上面的四个接口,看是否可以正常工作

启动类

@SpringBootApplication
public class Application {

    public Application(MoneyRepository repository) {
        repository.testMapper();
    }

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

测试类

@Repository
public class MoneyRepository {
    @Autowired
    private MoneyMapper moneyMapper;

    private Random random = new Random();

    public void testMapper() {
        MoneyPo po = new MoneyPo();
        po.setName("mybatis noxml user");
        po.setMoney((long) random.nextInt(12343));
        po.setIsDeleted(0);

        moneyMapper.savePo(po);
        System.out.println("add record: " + po);
        moneyMapper.addMoney(po.getId(), 200);
        System.out.println("after update: " + moneyMapper.findByName(po.getName()));
        moneyMapper.delPo(po.getId());
        System.out.println("after delete: " + moneyMapper.findByName(po.getName()));
    }
}

输出结果

II. 其他

0. 相关

推荐阅读

源码

3 - 3.MybatisPlus整合篇

前面介绍了SpringBoot整合Mybatis 实现db的增删改查操作,分别给出了xml和注解两种实现mapper接口的方式;虽然注解方式干掉了xml文件,但是使用起来并不优雅,本文将介绍mybats-plus的使用case,简化常规的CRUD操作

I. 环境

本文使用SpringBoot版本为 2.2.1.RELEASE, mybatis-plus版本为3.2.0,数据库为mysql 5+

1. 项目搭建

推荐是用官方的教程来创建一个SpringBoot项目; 如果直接创建一个maven工程的话,将下面配置内容,拷贝到你的pom.xml

  • 主要引入的是mybatis-spring-boot-starter,可以减少令人窒息的配置
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot-local</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/libs-milestone-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/libs-release-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

2. 配置信息

application.yml 配置文件中,加一下db的相关配置

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password:

接下来准备一个测试表(依然借用之前db操作系列博文中的表结构),用于后续的CURD;表结果信息如下

DROP TABLE IF EXISTS `money`;

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '有多少钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

II. 实例整合

mybatis-plus与mybatis的使用姿势有一些区别,下面为不借助generator直接手撸代码的case

1. PO

创建表对应的PO对象: MoneyPo

@Data
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

2. DAO接口

表的操作接口,与mybatis不同的是这个接口继承BaseMapper之后,就自带了单表的CURD操作接口了,基本上不需要定义额外的接口,就可以实现db交互

public interface MoneyMapper extends BaseMapper<MoneyPo> {
}
  • 注意BaseMapper的参数为表对应的PO对象

3. 测试

上面完成之后,整合过程基本上就完了,没错,就这么简单,接下来我们进入测试环节

首先是启动类,我们加上了@MapperScan注解,这样在DAO接口上就不需要添加@Mapper注解了

@SpringBootApplication
@MapperScan("com.git.hui.boot.mybatisplus.mapper")
public class Application {

    public Application(MoneyRepository repository) {
        repository.testMapper();
    }

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

关于测试case,下面会演示CRUD四种基本的操作case,因为本文重点不是介绍mybatis-plus的用法,对于下面代码有疑问的可以查看官方文档: https://mp.baomidou.com/guide/

@Component
public class MoneyRepository {
    @Autowired
    private MoneyMapper moneyMapper;

    private Random random = new Random();

    public void testDemo() {
        MoneyPo po = new MoneyPo();
        po.setName("mybatis plus user");
        po.setMoney((long) random.nextInt(12343));
        po.setIsDeleted(0);

        // 添加一条数据
        moneyMapper.insert(po);

        // 查询
        List<MoneyPo> list =
                moneyMapper.selectList(new QueryWrapper<MoneyPo>().lambda().eq(MoneyPo::getName, po.getName()));
        System.out.println("after insert: " + list);

        // 修改
        po.setMoney(po.getMoney() + 300);
        moneyMapper.updateById(po);
        System.out.println("after update: " + moneyMapper.selectById(po.getId()));

        // 删除
        moneyMapper.deleteById(po.getId());

        // 查询
        Map<String, Object> queryMap = new HashMap<>(2);
        queryMap.put("name", po.getName());
        System.out.println("after delete: " + moneyMapper.selectByMap(queryMap));
    }
}

输出结果

II. 其他

0. 项目

4 - 4.Mybatis-Plus代码自动生成

一个简单的实例工程,介绍利用mybatis-plus的代码自动生成插件,根据表结构来生成对应的类和xml配置文件

I. 代码生成

本文主要内容来自官方教程,通过实例方式介绍代码生成过程

1. 准备

准备两张表,用于测试

CREATE TABLE `userT0` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `pwd` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
  `isDeleted` tinyint(1) NOT NULL DEFAULT '0',
  `created` varchar(13) NOT NULL DEFAULT '0',
  `updated` varchar(13) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `story_t0` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `userId` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者的userID',
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '作者名',
  `title` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
  `story` text COMMENT '故事内容',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` varchar(13) NOT NULL DEFAULT '0',
  `update_at` varchar(13) NOT NULL DEFAULT '0',
  `tag` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `userId` (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

请注意,上面两张表的命名格式并不一样,有的是驼峰,有的是下划线(主要为了演示不同表名,对于生成代码的影响)

2. 配置依赖

首先需要在我们的xml文件中,添加相关的依赖

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.3.1.tmp</version>
    </dependency>
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <!-- 下面两个用于测试生成后的代码在生成代码时可以不需要-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.2.0</version>
    </dependency>
</dependencies>

3. 代码生成类

写一个代码生成类方法,主要逻辑如下

public class CodeGenerator {
    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir") + "/spring-boot/106-mybatis-plus-generator";
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("YiHui");
        gc.setOpen(false);
        // 覆盖写
        gc.setFileOverride(false);
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        // 不额外指定模块,如果指定为 test,则生成的xml会在 mapper/test/ 目录下
        pc.setModuleName("");
        pc.setParent("com.git.hui.boot.mybatis.plus");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" +
                        tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        templateConfig.setXml(null);
        // 不自动生成controller类
        templateConfig.setController(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        // 公共父类
        // strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
        // strategy.setSuperEntityColumns("id");

        // 设置需要生成的表名
        strategy.setInclude("userT0", "story_t0");
        strategy.setControllerMappingHyphenStyle(true);
        // strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

上面的代码,绝大部分都是通用的,下面着重说明需要注意的点

  • GlobalConfig#setOutputDir: 设置代码输出的项目根路径,请根据具体的项目要求进行指定,不包含包名哦
  • GlobalConfig#setFileOverride(true): 设置为true,则每次生成都会覆盖之前生成的代码,适用于表结构发生变化的场景
    • 注意:会导致之前添加的业务代码被覆盖掉,需要额外注意
    • 通常希望设置为false,当表结构发生变化时,手动介入
  • DataSourceConfig: 数据源的设置,上面设置的是mysql的相关配置
  • PackageConfig: 包信息
    • setParent: java包路径
    • setModuleName: 设置模块名,如设置为test,则xml在mapper/test/目录下; parent包自动加上.test
  • FileOutConfig: xml文件名
  • TemplateConfig: 模板配置
    • 可用默认的代码生成模板,也可以使用自定义的模板
    • 不想生成某个模板类时,设置为null即可(如上面的不生成controller)
  • StrategyConfig: 策略配置
    • 可以指定db->pojo字段名的映射规则
    • 可以指定POJO/Controller继承自定义的基类

在IDEA中,直接右键执行上面的代码,就会生成目标类,如下截图

4. 输出测试

测试我们生成的类,是否可以对db进行操作,则有必要写一个启动类

@RestController
@SpringBootApplication
@MapperScan("com.git.hui.boot.mybatis.plus.mapper")
public class Application {
    @Autowired
    private IUserT0Service userT0Service;

    @GetMapping
    public UserT0 hello(int id) {
        return userT0Service.getById(id);
    }

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

请注意上面的@MapperScan注解,其次对应的application.yml配置文件内容如下

spring:
  datasource:
    # 注意指定时区
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

mybatis-plus:
  configuration:
    # 执行的sql语句日志输出
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

在db中插入一条数据

INSERT INTO `userT0` (`id`, `name`, `pwd`, `isDeleted`, `created`, `updated`)
VALUES
	(1, '一灰灰', 'yihuihuiblog', 0, '2020-04-06 15', '2020-04-06 15');

访问url: http://localhost:8080/?id=1

控制台输出如下:

5. 特殊场景说明

上面的代码生成,针对首次执行生成打码时,问题不大;但是后续的业务开发中,总会有一些其他的情况,下面分别说明

a. 表结构修改

当表的结构发生变化时,我们需要一般需要重新生成对应的Entity,这个时候,需要GlobalConfig#setFileOverride(true)

b. 继承公用POJO

我们可以定义一个通用的PO类,希望所有的表生成的POJO继承它

@Data
public class BasePo implements Serializable {
    private static final long serialVersionUID = -1136173266983480386L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

}

在代码自动生成类的策略配置中,添加下面的两行设置即可

// 所有实体类继承自 BasePo, 且id在父类中
StrategyConfig strategy = new StrategyConfig();
strategy.setSuperEntityClass(BasePo.class);
strategy.setSuperEntityColumns("id");

c. 生成部分代码

有些时候,我并不希望生成service,xml,可能就只需要实体类 + mapper接口,这个时候可以设置TemplateConfig

TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setController(null);
templateConfig.setEntityKt(null);
templateConfig.setService(null);
templateConfig.setServiceImpl(null);

II. 其他

0. 项目

系列博文

源码

5 - 5.Mybatis多数据源配置与使用

上一篇博文介绍JdbcTemplate配置多数据源的使用姿势,在我们实际的项目开发中,使用mybatis来操作数据库的可能还是非常多的,本文简单的介绍一下mybatis中,多数据源的使用姿势

  • 通过区分包路径配合配置文件指定不同包下对应不同数据源的实现方式

I. 环境准备

1. 数据库相关

以mysql为例进行演示说明,因为需要多数据源,一个最简单的case就是一个物理库上多个逻辑库,本文是基于本机的mysql进行操作

创建数据库teststory,两个库下都存在一个表money (同名同结构表,但是数据不同哦)

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

下面是核心的pom.xml(源码可以再文末获取)

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

配置文件信息application.yml

# 数据库相关配置
spring:
  datasource:
    story:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password:
    test:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password:


# 日志相关
logging:
  level:
    root: info
    org:
      springframework:
        jdbc:
          core: debug

请注意上面的数据库配置,我们前面介绍的但数据库配置如下,它们层级并不一样,上面的配置需要我们自己额外进行加载解析

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

II. 包路径指定

这种实现方式和前文中JdbcTemplate的多数据源配置方式很类似,将不同数据源的Mapper文件拆分在不同的包中,然后在配置mybatis数据源及资源文件加载时,分别进行指定

1. 项目结构

本项目中使用story + test两个数据库,我们将不同数据库的mapper.xml以及对应的实体相关类都分别放开,如下图

2. 具体实现

因为两个库中表结构完全一致,所以上图中的 Entity, Mapper, Repository以及xml文件基本都是一致的,下面代码只给出其中一份

数据库实体类StoryMoneyEntity

@Data
public class StoryMoneyEntity {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

xml对应的Mapper接口StoryMoneyMapper

@Mapper
public interface StoryMoneyMapper {
    List<StoryMoneyEntity> findByIds(List<Integer> ids);
}

mapper对应的xml文件

<?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.git.hui.boot.multi.datasource.story.mapper.StoryMoneyMapper">

    <resultMap id="BaseResultMap" type="com.git.hui.boot.multi.datasource.story.entity.StoryMoneyEntity">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="money" property="money" jdbcType="INTEGER"/>
        <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
        <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
        <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
    </resultMap>
    <sql id="money_po">
      id, `name`, money, is_deleted, create_at, update_at
    </sql>

    <select id="findByIds" parameterType="list" resultMap="BaseResultMap">
        select
        <include refid="money_po"/>
        from money where id in
        <foreach item="id" collection="list" separator="," open="(" close=")" index="">
            #{id}
        </foreach>
    </select>
</mapper>

数据库操作封装类StoryMoneyRepository

@Repository
public class StoryMoneyRepository {
    @Autowired
    private StoryMoneyMapper storyMoneyMapper;

    public void query() {
        List<StoryMoneyEntity> list = storyMoneyMapper.findByIds(Arrays.asList(1, 1000));
        System.out.println(list);
    }
}

接下来的重点看一下数据源以及Mybatis的相关配置StoryDatasourceConfig

// 请注意下面这个MapperScan,将数据源绑定在对应的包路径下
@Configuration
@MapperScan(basePackages = "com.git.hui.boot.multi.datasource.story.mapper", sqlSessionFactoryRef = "storySqlSessionFactory")
public class StoryDatasourceConfig {

    // 从配置文件中,获取数据库的相关配置
    @Primary
    @Bean(name = "storyDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.story")
    public DataSourceProperties storyDataSourceProperties() {
        return new DataSourceProperties();
    }

    // DataSource的实例创建
    @Primary
    @Bean(name = "storyDataSource")
    public DataSource storyDataSource(@Qualifier("storyDataSourceProperties") DataSourceProperties storyDataSourceProperties) {
        return storyDataSourceProperties.initializeDataSourceBuilder().build();
    }

    // ibatis 对应的SqlSession工厂类
    @Primary
    @Bean("storySqlSessionFactory")
    public SqlSessionFactory storySqlSessionFactory(DataSource storyDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(storyDataSource);
        bean.setMapperLocations(
                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/story/*.xml"));
        return bean.getObject();
    }

    @Primary
    @Bean("storySqlSessionTemplate")
    public SqlSessionTemplate storySqlSessionTemplate(SqlSessionFactory storySqlSessionFactory) {
        return new SqlSessionTemplate(storySqlSessionFactory);
    }
}

另外一个数据源的配置文件则如下

@Configuration
@MapperScan(basePackages = "com.git.hui.boot.multi.datasource.test.mapper", sqlSessionFactoryRef = "testSqlSessionFactory")
public class TestDatasourceConfig {

    @Bean(name = "testDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.test")
    public DataSourceProperties testDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "testDataSource")
    public DataSource testDataSource(@Qualifier("testDataSourceProperties") DataSourceProperties storyDataSourceProperties) {
        return storyDataSourceProperties.initializeDataSourceBuilder().build();
    }

    @Bean("testSqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("testDataSource") DataSource testDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(testDataSource);
        bean.setMapperLocations(
                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test/*.xml"));
        return bean.getObject();
    }

    @Bean("testSqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory testSqlSessionFactory) {
        return new SqlSessionTemplate(testSqlSessionFactory);
    }
}

3. 测试

简单测试一下是否生效,直接在启动类中,调用

@SpringBootApplication
public class Application {

    public Application(StoryMoneyRepository storyMoneyRepository, TestMoneyRepository testMoneyRepository) {
        storyMoneyRepository.query();
        testMoneyRepository.query();
    }

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

}

输出如下

4. 小结

本文中介绍的多数据源方式,其实和但数据源的mybatis配置方式基本一致,顶多就是SpringBoot中,遵循默认的规范不需要我们显示的创建DataSource实例、SqlSessionFactory实例等

上面介绍的方式,实际上就是显示的声明Mybatis配置过程,多一个数据源,就多一个相关的配置,好处是理解容易,缺点是不灵活,如果我的Mapper类放错位置,可能就会出问题了

那么有其他的方式么,如果我希望将所有的Mapper放在一个包路径下,可以支持么?

下一篇博文,将介绍一种基于AbstractRoutingDataSource + 注解的方式来实现多数据源的支持

II. 其他

0. 项目

相关博文

源码

6 - 6.Mybatis-Plus多数据源配置

前面介绍了两种Mybatis的数据源配置,当然也少不了mybatis-plus

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,既然做增强,那多数据源这种硬性场景,肯定是有非常简单的解决方案的

本文将实例演示Mybatis-Plus多数据源的配置

I. 环境准备

1. 数据库相关

以mysql为例进行演示说明,因为需要多数据源,一个最简单的case就是一个物理库上多个逻辑库,本文是基于本机的mysql进行操作

创建数据库teststory,两个库下都存在一个表money (同名同结构表,但是数据不同哦)

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

下面是核心的pom.xml(源码可以再文末获取)

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>
</dependencies>

配置文件信息application.yml,请注意下面的写法格式,如有疑问可以参考官方教程

spring:
  datasource:
    dynamic:
      primary: story #设置默认的数据源或者数据源组,默认值即为master
      strict: false  #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
      datasource:
        story:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password:
        test:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password:

II. 项目演示

本文主要参考自Mybatis-Plus官方教程,如后续版本有啥变动,请以官方说明为准 https://mp.baomidou.com/guide/dynamic-datasource.html#%E6%96%87%E6%A1%A3-documentation

1. 实体类

mybatis-plus可以借助插件实现自动生成相应的代码,我们这里简单自主实现测试demo,因为两个数据库中表结构完全一致,所以只需要一个Entity

@Data
@Accessors(chain = true)
@TableName(value = "money")
public class MoneyPo {
    /**
     * 指定自增策略
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String name;

    private Long money;

    @TableField("is_deleted")
    private Integer isDeleted;

    @TableField(value = "create_at")
    private Timestamp createAt;

    @TableField(value = "update_at")
    private Timestamp updateAt;
}

2. Mapper接口

数据库操作定义接口MoneyMapper

public interface MoneyMapper extends BaseMapper<MoneyPo> {
}

对应的xml文件resources/mapper/money-mapper.xml

<?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.git.hui.boot.multi.datasource.mapper.MoneyMapper">
</mapper>

3. Service接口与实现

因为两张表,所以我们可以定义一个接口,两个不同的实现

public interface MoneyService extends IService<MoneyPo> {
}

@Service
@DS("story")
public class StoryMoneyServiceImpl extends ServiceImpl<MoneyMapper, MoneyPo> implements MoneyService {
}

@Service
@DS("test")
public class TestMoneyServiceImpl extends ServiceImpl<MoneyMapper, MoneyPo> implements MoneyService {
}

请注意上面Service的注解@DS,value为前面数据源配置文件中的key(spring.datasource.dynamic.datasource下面的story + test)

这个注解可以放在类上也可以放在方法上,方法上的优先级 > 类,所以上面的两个Service实现可以改成一个

@Service
public class MoneyServiceImpl extends ServiceImpl<MoneyMapper, MoneyPo> implements MoneyService {

    @DS("story")
    public List<MoneyPo> findByStoryIds(Collection<Long> ids) {
        return baseMapper.selectBatchIds(ids);
    }

    @DS("test")
    public List<MoneyPo> findByTestIds(Collection<Long> ids) {
        return baseMapper.selectBatchIds(ids);
    }
}

4. 测试

为简单起见,直接在启动类中添加写上测试代码

@SpringBootApplication
@MapperScan("com.git.hui.boot.multi.datasource.mapper")
public class Application {

    public Application(TestMoneyServiceImpl testMoneyService, StoryMoneyServiceImpl storyMoneyService) {
        List<MoneyPo> moneyPoList = testMoneyService.listByIds(Arrays.asList(1, 1000));
        System.out.println(moneyPoList);
        System.out.println("--------------");

        moneyPoList = storyMoneyService.listByIds(Arrays.asList(1, 1000));
        System.out.println(moneyPoList);
    }

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

II. 其他

0. 项目

相关博文

源码

7 - 7.基于AbstractRoutingDataSource与AOP实现多数据源切换

前面一篇博文介绍了Mybatis多数据源的配置,简单来讲就是一个数据源一个配置指定,不同数据源的Mapper分开指定;本文将介绍另外一种方式,借助AbstractRoutingDataSource来实现动态切换数据源,并通过自定义注解方式 + AOP来实现数据源的指定

I. 环境准备

1. 数据库相关

以mysql为例进行演示说明,因为需要多数据源,一个最简单的case就是一个物理库上多个逻辑库,本文是基于本机的mysql进行操作

创建数据库teststory,两个库下都存在一个表money (同名同结构表,但是数据不同哦)

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

下面是核心的pom.xml(源码可以再文末获取)

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

配置文件信息application.yml

# 数据库相关配置,请注意这个配置和之前一篇博文的不一致,后面会给出原因
spring:
  dynamic:
    datasource:
      story:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password:
      test:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password:


# 日志相关
logging:
  level:
    root: info
    org:
      springframework:
        jdbc:
          core: debug

II. 多数据源配置

强烈建议没有看上一篇博文的小伙伴,先看一下上篇博文 【DB系列】Mybatis多数据源配置与使用

在开始之前,先有必要回顾一下之前Mybatis多数据源配置的主要问题在哪里

  • 多加一个数据源,需要多一份配置
  • Mapper文件需要分包处理,对开发人员而言这是个潜在的坑

针对上面这个,那我们想实现的目的也很清晰了,解决上面两个问题

1. AbstractRoutingDataSource

实现多数据源的关键,从名字上就可以看出,它就是用来路由具体的数据源的,其核心代码如

// 返回选中的数据源
protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = this.determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }

    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}

@Nullable
protected abstract Object determineCurrentLookupKey();

其中determineCurrentLookupKey需要我们自己来实现,到底返回哪个数据源

2. 动态数据源实现

我们创建一个DynamicDataSource继承自上面的抽象类

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataBaseType = DSTypeContainer.getDataBaseType();
        return dataBaseType;
    }
}

注意上面的实现方法,怎样决定具体的返回数据源呢?

一个可考虑的方法是,在Mapper文件上添加一个注解@DS,里面指定对应的数据源,然后再执行时,通过它来确定具体需要执行的数据源;

因为上面的实现没有传参,因此我们考虑借助线程上下文的方式来传递信息

public class DSTypeContainer {
    private static final ThreadLocal<String> TYPE = new ThreadLocal<String>();

    public static String defaultType;

    /**
     * 往当前线程里设置数据源类型
     *
     * @param dataBase
     */
    public static void setDataBaseType(String dataBase) {
        if (StringUtils.isEmpty(dataBase)) {
            dataBase = defaultType;
        }
        TYPE.set(dataBase);
        System.err.println("[将当前数据源改为]:" + dataBase);
    }

    /**
     * 获取数据源类型
     *
     * @return
     */
    public static String getDataBaseType() {
        String database = TYPE.get();
        System.err.println("[获取当前数据源的类型为]:" + database);
        return database;
    }

    /**
     * 清空数据类型
     */
    public static void clearDataBaseType() {
        TYPE.remove();
    }
}

3. 注解实现

上面虽然给出了数据源选择的策略,从线程上下文中获取DataBaseType,但是应该怎样向线程上下文中塞这个数据呢?

我们需要支持的方案必然是在Sql执行之前,先拦截它,写入这个DataBaseType,因此我们可以考虑在xxxMapper接口上,定义一个注解,然后拦截它的访问执行,在执行之前获取注解中指定的数据源写入上下文,在执行之后清楚上下文

一个最基础的数据源注解@DS

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DS {
    String value() default "";
}

注解拦截

@Aspect
@Component
public class DsAspect {

    // 拦截类上有DS注解的方法调用
    @Around("@within(DS)")
    public Object dsAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        DS ds = (DS) proceedingJoinPoint.getSignature().getDeclaringType().getAnnotation(DS.class);
        try {
            // 写入线程上下文,应该用哪个DB
            DSTypeContainer.setDataBaseType(ds == null ? null : ds.value());
            return proceedingJoinPoint.proceed();
        } finally {
            // 清空上下文信息
            DSTypeContainer.clearDataBaseType();
        }
    }
}

4. 注册配置

接下来就是比较关键的数据源配置了,我们现在需要注册DynamicDataSource,然后将他提供给SqlSessionFactory,在这里,我们希望解决即便多加数据源也不需要修改配置,所以我们调整了一下数据源的配置结构

spring:
  dynamic:
    datasource:
      story:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password:
      test:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password:

然后给出一个加载上面配置的配置类DSProperties

@Data
@ConfigurationProperties(prefix = "spring.dynamic")
public class DSProperties {
    private Map<String, DataSourceProperties> datasource;
}

然后我们的AutoConfiguration类的实现方式就相对明确了(建议对比上一篇博文中的配置类)

@Configuration
@EnableConfigurationProperties(DSProperties.class)
@MapperScan(basePackages = {"com.git.hui.boot.multi.datasource.mapper"},
        sqlSessionFactoryRef = "SqlSessionFactory")
public class DynamicDataSourceConfig {

    @SuppressWarnings("unchecked")
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(DSProperties dsProperties) {
        Map targetDataSource = new HashMap<>(8);
        dsProperties.getDatasource().forEach((k, v) -> {
            targetDataSource.put(k, v.initializeDataSourceBuilder().build());
        });
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);

        // 设置默认的数据库,下面这个赋值方式写法不太推荐,这里只是为了方便而已
        DSTypeContainer.defaultType = (String) targetDataSource.keySet().stream().findFirst().get();
        dataSource.setDefaultTargetDataSource(targetDataSource.get(DSTypeContainer.defaultType));
        return dataSource;
    }

    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*/*.xml"));
        return bean.getObject();
    }
}

5. 数据库实体类

项目结构图

所有前面的东西属于通用配置相关,接下来给出具体的数据库操作相关实体类、Mapper类

数据库实体类StoryMoneyEntity

@Data
public class StoryMoneyEntity {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

mapper定义接口 StoryMoneyMapper + TestMoneyMapper

@DS(value = "story")
@Mapper
public interface StoryMoneyMapper {
    List<StoryMoneyEntity> findByIds(List<Integer> ids);
}

@DS(value = "test")
@Mapper
public interface TestMoneyMapper {
    List<TestMoneyEntity> findByIds(List<Integer> ids);
}

对应的xml文件

<?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.git.hui.boot.multi.datasource.mapper.StoryMoneyMapper">
    <resultMap id="BaseResultMap" type="com.git.hui.boot.multi.datasource.entity.StoryMoneyEntity">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="money" property="money" jdbcType="INTEGER"/>
        <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
        <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
        <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
    </resultMap>
    <sql id="money_po">
      id, `name`, money, is_deleted, create_at, update_at
    </sql>

    <select id="findByIds" parameterType="list" resultMap="BaseResultMap">
        select
        <include refid="money_po"/>
        from money where id in
        <foreach item="id" collection="list" separator="," open="(" close=")" index="">
            #{id}
        </foreach>
    </select>
</mapper>

<!-- 省略第二个xml文件 内容基本一致-->

数据库操作封装类StoryMoneyRepository + TestMoneyRepository

@Repository
public class StoryMoneyRepository {
    @Autowired
    private StoryMoneyMapper storyMoneyMapper;

    public void query() {
        List<StoryMoneyEntity> list = storyMoneyMapper.findByIds(Arrays.asList(1, 1000));
        System.out.println(list);
    }
}

@Repository
public class TestMoneyRepository {
    @Autowired
    private TestMoneyMapper testMoneyMapper;

    public void query() {
        List<TestMoneyEntity> list = testMoneyMapper.findByIds(Arrays.asList(1, 1000));
        System.out.println(list);
    }
}

6. 测试

最后简单的测试下,动态数据源切换是否生效

@SpringBootApplication
public class Application {

    public Application(StoryMoneyRepository storyMoneyRepository, TestMoneyRepository testMoneyRepository) {
        storyMoneyRepository.query();
        testMoneyRepository.query();
    }

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

输出日志如下

6.小结

本文主要给出了一种基于AbstractRoutingDataSource + AOP实现动态数据源切换的实现方式,使用了下面三个知识点

  • AbstractRoutingDataSource实现动态数据源切换
  • 自定义@DS注解 + AOP指定Mapper对应的数据源
  • ConfigurationProperties方式支持添加数据源无需修改配置

II. 其他

0. 项目

相关博文

源码

8 - 8.SpringBoot系列Mybatis之Mapper注册的几种方式

SpringBoot项目中借助Mybatis来操作数据库,对大部分java技术栈的小伙伴来说,并不会陌生;我们知道,使用mybatis,一般会有下面几个

  • Entity: 数据库实体类
  • Mapper: db操作接口
  • Service: 服务类

本片博文中的注解,放在Mapper上,你知道注册Mapper有几种方式么(这个问题像不像"茴"字有几个写法😬)

I. 环境准备

1. 数据库准备

使用mysql作为本文的实例数据库,新增一张表

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

pom依赖如下

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

db配置信息 application.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

II. 实例演示

前面基础环境搭建完成,接下来准备下Mybatis的Entity,Mapper等基础类

1. 实体类,Mapper类

数据库实体类MoneyPo

@Data
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

对应的Mapper接口(这里直接使用注解的方式来实现CURD)

public interface MoneyMapper {

    /**
     * 保存数据,并保存主键id
     *
     * @param po
     * @return int
     */
    @Options(useGeneratedKeys = true, keyProperty = "po.id", keyColumn = "id")
    @Insert("insert into money (name, money, is_deleted) values (#{po.name}, #{po.money}, #{po.isDeleted})")
    int save(@Param("po") MoneyPo po);

    /**
     * 更新
     *
     * @param id    id
     * @param money 钱
     * @return int
     */
    @Update("update money set `money`=#{money} where id = #{id}")
    int update(@Param("id") int id, @Param("money") long money);

    /**
     * 删除数据
     *
     * @param id id
     * @return int
     */
    @Delete("delete from money where id = #{id}")
    int delete(@Param("id") int id);

    /**
     * 主键查询
     *
     * @param id id
     * @return {@link MoneyPo}
     */
    @Select("select * from money where id = #{id}")
    @Results(id = "moneyResultMap", value = {
            @Result(property = "id", column = "id", id = true, jdbcType = JdbcType.INTEGER),
            @Result(property = "name", column = "name", jdbcType = JdbcType.VARCHAR),
            @Result(property = "money", column = "money", jdbcType = JdbcType.INTEGER),
            @Result(property = "isDeleted", column = "is_deleted", jdbcType = JdbcType.TINYINT),
            @Result(property = "createAt", column = "create_at", jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP)})
    MoneyPo getById(@Param("id") int id);
}

对应的Service类

@Slf4j
@Service
public class MoneyService {
    @Autowired
    private MoneyMapper moneyMapper;

    public void basicTest() {
        int id = save();
        log.info("save {}", getById(id));
        boolean update = update(id, 202L);
        log.info("update {}, {}", update, getById(id));
        boolean delete = delete(id);
        log.info("delete {}, {}", delete, getById(id));
    }

    private int save() {
        MoneyPo po = new MoneyPo();
        po.setName("一灰灰blog");
        po.setMoney(101L);
        po.setIsDeleted(0);
        moneyMapper.save(po);
        return po.getId();
    }

    private boolean update(int id, long newMoney) {
        int ans = moneyMapper.update(id, newMoney);
        return ans > 0;
    }

    private boolean delete(int id) {
        return moneyMapper.delete(id) > 0;
    }

    private MoneyPo getById(int id) {
        return moneyMapper.getById(id);
    }
}

2. 注册方式

注意,上面写完之后,若不通过下面的几种方式注册Mapper接口,项目启动会失败,提示找不到MoneyMapper对应的bean

Field moneyMapper in com.git.hui.boot.mybatis.service.MoneyService required a bean of type 'com.git.hui.boot.mybatis.mapper.MoneyMapper' that could not be found.

2.1 @MapperScan注册方式

在配置类or启动类上,添加@MapperScan注解来指定Mapper接口的包路径,从而实现Mapper接口的注册

@MapperScan(basePackages = "com.git.hui.boot.mybatis.mapper")
@SpringBootApplication
public class Application {

    public Application(MoneyService moneyService) {
        moneyService.basicTest();
    }

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

执行之后输出结果如下

2021-07-06 19:12:57.984  INFO 1876 --- [           main] c.g.h.boot.mybatis.service.MoneyService  : save MoneyPo(id=557, name=一灰灰blog, money=101, isDeleted=0, createAt=2021-07-06 19:12:57.0, updateAt=2021-07-06 19:12:57.0)
2021-07-06 19:12:58.011  INFO 1876 --- [           main] c.g.h.boot.mybatis.service.MoneyService  : update true, MoneyPo(id=557, name=一灰灰blog, money=202, isDeleted=0, createAt=2021-07-06 19:12:57.0, updateAt=2021-07-06 19:12:57.0)
2021-07-06 19:12:58.039  INFO 1876 --- [           main] c.g.h.boot.mybatis.service.MoneyService  : delete true, null

注意:

  • basePackages: 传入Mapper的包路径,数组,可以传入多个
  • 包路径支持正则,如com.git.hui.boot.*.mapper
    • 上面这种方式,可以避免让我们所有的mapper都放在一个包路径下,从而导致阅读不友好

2.2 @Mapper 注册方式

前面的@MapperScan指定mapper的包路径,这个注解则直接放在Mapper接口上

@Mapper
public interface MoneyMapper {
...
}

测试输出省略…

2.3 MapperScannerConfigurer注册方式

使用MapperScannerConfigurer来实现mapper接口注册,在很久以前,还是使用Spring的xml进行bean的声明的时候,mybatis的mapper就是这么玩的

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="xxx"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

对应的java代码如下:

@Configuration
public class AutoConfig {
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(
                // 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
        return bean.getObject();
    }

    @Bean("sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory storySqlSessionFactory) {
        return new SqlSessionTemplate(storySqlSessionFactory);
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.git.hui.boot.mybatis.mapper");
        mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
        mapperScannerConfigurer.setSqlSessionTemplateBeanName("sqlSessionTemplate");
        return mapperScannerConfigurer;
    }
}

测试输出省略

3. 小结

本文主要介绍Mybatis中Mapper接口的三种注册方式,其中常见的两种注解方式

  • @MapperScan: 指定Mapper接口的包路径
  • @Mapper: 放在mapper接口上
  • MapperScannerConfigurer: 编程方式注册

那么疑问来了,为啥要介绍这三种方式,我们实际的业务开发中,前面两个基本上就满足了;什么场景会用到第三种方式?

  • 如写通用的Mapper(类似Mybatis-Plus中的BaseMapper)
  • 如一个Mapper,多数据源的场景(如主从库,冷热库,db的操作mapper一致,但是底层的数据源不同)

本文到此结束,关于上面两个场景的实例case,后面有空再补上,我是一灰灰,有缘再见(欢迎关注长草的公众号一灰灰blog

III. 不能错过的源码和相关知识点

0. 项目

9 - 9.Mapper接口与Sql绑定几种姿势

通常我们在使用Mybatis进行开发时,会选择xml文件来写对应的sql,然后将Mapper接口与sql的xml文件建立绑定关系,然后在项目中调用mapper接口就可以执行对应的sql

那么如何将Mapper接口与sql进行绑定呢?本文将介绍四种常见的姿势

  • 默认策略
  • SpringBoot配置参数mybatis.mapper-locations
  • <mapper>指定
  • SqlSessionFactory指定

I. 环境准备

1. 数据库准备

使用mysql作为本文的实例数据库,新增一张表

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

pom依赖如下

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

db配置信息 application.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

II. 实例演示

环境搭建完毕,准备对应的实体类,Mapper接口

1. 实体类,Mapper接口

数据库实体类: MoneyPo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

一个基础的Mapper接口

@Mapper
public interface MoneyMapper {
	int savePo(@Param("po") MoneyPo po);
}

一个demo service

@Repository
public class MoneyRepository {
    private Random random = new Random();

    public void testMapper() {
        MoneyPo po = new MoneyPo();
        po.setName("mybatis user");
        po.setMoney((long) random.nextInt(12343));
        po.setIsDeleted(0);

        moneyMapper.savePo(po);
        System.out.println("add record: " + po);
}

2. sql文件

写sql的xml文件内容如下

<?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.git.hui.boot.mybatis.mapper.MoneyMapper">

    <insert id="savePo" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" useGeneratedKeys="true"
            keyProperty="po.id">
      INSERT INTO `money` (`name`, `money`, `is_deleted`)
      VALUES
	  (#{po.name}, #{po.money}, #{po.isDeleted});
    </insert>
</mapper>

3. Mapper与Sql绑定

以上为代码层面实现CURD的基础知识,基本上就是mybatis操作的那些套路,没有什么需要特殊注意的;接下来我们进入本文主题

如何告诉mybatis,将上面的MoenyMapper接口与xml文件关联起来

3.1 默认方式

采用默认的绑定方式,不需要我们做额外的操作,重点是需要遵循规则

  • xml的目录结构,与Mapper接口的包路径完全一致
  • xml文件名与Mapper接口名完全一致(注意大小写都要完全一致)

请注意上面的另个完全一致

使用默认的方式进行绑定时,一个示例如上图;特别需要注意的是文件名的大小写,xml文件的目录层级都需要完全一致

如果使用上面这种方式,在执行时,依然提示有问题,排查的思路就是查看 target目录下生成的class文件与xml文件是否在一起,如下图就是正常的case

再次说明

  • 基于上面的case,我们可以直接将xml文件,与mapper接口写在一起,不放在资源路径resources下面

3.2 SpringBoot配置

SpringBoot提供了一个简单的配置,来指定Mapper接口与sql的绑定,一行配置即可

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

使用这种方式就比较简单了,不要求xml文件与Mapper接口文件名一致;也没有指定路径层级一致

3.3 Mapper标签

mapper标签,需要放在mybatis的配置文件中,因此我们首先通过SpringBoot的配置参数指定文件路径

mybatis:
  configuration:
    config-location: classpath:mybatis-config.xml

在资源文件下,新建文件 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
        <mapper resource="sqlmapper/money-mapper.xml"/>
    </mappers>
</configuration>

通过上面的mapper标签来指定注册关系,也是可行的,详情可参考官方文档 !

https://mybatis.org/mybatis-3/configuration.html#mappers

3.4 SqlSessionFactory

在前面一篇介绍Mapper接口注册的博文中,就介绍了通过qlSessionFactory+ MapperScannerConfigurer来注册

这里也是可以通过SqlSessionFactory来指定xml文件的

 @Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setMapperLocations(
            // 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
    // 注册typehandler,供全局使用
    bean.setTypeHandlers(new Timestamp2LongHandler());
    bean.setPlugins(new SqlStatInterceptor());
    return bean.getObject();
}

4. 小结

本文主要介绍了四种Mapper接口与sql文件关系绑定的姿势,了解几种不同的姿势的特点,在实际的项目开发中,选择一个即可

  • 默认:在resource资源目录下,xml文件的目录层级与Mapper接口的包层级完全一致,且xml文件名与mapper接口文件名也完全一致
    • 如mapper接口: com.git.hui.boot.mybatis.mapper.MoneyMapper
    • 对应的xml文件: com/git/hui/boot/mybatis/mapper/MoneyMapper.xml
  • springboot配置参数:
    • application.yml配置文件中,指定 mybatis.mapper-locations=classpath:sqlmapper/*.xml
  • mybatis-config配置文件
    • 这种姿势常见于非SpringBoot项目集成mybatis,通常将mybatis的相关配置放在 mybatis-config.xml 文件中
    • 首先在配置文件中,指定加载参数 mybatis.config-location=classpath:mybatis-config.xml
    • 然后指定映射器 <mappers><mapper resource="sqlmapper/money-mapper.xml"/></mappers>
  • SqlSessionFactory指定
    • 直接在SqlSessionFactory中指定即可Mapper文件
// 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));

除了上面几种方式之外,mybatis还支持无xml的方式,完全依靠注解来实现sql的拼装,因此也就不存在映射关系绑定了,关于注解的case,可以参考博文 【DB系列】Mybatis+注解整合篇

III. 不能错过的源码和相关知识点

0. 项目

mybatis系列博文

10 - 10.自定义类型转换TypeHandler

在使用mybatis进行db操作的时候,我们经常会干的一件事情就是将db中字段映射到java bean,通常我们使用ResultMap来实现映射,通过这个标签可以指定两者的绑定关系,那么如果java bean中的字段类型与db中的不一样,应该怎么处理呢?

如db中为timestamp, 而java bean中定义的却是long

  • 通过BaseTypeHandler来实现自定义的类型转换

I. 环境准备

1. 数据库准备

使用mysql作为本文的实例数据库,新增一张表

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

pom依赖如下

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

db配置信息 application.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

II. 实例演示

1. entity定义

注意上面case中的create_atupdate_at的类型都是timestmap,我们定义的Entity如下

@Data
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Long updateAt;
}

2. Mapper测试接口

定义一个简单的查询接口,这里直接使用注解的方式(至于xml的写法差别也不大)

/**
 * 主键查询
 *
 * @param id id
 * @return {@link MoneyPo}
 */
@Select("select * from money where id = #{id}")
@Results(id = "moneyResultMap", value = {
        @Result(property = "id", column = "id", id = true, jdbcType = JdbcType.INTEGER),
        @Result(property = "name", column = "name", jdbcType = JdbcType.VARCHAR),
        @Result(property = "money", column = "money", jdbcType = JdbcType.INTEGER),
        @Result(property = "isDeleted", column = "is_deleted", jdbcType = JdbcType.TINYINT),
        @Result(property = "createAt", column = "create_at", jdbcType = JdbcType.TIMESTAMP),
//            @Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP)})
        @Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP, typeHandler = Timestamp2LongHandler.class)})
MoneyPo getById(@Param("id") int id);

// 关于 SelectProvider 的使用,后面再说,主要是动态sql的演示
@SelectProvider(type = MoneyService.class, method = "getByIdSql")
@ResultMap(value = "moneyResultMap")
MoneyPo getByIdForProvider(@Param("id") int id);

说明:

  • @Results: 这个注解与 ResultMap 标签效果一致,主要用于定义db的字段与java bean的映射关系
  • id = "moneyResultMap" 这个id定义,可以实现@Results的复用
  • @Result: 关注下updateAt的typeHandler,这里指定了自定义的TypeHandler,来实现JdbcType.TEMSTAMP与Java Bean中的long的转换

3. 类型转换

自定义类型转换,主要是继承BaseTypeHandler类,泛型的类型为Java Bean中的类型

/**
 * 自定义类型转换:将数据库中的日期类型,转换成long类型的时间戳
 *
 * 三种注册方式:
 * 1.直接在 result标签中,指定typeHandler,如@Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP, typeHandler = Timestamp2LongHandler.class)
 * 2.在SqlSessionFactory实例中,注册 在SqlSessionFactory实例中.setTypeHandlers(new Timestamp2LongHandler());
 * 3.xml配置,<typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
 *
 * @author yihui
 * @date 2021/7/7
 */
@MappedTypes(value = Long.class)
@MappedJdbcTypes(value = {JdbcType.DATE, JdbcType.TIME, JdbcType.TIMESTAMP})
public class Timestamp2LongHandler extends BaseTypeHandler<Long> {

    /**
     * 将java类型,转换为jdbc类型
     *
     * @param preparedStatement
     * @param i
     * @param aLong             毫秒时间戳
     * @param jdbcType          db字段类型
     * @throws SQLException
     */
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Long aLong, JdbcType jdbcType) throws SQLException {
        if (jdbcType == JdbcType.DATE) {
            preparedStatement.setDate(i, new Date(aLong));
        } else if (jdbcType == JdbcType.TIME) {
            preparedStatement.setTime(i, new Time(aLong));
        } else if (jdbcType == JdbcType.TIMESTAMP) {
            preparedStatement.setTimestamp(i, new Timestamp(aLong));
        }
    }

    @Override
    public Long getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return parse2time(resultSet.getObject(s));
    }

    @Override
    public Long getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return parse2time(resultSet.getObject(i));
    }

    @Override
    public Long getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return parse2time(callableStatement.getObject(i));
    }

    private Long parse2time(Object value) {
        if (value instanceof Date) {
            return ((Date) value).getTime();
        } else if (value instanceof Time) {
            return ((Time) value).getTime();
        } else if (value instanceof Timestamp) {
            return ((Timestamp) value).getTime();
        }
        return null;
    }
}
  • setNonNullParameter:将java类型,转换为jdbc类型
  • getNullableResult:将jdbc类型转java类型

4. TypeHandler注册

我们自己定义一个TypeHandler没啥问题,接下来就是需要它生效,一般来讲,有下面几种方式

4.1 result标签中指定

通过result标签中的typeHandler指定

使用xml的方式如

<result column="update_at" property="updateAt" jdbcType="TIMESTAMP" typeHandler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>

注解@Result的方式如

@Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP, typeHandler = Timestamp2LongHandler.class)

4.2 SqlSessionFactory全局配置

上面的使用姿势为精确指定,如果我们希望应用到所有的场景,则可以通过SqlSessionFactory来实现

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setMapperLocations(
            // 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
    // 注册typehandler,供全局使用
    bean.setTypeHandlers(new Timestamp2LongHandler());
    return bean.getObject();
}

4.3 全局xml配置

除上面case之外,还有一个就是借助mybatis-config.xml配置文件来注册,如

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 驼峰下划线格式支持 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeHandlers>
        <typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
    </typeHandlers>
</configuration>

注意,使用上面的配置文件,需要在SpringBoot中指定如下配置,否则将不会生效

mybatis:
  config-location: classpath:mybatis-config.xml

4.4 SpringBoot配置方式

springboot配置文件,可以通过指定type-handlers-package来注册TypeHandler

mybatis:
  type-handlers-package: com.git.hui.boot.mybatis.handler

5. 小结

本文主要介绍db中的类型与java bean中类型的映射适配策略,主要是通过继承BaseTypeHandler来实现自定义的类型转化

要使用自定义的TypeHandler,有全局生效与精确指定两种方式

  • @Result/<result>标签中,通过typeHandler指定
  • SqlSessionFactory 全局设置typeHandler
  • mybatis-config.xml 配置文件设置typeHandlers

此外本文的配置中,还支持了驼峰与下划线的互转配置,这个也属于常见的配置,通过在mybatis-config中如下配置即可

<setting name="mapUnderscoreToCamelCase" value="true"/>

接下来问题来了,驼峰可以和下划线互转,那么有办法实现自定义的name映射么,如果有知道的小伙伴,请不吝指教

III. 不能错过的源码和相关知识点

0. 项目

mybatis系列博文

11 - 11.插件机制Interceptor

在Mybatis中,插件机制提供了非常强大的扩展能力,在sql最终执行之前,提供了四个拦截点,支持不同场景的功能扩展

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

本文将主要介绍一下自定义Interceptor的使用姿势,并给出一个通过自定义插件来输出执行sql,与耗时的case

I. 环境准备

1. 数据库准备

使用mysql作为本文的实例数据库,新增一张表

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

pom依赖如下

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

db配置信息 application.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

II. 实例演示

关于myabtis的配套Entity/Mapper相关内容,推荐查看之前的系列博文,这里就不贴出来了,将主要集中在Interceptor的实现上

1. 自定义interceptor

实现一个自定义的插件还是比较简单的,试下org.apache.ibatis.plugin.Interceptor接口即可

比如定义一个拦截器,实现sql输出,执行耗时输出

@Slf4j
@Component
@Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
public class ExecuteStatInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // MetaObject 是 Mybatis 提供的一个用于访问对象属性的对象
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        BoundSql sql = statement.getBoundSql(invocation.getArgs()[1]);

        long start = System.currentTimeMillis();
        List<ParameterMapping> list = sql.getParameterMappings();
        OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
        List<Object> params = new ArrayList<>(list.size());
        for (ParameterMapping mapping : list) {
            params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
        }
        try {
            return invocation.proceed();
        } finally {
            System.out.println("------------> sql: " + sql.getSql() + "\n------------> args: " + params + "------------> cost: " + (System.currentTimeMillis() - start));
        }
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

注意上面的实现,核心逻辑在intercept方法,内部实现sql获取,参数解析,耗时统计

1.1 sql参数解析说明

上面case中,对于参数解析,mybatis是借助Ognl来实现参数替换的,因此上面直接使用ognl表达式来获取sql参数,当然这种实现方式比较粗暴

// 下面这一段逻辑,主要是OGNL的使用姿势
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
    params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}

除了上面这种姿势之外,我们知道最终mybatis也是会实现sql参数解析的,如果有分析过源码的小伙伴,对下面这种姿势应该比较熟悉了

源码参考自: org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters

BoundSql sql = statementHandler.getBoundSql();
DefaultParameterHandler handler = (DefaultParameterHandler) statementHandler.getParameterHandler();
Field field = handler.getClass().getDeclaredField("configuration");
field.setAccessible(true);
Configuration configuration = (Configuration) ReflectionUtils.getField(field, handler);
// 这种姿势,与mybatis源码中参数解析姿势一直
// 
MetaObject mo = configuration.newMetaObject(sql.getParameterObject());
List<Object> args = new ArrayList<>();
for (ParameterMapping key : sql.getParameterMappings()) {
    args.add(mo.getValue(key.getProperty()));
}

但是使用上面这种姿势,需要注意并不是所有的切点都可以生效;这个涉及到mybatis提供的四个切点的特性,这里也就不详细进行展开,在后面的源码篇,这些都是绕不过去的点

1.2 Intercepts注解

接下来重点关注一下类上的@Intercepts注解,它表明这个类是一个mybatis的插件类,通过@Signature来指定切点

其中的type, method, args用来精确命中切点的具体方法

如根据上面的实例case进行说明

@Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})

首先从切点为Executor,然后两个方法的执行会被拦截;这两个方法的方法名分别是query, update,参数类型也一并定义了,通过这些信息,可以精确匹配Executor接口上定义的类,如下

// org.apache.ibatis.executor.Executor

// 对应第一个@Signature
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

// 对应第二个@Signature
int update(MappedStatement var1, Object var2) throws SQLException;

1.3 切点说明

mybatis提供了四个切点,那么他们之间有什么区别,什么样的场景选择什么样的切点呢?

一般来讲,拦截ParameterHandler是最常见的,虽然上面的实例是拦截Executor,切点的选择,主要与它的功能强相关,想要更好的理解它,需要从mybatis的工作原理出发,这里将只做最基本的介绍,待后续源码进行详细分析

  • Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
  • StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  • ParameterHandler:是用来处理SQL参数的。
  • ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。

借用网上的一张mybatis执行过程来辅助说明

原文 https://blog.csdn.net/weixin_39494923/article/details/91534658

2. 插件注册

上面只是自定义插件,接下来就是需要让这个插件生效,也有下面几种不同的姿势

2.1 Spring Bean

将插件定义为一个普通的Spring Bean对象,则可以生效

2.2 SqlSessionFactory

直接通过SqlSessionFactory来注册插件也是一个非常通用的做法,正如之前注册TypeHandler一样,如下

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setMapperLocations(
            // 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
    // 注册typehandler,供全局使用
    bean.setTypeHandlers(new Timestamp2LongHandler());
    bean.setPlugins(new SqlStatInterceptor());
    return bean.getObject();
}

2.3 xml配置

习惯用mybatis的xml配置的小伙伴,可能更喜欢使用下面这种方式,在mybatis-config.xml全局xml配置文件中进行定义

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 驼峰下划线格式支持 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="com.git.hui.boot.mybatis.entity"/>
    </typeAliases>
    
    <!-- type handler 定义 -->
    <typeHandlers>
        <typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
    </typeHandlers>

    <!-- 插件定义 -->
    <plugins>
        <plugin interceptor="com.git.hui.boot.mybatis.interceptor.SqlStatInterceptor"/>
        <plugin interceptor="com.git.hui.boot.mybatis.interceptor.ExecuteStatInterceptor"/>
    </plugins>
</configuration>

3. 小结

本文主要介绍mybatis的插件使用姿势,一个简单的实例演示了如果通过插件,来输出执行sql,以及耗时

自定义插件实现,重点两步

  • 实现接口org.apache.ibatis.plugin.Interceptor
  • @Intercepts 注解修饰插件类,@Signature定义切点

插件注册三种姿势:

  • 注册为Spring Bean
  • SqlSessionFactory设置插件
  • myabtis.xml文件配置

III. 不能错过的源码和相关知识点

0. 项目

mybatis系列博文

12 - 12.CURD基本使用姿势

mybatis作为数据的ORM框架,在国内的应用市场还是非常可观的,当初刚开始工作时使用spring + mybatis进行开发,后来也使用过hibernate, jdbctemplate, jooq,mybatisplus等其他的一些框架,

就个人使用感触来讲jooq的使用姿势和写sql差不多,基本上可以会写sql的无需额外的培训,立马可以上手;

hibernate最大的特点就是借助方法名来映射sql语句,非常有特点,但是当查询条件复杂一些的话,对小白而言就没有那么友好了;

而jdbctemplate,这个在小项目,轻量的db操作中,用起来还是很爽的,非常灵活,但是也有一些点需要特别注意,比如queryForObject,查不到数据时抛异常而不是返回null;

至于mybatis以及衍生的mybatis-plus,也就是接下来的主角了,它的特点如何,为什么受到国内大量开发者的追捧,将它作为db操作的第一ORM框架,让我们看完之后再说

I. 基础环境搭建

接下来的Mybatis的项目演示,主要是在SpringBoot的环境下运行,底层的数据库采用MySql,对应的版本信息如下

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. SpringBoot项目配置

关于SpringBoot的项目创建过程省略,下面是核心的pom依赖

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库准备

在本地数据库中,新增了一个表如下

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

接下来本文涉及到的CURD都是针对这张表来说的

II. MyBatis CURD

接下来我们将从0到1,实现基于mybatis进行mysql操作的全流程

1. 基本对象

经常使用Mybatis的小伙伴可能知道,操作一个db,通常会伴随几个不可或缺的东西

  • 数据库实体类:可以理解为数据库表锁映射到的Java Bean对象
  • Mapper接口:interface类,其中定义db的操作方法
  • xml文件:与上面接口对应,xml文件中写实际的sql

mybatis推荐的玩法是借助xml来写sql,但是官方也提供了注解的方式,因此xml文件并不是必须的;后面会介绍注解的操作方式;本文将主要是传统的xml配套使用姿势

针对上面这张表,第一步定义实体类MoneyPo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}
  • 上面的三个注解属于lombok的知识点,有不清楚的小伙伴可以搜索一下

接下来是Mapper接口, MoneyMapper如下

// 注意这个@Mapper注解,用于表明这个接口属于Mybatis的Mapper对象
@Mapper
public interface MoneyMapper {
}

然后是Mapper接口对应的xml文件MoneyMapper.xml

注意xml文件放在资源文件resources下面,且xml文件的目录结构,与上面的Mapper接口的包路径保持完全一致 (why? 参看博文 【DB系列】SpringBoot系列Mybatis之Mapper接口与Sql绑定几种姿势)

<?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.git.hui.boot.mybatis.mapper.MoneyMapper">
</mapper>

2. 数据插入

前面的三步骤,将我们需要的实体类,接口对象,xml文件都初始化完毕,接下来就是进入我们的CURD环节,实现数据库的增删改查,这里主要使用insert标签

比如我们现在希望插入一条数据,首先需要做的就是在Mapper接口中定义一个方法

int savePo(@Param("po") MoneyPo po);

接着就是在xml文件中对应的sql

<insert id="savePo" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" useGeneratedKeys="true"
        keyProperty="po.id">
  INSERT INTO `money` (`name`, `money`, `is_deleted`)
  VALUES
(#{po.name}, #{po.money}, #{po.isDeleted});
</insert>

2.1 解析说明

注意上面的xml文件

  • parameterType: 用于指定传参类型
  • useGenerateKeys + keyProperty: 表明需要将插入db的主键id,会写到这个实体类的id字段上
  • sql语句传参:形如#{},大括号里面填写变量名,上面用的是po.name,po为接口定义中的参数名,这个就表示使用po对象的name成员,作为db的name字段

接下来就是重要知识点:

  • 传参除了使用 #{}之外,还可以使用 ${},区别在于前面为参数参数占位,后面为字符串替换,因此存在sql注入的风险

举例说明

select * from money where id=${id}
select * from money where id=#{id}

针对上面这两个sql,当id = 1 or 1=1,对应的两个sql变成

-- 第一个sql会返回所有的数据
select * from money where id = 1 or 1 =1 
-- 下面这个会抛sql异常
select * from money where id = '1 or 1=1'

2.2 批量插入

除了上面的单挑插入,批量插入也是ok的,和前面的使用姿势差不多

int batchSave(@Param("list") List<MoneyPo> list);

对应的sql如下

<insert id="batchSave" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo"  useGeneratedKeys="true" keyProperty="id">
    insert ignore into `money` (`name`, `money`, `is_deleted`)
    values
    <foreach collection="list" item="item" index="index" separator=",">
        (#{item.name}, #{item.money}, #{item.isDeleted})
    </foreach>
</insert>

对于foreach标签的说明,会放在后面的博文中专门进行介绍,这里简单理解为遍历即可

3. 数据查询

查询可以说是我们日常开发中最常见的情况了,这里先给出简单的查询demo,至于更复杂的查询条件(如联表,子查询,条件查询等)在后面的博文中进行介绍

如根据主键进行查询,主要借助select标签来实现

MoneyPo findById(int id);

对应的sql

  <resultMap id="BaseResultMap" type="com.git.hui.boot.mybatis.entity.MoneyPo">
    <id column="id" property="id" jdbcType="INTEGER"/>
    <result column="name" property="name" jdbcType="VARCHAR"/>
    <result column="money" property="money" jdbcType="INTEGER"/>
    <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
    <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
    <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="money_po">
  id, name, money, is_deleted, create_at, update_at
</sql>

<select id="findById" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money where id=#{id}
</select>

重点关注下上面的实现,select语句内容比较简单,但是有几个需要注意的点

  • sql标签:内部定义需要查询的db字段,最大的特点是供后面的查询语句,通过include来引入,从而实现代码片段的复用
  • resullMap标签:从db字段与MoneyPo实体类对比,我们可以知道部分字段名不是完全一样,如db中使用下划线,java中使用驼峰,那么db字段与java 成员变量如何映射呢?这里使用result标签来指定两者的映射关系,以及类型

(上面这个相信会始终伴随各位小伙伴的开发生涯)

4. 数据更新

更新主要借助update标签,相比较上面的两个,它的知识点就比较少了

int addMoney(@Param("id") int id, @Param("money") int money);

对应的sql如下

<update id="addMoney" parameterType="java.util.Map">
    update money set money=money+#{money} where id=#{id}
</update>

说明

  • 上面标签中的parameterType,在这里实际上是可以省略的
  • @Param注解:主要用于指定参数名,在xml中可以使用内部定义的名字来作为参数变量;如果不加上这个注解,在xml中,参数变量则使用param0, param1来替代

5. 数据删除

删除使用delete标签

int delPo(@Param("id") int id);

对应的sql如下

<delete id="delPo" parameterType="java.lang.Integer">
    delete from money where id = #{id,jdbcType=INTEGER}
</delete>

6. 使用演示

上面的mapper接口中定义了完整的CURD,接下来就是使用这个Mapper接口来实现交互了,在Spring中,使用姿势就非常简单了,直接当一个Spring Bean对象注入到service类中即可

@Repository
public class MoneyRepository {
    @Autowired
    private MoneyMapper moneyMapper;

    public void testBasic() {
        MoneyPo po = new MoneyPo();
        po.setName("mybatis user");
        po.setMoney((long) random.nextInt(12343));
        po.setIsDeleted(0);

        moneyMapper.savePo(po);
        System.out.println(po);
        MoneyPo out = moneyMapper.findById(po.getId());
        System.out.println("query:" + out);
        moneyMapper.addMoney(po.getId(), 100);
        System.out.println("after update:" + moneyMapper.findById(po.getId()));
        moneyMapper.delPo(po.getId());
        System.out.println("after del:" + moneyMapper.findById(po.getId()));
    }
}

执行输出结果如下

MoneyPo(id=552, name=mybatis user, money=7719, isDeleted=0, createAt=null, updateAt=null)
query:MoneyPo(id=552, name=mybatis user, money=7719, isDeleted=0, createAt=2021-08-01 11:47:23.0, updateAt=2021-08-01 11:47:23.0)
after update:MoneyPo(id=552, name=mybatis user, money=7819, isDeleted=0, createAt=2021-08-01 11:47:23.0, updateAt=2021-08-01 11:47:23.0)
after del:null

7. 小结

相信各位小伙伴看到这里,搭建一个mybatis实现数据库的CURD的项目应该是问题不大了,本文的主要知识点如下

  • mybatis项目的三套件:实体类 + mapper接口 + xml文件
  • 数据库的增删改查

其中有一些知识点比较重要,本文只是抛出来了,有兴趣的小伙伴可以持续关注后续更新

下面这些知识点,后面会进行更详细的说明

  • 如何获取插入数据的主键id
  • 批量场景下的foreach标签使用
  • 数据库表结构与java 实体类的映射 resultMap标签
  • Mapper接口与xml文件的关联方式
  • Mapper接口如何被扫描到,并被Spring bean对象
  • Mapper接口与xml的传参方式 @Param注解
  • sql参数替换的两种写法 ${}, #{}
  • 传参类型,返回值类型定义
  • 代码复用片段sql标签

III. 不能错过的源码和相关知识点

0. 项目

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

13 - 13.Mybatis传参作为字段/表名时的注意事项

今天遇到一个非常有意思的事情,一个小伙伴反馈,同样的sql,为啥直接通过mysql终端执行的结果,和mybatis的执行结果不一致,感觉有点鬼畜;然后看了一下,发现这是个比较典型的问题,#{}${}的使用区别

接下来我们看一下这个问题,顺带也重新学习一下它们两的区别

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库表

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. 场景复现

一个简单的demo来演示下使用姿势,根据传参,来指定排序的字段;

List<MoneyPo> orderBy(@Param("order") String order);

对应的xml如下

<select id="orderBy" resultMap="BaseResultMap">
    select * from `money` order by #{order} asc
</select>

上面这个执行之后可能与我们预期的不一致,如下

1. 问题修复

上面的演示中,本来是希望根据传参进行排序,最后的执行结果会发现都是按照id进行排序

要解决上面这个问题,也很简单,将#改成$

<select id="orderBy" resultMap="BaseResultMap">
    select * from `money` order by ${order} asc
</select>

再次测试如下,和我们的预期一致了

2. 原因分析

上面这个问题的关键原因在于 $#的本质区别,有过一点了解的小伙伴会知道$最终的效果是替换,而#则是占位

比如上面的两个,转成sql,对应如下

  • #{}: select * from money order by 'money' asc
    • 注意money作为字符串传入的
  • ${}: select * from money order by money asc
    • 注意money作为列名

上面的第一个sql,非常有意思,执行居然不会抛错,可以正常执行(注意,这个与数据库版本有关,并不是所有的版本都可以正常执行)

3. #{}与${}对比

#{} ${}
参数占位,相当于 ? 直接替换到sql的一部分
动态解析 -> 预编译 -> 执行 动态解析 -> 编译 -> 执行
变量替换是在DBMS 中 变量替换是在 DBMS 外
变量替换后,#{} 对应的变量自动加上单引号 '' 变量替换后,${} 对应的变量不会加上单引号 ''
防sql注入 不能防sql注入

注意事项:

select * from money where name = #{name}
select * from money where name = ${name}

如上面两条sql,在具体传参的时候,就会有一个显著的去呗

  • #{name}: 传参 一灰灰,对应sql如下
    • select * from money where name = '一灰灰'
  • ${name}: 传参 一灰灰,对应sql如下
    • select * from money where name = 一灰灰
    • 注意上面的sql中,name的传参没有引号,直接就是bad sql
    • 所以传参应该是 '一灰灰',需要手动的加上单引号

使用姿势:

  • 能用 #{} 的地方就用 #{},不用或少用 ${}
  • 表名作参数时,必须用 ${}
  • order by 时,必须用 ${}
  • 使用 ${} 时,要注意何时加或不加单引号,即 ${} 和 ‘${}’

III. 不能错过的源码和相关知识点

0. 项目

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

14 - 14.CURD基本使用姿势-注解篇

上面一篇博文介绍了mybatis + xml配置文件的使用方式,在上文中介绍到,xml文件是可以省略掉的,直接使用java注解来实现CURD,接下来我们看一下,如何使用注解来实现等同的效果

I. Mybatis注解开发

关于项目环境的搭建与前文一致,如有疑问,查看博文:

1. 基础配置

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

对应的数据库实体类 MoneyPO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

Mapper接口类

@Mapper
public interface MoneyMapper {
}

注意与前文的区别,这里我们没有xml文件

2. 数据插入

新增数据,使用注解 @Insert,直接放在接口中定义的方法上即可

/**
 * 保存数据,并保存主键id
 *
 * @param po
 * @return int
 */
@Options(useGeneratedKeys = true, keyProperty = "po.id", keyColumn = "id")
@Insert("insert into money (name, money, is_deleted) values (#{po.name}, #{po.money}, #{po.isDeleted})")
int save(@Param("po") MoneyPo po);

@Insert注解中的内容与前面xml中标签内容一致,没有什么区别,重点关注一下 @Options注解,用来指定一些配置信息,比如上面的case,就用来配置将插入的id,保存到参数MoneyPo的id字段上

3. 查询数据

查询则使用@Select注解


    /**
     * 主键查询
     *
     * @param id id
     * @return {@link MoneyPo}
     */
    @Select("select * from money where id = #{id}")
    @Results(id = "moneyResultMap", value = {
            @Result(property = "id", column = "id", id = true, jdbcType = JdbcType.INTEGER),
            @Result(property = "name", column = "name", jdbcType = JdbcType.VARCHAR),
            @Result(property = "money", column = "money", jdbcType = JdbcType.INTEGER),
            @Result(property = "isDeleted", column = "is_deleted", jdbcType = JdbcType.TINYINT),
            @Result(property = "createAt", column = "create_at", jdbcType = JdbcType.TIMESTAMP),
            @Result(property = "updateAt", column = "update_at", jdbcType = JdbcType.TIMESTAMP)})
    MoneyPo getById(@Param("id") int id);

select注解没有什么特别的,重点关注一下@Results注解

这个注解的作用,与xml中的<resultMap>标签功能一直,通过内部的@Result来指定数据库表结构与java实体类的映射关系,最外层的id主要用于复用

@Select("select * from money where `name` = #{name}")
@ResultMap(value = "moneyResultMap")
MoneyPo getByName(@Param("name") String name);

4. 数据更新

直接给出对应的case


/**
 * 更新
 *
 * @param id    id
 * @param money 钱
 * @return int
 */
@Update("update money set `money`=#{money} where id = #{id}")
int addMoney(@Param("id") int id, @Param("money") long money);

5. 数据删除

/**
 * 删除数据
 *
 * @param id id
 * @return int
 */
@Delete("delete from money where id = #{id}")
int delete(@Param("id") int id);

6. 小结

从注解的使用来看,与xml文件的方式基本上没有什么区别,当然从上面的示例来说,貌似使用注解的方式更加简洁,毕竟sql语句直接放在方法上,不需要像之前那样,两个文件来回切换

但是,请注意,注解的使用姿势并没有特别广泛使用也是有原因的,上面只是一些简单接触的case,当sql语句比较复杂的时,注解的方式写起来就没有那么爽快了

如in查询

/**
 * foreach 查询
 *
 * @param ids
 * @return
 */
@Select("<script> select * from money where id in  " +
        "<foreach collection='ids' index='index' item='id' open='(' separator=',' close=')'>" +
        "#{id}" +
        "</foreach></script>")
List<MoneyPo> getByIds(@Param("ids") List<Integer> ids);

注解上的内容太多,特别是上面的字符串拼接方式,会极大的影响阅读体验;当然在jdk14的新特性中,提供了文本块的支持,类似pyton中使用三个双引号来标注一个大的文本块,然而现实的是,实际上又有多少项目升级到了jdk14呢?

III. 不能错过的源码和相关知识点

0. 项目

系列博文

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

15 - 15.参数传递的几种姿势

在mybatis的日常开发中,mapper接口中定义的参数如何与xml中的参数进行映射呢?除了我们常用的@Param注解之外,其他的方式是怎样的呢?

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库表

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. 参数传递

接下来我们看一下Mapper接口中的参数与xml文件中的参数映射的几种姿势;关于mybatis项目的搭建,这里就略过,重点信息有下面几个

数据库实体对象

@Data
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;

    private Integer cnt;
}

mapper接口

@Mapper
public interface MoneyMapper {
}

xml文件,在资源文件夹下,目录层级与mapper接口的包路径完全一致(遵循默认的Mapper接口与xml文件绑定关系,详情查看SpringBoot系列Mybatis之Mapper接口与Sql绑定几种姿势

<?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.git.hui.boot.mybatis.mapper.MoneyMapper">

    <resultMap id="BaseResultMap" type="com.git.hui.boot.mybatis.entity.MoneyPo">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="money" property="money" jdbcType="INTEGER"/>
        <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
        <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
        <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
    </resultMap>
    <sql id="money_po">
      id, name, money, is_deleted, create_at, update_at
    </sql>
</mapper>

1. @Param注解

在接口的参数上添加@Param注解,在内部指定传递给xml的参数名

一个简单的case如下

int addMoney(@Param("id") int id, @Param("money") int money);

重点关注上面的参数

  • 通过@Param来指定传递给xml时的参数名

对应的xml文件中的sql如下,使用#{}来实现参数绑定

<update id="addMoney" parameterType="java.util.Map">
    update money set money=money+#{money} where id=#{id}
</update>

2. 单参数

接下来我们看一下不使用@Param注解时,默认场景下,xml中应该如何指定参数;因为单参数与多参数的实际结果不一致,这里分开进行说明

单参数场景下,xml中的参数名,可以用任意值来表明

mapper接口定义如下

/**
 * 单个参数时,默认可以直接通过参数名来表示,实际上#{}中用任意一个值都可以,没有任何限制,都表示的是这个唯一的参数
 * @param id
 * @return
 */
MoneyPo findById(int id);

/**
 * 演示xml中的 #{} 为一个匹配补上的字符串,也可以正确的实现参数替换
 * @param id
 * @return
 */
MoneyPo findByIdV2(int id);

对应的xml文件内容如下

<select id="findById" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money where id=#{id}
</select>

<select id="findByIdV2" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money where id=#{dd}
</select>

重点看一下上面的findByIdV2,上面的sql中传参使用的是 #{dd},和mapper接口中的参数名并不相同,但是最终的结果却没有什么区别

3. 多参数

当参数个数超过1个的时候,#{}中的参数,有两种方式

  • param1…N: 其中n代表的接口中的第几个参数
  • arg0…N
/**
 * 不指定参数名时,mybatis自动封装一个  param1 ... paramN的Map,其中n表示第n个参数
 * 也可以使用 arg0...n 来指代具体的参数
 *
 * @param name
 * @param money
 * @return
 */
List<MoneyPo> findByNameAndMoney(String name, Integer money);

对应的xml如下

<select id="findByNameAndMoney" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    -- from money where name=#{param1} and money=#{param2}
    from money where name=#{arg0} and money=#{arg1}
</select>

注意上面的xml中,两种传参都是可以的,当然不建议使用这种默认的方式来传参,因为非常不直观,对于后续的维护很不优雅

3. Map传参

如果参数类型并不是简单类型,当时Map类型时,在xml文件中的参数,可以直接使用map中对应的key来指代

/**
 * 参数类型为map时,直接使用key即可
 * @param map
 * @return
 */
List<MoneyPo> findByMap(Map<String, Object> map);

对应的xml如下

<select id="findByMap" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money
    <trim prefix="WHERE" prefixOverrides="AND | OR">
        <if test="id != null">
            id = #{id}
        </if>
        <if test="name != null">
            AND name=#{name}
        </if>
        <if test="money != null">
            AND money=#{money}
        </if>
    </trim>
</select>

4. POJO对象

另外一种常见的case是传参为简单的实体对象,这个时候xml中的参数也可以直接使用对象的fieldName来指代,和map的使用方式差不多

/**
 * 参数类型为java对象,同样直接使用field name即可
 * @param po
 * @return
 */
List<MoneyPo> findByPo(MoneyPo po);

对应的xml文件如下

<select id="findByPo" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" resultMap="BaseResultMap">
    select
    <include refid="money_po"/>
    from money
    <trim prefix="WHERE" prefixOverrides="AND | OR">
        <if test="id != null">
            id = #{id}
        </if>
        <if test="name != null">
            AND name=#{name}
        </if>
        <if test="money != null">
            AND money=#{money}
        </if>
    </trim>
</select>

5. 简单参数 + Map参数

当参数有多个,其中部分为简单类型,部分为Map,这样的场景下参数如何处理呢?

  • 简单类型遵循上面的规则
  • map参数的传参,使用前缀 + “.” + key的方式

一个实例如下

List<MoneyPo> findByIdOrCondition(@Param("id") int id, @Param("map") Map<String, Object> map);

List<MoneyPo> findByIdOrConditionV2(int id, Map<String, Object> map);

对应的xml如下

<select id="findByIdOrCondition" resultMap="BaseResultMap">
    select <include refid="money_po"/> from money where id = #{id} or  `name`=#{map.name}
</select>

<select id="findByIdOrConditionV2" resultMap="BaseResultMap">
    select <include refid="money_po"/> from money where id = #{param1} or `name`=#{param2.name}
</select>

6.小结

本文主要介绍mybatis中传参的几种姿势:

  • 默认场景下,单参数时,xml文件中可以用任意名称代替传参
  • 默认场景下,多参数时,第一个参数可用 param1 或 arg0来表示,第二个参数为 param2 或 arg1。。。
  • 单参数,且为map时,可以直接使用map的key作为传参
  • 单参数,pojo对象时,使用对象的fieldName来表示传参
  • @Param注解中定义的值,表示这个参数与xml中的占位映射关联
  • 多参数场景下,简单对象 + map/pojo时,对于map/pojo中的参数占位,可以通过 paramN.xxx 的方式来完成

III. 不能错过的源码和相关知识点

0. 项目

系列博文

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

16 - 16.转义符的使用姿势

在mybatis的xml文件中直接写sql比较方便简洁,但是需要注意的是,在xml文件中,经常会遇到一些需要转义的场景,比如查询 id < xxx的数据,这个小于号就不能直接写在sql中,接下来我们将看一下,mybatis中的有哪些转义符,可以怎么处理转义问题

I.转义

1. 转义符

在mybatis的xml文件中,我们最常见的转义符为小于号,如查询id小于100的数据

<select id="xxx">
  select * from `money` where id &lt; #{id}
</select>

注意上面的sql,小于号实际使用的是 &lt;,不能直接使用 <,比如直接使用小于号,在idea中会有如下的错误提示

日常开发中除了上面的小于号之外,另外一个常见的则是 & 与操作符,如果sql中有位操作的场景,同样需要转义

<select id="xxx">
  -- select * from `money` where id & 1 = 1 的sql,需要如下转义
  select * from `money` where id &amp; 1 = 1
</select>

在mybatis中常见的几个转义字符表映射关系如下表 (mybatis的转义实际上完全遵循的是xml转义规则,主要有下面几个)

符号 转义 说明
< < 小于
> > 大于
& &
' ' 单引号
" " 双引号

2. 写法

通过转义的方式虽然简单,但是有一个问题就是不够直观,在阅读sql时,还需要在脑海里反转义一下,这样就不太友好了,好在xml提供了CDATA的语法,被包裹在它内部的语句,不会被xml解析器进行解析

如通过下面的写法来写与操作

<select id="queryBitCondition" resultType="long">
    select id from money where  <![CDATA[ `money` & #{bit} = #{bit} ]]>
</select>

使用这种方式时,需要注意:

  • 不支持嵌套的写法
  • 结尾符 ]]> 注意与起始符配套使用

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

17 - 17.传参类型如何确定

最近有小伙伴在讨论#{}${}的区别时,有提到#{}是用字符串进行替换,就我个人的理解,它的主要作用是占位,最终替换的结果并不一定是字符串方式,比如我们传参类型是整形时,最终拼接的sql,传参讲道理也应该是整形,而不是字符串的方式

接下来我们来看一下,mapper接口中不同的参数类型,最终拼接sql中是如何进行替换的

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库表

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

测试数据,主要是name字段,值为一个数字的字符串

INSERT INTO `money` (`id`, `name`, `money`, `is_deleted`, `create_at`, `update_at`)
VALUES
	(120, '120', 200, 0, '2021-05-24 20:04:39', '2021-09-27 19:21:40');

II. 传参类型确定

本文忽略掉mybatis中的po、mapper接口、xml文件的详情,有兴趣的小伙伴可以直接查看最下面的源码(或者查看之前的博文也可以)

1. 参数类型为整形

针对上面的case,定义一个根据name查询数据的接口,但是这个name参数类型为整数

mapper接口:

/**
 * int类型,最终的sql中参数替换的也是int
 * @param name
 * @return
 */
List<MoneyPo> queryByName(@Param("name") Integer name);

对应的xml文件如下

<select id="queryByName" resultMap="BaseResultMap">
    select * from money where `name` = #{name}
</select>

上面这个写法非常常见了,我们现在的问题就是,传参为整数,那么最终的sql是 name = 120 还是 name = '120'呢?

那么怎么确定最终生成的sql是啥样的呢?这里介绍一个直接输出mysql执行sql日志的方式

在mysql服务器上执行下面两个命令,开启sql执行日志

set global general_log = "ON";
show variables like 'general_log%';

当我们访问上面的接口之后,会发现最终发送给mysql的sql语句中,参数替换之后依然是整数

select * from money where `name` = 120

2. 指定jdbcType

在使用#{}, ${}时,有时也会看到除了参数之外,还会指定jdbcType,那么我们在xml中指定这个对最终的sql生成会有影响么?

<select id="queryByNameV2" resultMap="BaseResultMap">
    select * from money where `name` = #{name, jdbcType=VARCHAR} and 0=0
</select>

生成的sql如下

select * from money where `name` = 120 and 0=0

从实际的sql来看,这个jdbcType并没有影响最终的sql参数拼接,那它主要是干嘛用呢?(它主要适用于传入null时,类型转换可能出现的异常)

3. 传参类型为String

当我们传参类型为string时,最终的sql讲道理应该会带上引号

/**
 * 如果传入的参数类型为string,会自动带上''
 * @param name
 * @return
 */
List<MoneyPo> queryByNameV3(@Param("name") String name);

对应的xml

<select id="queryByNameV3" resultMap="BaseResultMap">
    select * from money where `name` = #{name, jdbcType=VARCHAR} and 1=1
</select>

上面这个最终生成的sql如下

select * from money where `name` = '120' and 1=1

4. TypeHandler实现参数替换强制添加引号

看完上面几节,基本上可以有一个得出一个简单的推论(当然对不对则需要从源码上分析了)

  • sql参数替换,最终并不是简单使用字符串来替换,实际上是由参数java的参数类型决定,若java参数类型为字符串,拼接的sql为字符串格式;传参为整型,拼接的sql也是整数

那么问题来了,为什么要了解这个?

  • 关键点在于索引失效的问题

比如本文实例中的name上添加了索引,当我们的sql是 select * from money where name = 120 会走不了索引,如果想走索引,要求传入的参数必须是字符串,不能出现隐式的类型转换

基于此,我们就有一个应用场景了,为了避免由于传参类型问题,导致走不了索引,我们希望name的传参,不管实际传入参数类型是什么,最终拼接的sql,都是字符串的格式;

我们借助自定义的TypeHandler来实现这个场景

@MappedTypes(value = {Long.class, Integer.class})
@MappedJdbcTypes(value = {JdbcType.CHAR, JdbcType.VARCHAR, JdbcType.LONGVARCHAR})
public class StrTypeHandler extends BaseTypeHandler<Object> {

    /**
     * java 类型转 jdbc类型
     *
     * @param ps
     * @param i
     * @param parameter
     * @param jdbcType
     * @throws SQLException
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, String.valueOf(parameter));
    }

    /**
     * jdbc类型转java类型
     *
     * @param rs
     * @param columnName
     * @return
     * @throws SQLException
     */
    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getString(columnName);
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getString(columnIndex);
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
}

然后在xml中,指定TypeHandler

/**
 * 通过自定义的 TypeHandler 来实现 java <-> jdbc 类型的互转,从而实现即时传入的是int/long,也会转成String
 * @param name
 * @return
 */
List<MoneyPo> queryByNameV4(@Param("name") Integer name);
<select id="queryByNameV4" resultMap="BaseResultMap">
    select * from money where `name` = #{name, jdbcType=VARCHAR, typeHandler=com.git.hui.boot.mybatis.handler.StrTypeHandler} and 2=2
</select>

上面这种写法输出的sql就会携带上单引号,这样就可以从源头上解决传参类型不对,导致最终走不了索引的问题

select * from money where `name` = '120' and 2=2

5. 小结

本文通过一个简单的实例,来测试Mapper接口中,不同的参数类型,对最终的sql生成的影响

  • 参数类型为整数时,最终的sql的参数替换也是整数(#{}并不是简单的字符串替换哦)
  • 参数类型为字符串时,最终的sql参数替换,会自动携带''${}注意它不会自动带上单引号,需要自己手动添加)

当我们希望不管传参什么类型,最终生成的sql,都是字符串替换时,可以借助自定义的TypeHandler来实现,这样可以从源头上避免因为隐式类型转换导致走不了索引问题

最后疑问来了,上面的结论靠谱么?mybatis中最终的sql是在什么地方拼接的?这个sql拼接的流程是怎样的呢?

关于sql的拼接全流程,后续博文即将上线,我是一灰灰,走过路过的各位大佬帮忙点个赞、价格收藏、给个评价呗

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

18 - 18.ParameterMap、ParameterType传参类型指定使用姿势

在使用Mybatis开发时,借助xml来写具体的sql,再写传参类型或者返回结果类型时,通常会与ParameterType, ParameterMap, ResultMap, ResultType这四个打交到,那么这个Type与Map到底怎么区别,什么时候要指定类型,什么时候又可以不指定呢?

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库表

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. Parameter/Result介绍

1. ParameterMap & ParameterType

这两个主要用于指定传参类型,前面有一篇介绍过传参的姿势有兴趣的小伙伴可以查看一下【DB系列】Mybatis之参数传递的几种姿势

mybatis中传参一般可以区分为两类

  • 基本数据类型:int、string、long、Date;
  • 复杂数据类型:类(JavaBean、Integer等)和Map

一般来说基本的参数类型,在xml中的sql编写不需要额外的指定ParameterType,当然也可以根据实际需要指定

List<MoneyPo> queryByName(@Param("name") String name);


<select id="queryByName"  parameterType="java.lang.String" resultMap="BaseResultMap">
    select * from money where `name` = #{name}
</select>

通常有两种场景会经常看到ParmeterType,比如传参为Map

List<MoneyPo> queryByCondition(Map<String, Object> params);

<select id="queryByCondition" resultMap="BaseResultMap" parameterType="java.util.Map">
    select
    <include refid="money_po"/>
    from money where 1=1
    <if test="id != null">
        and id = #{id}
    </if>
    <if test="name != null">
        and `name` = #{name}
    </if>
    <if test="is_deleted != null">
        and `is_deleted` = #{is_deleted}
    </if>
</select>

若传参为Java bean时,可以如下

@Data
public class QueryBean {
    private String name;
    private Long id;
}

List<MoneyPo> queryByBean(QueryBean queryBean);

<select id="queryByBean" resultMap="BaseResultMap" parameterType="com.git.hui.boot.mybatis.entity.QueryBean">
    select
    <include refid="money_po"/>
    from money where 1=1
    <if test="id != null">
        and id = #{id}
    </if>
    <if test="name != null">
        and `name` = #{name}
    </if>
</select>

说明

  • 上面的几个case中,也可以都不指定parameterType

上面说到的都是parameterType,那么什么时候会用到parameterMap呢?

当我们希望针对某些查询条件做一些TypeHandler时,除了在#{}中指定之外,借助parameterMap也是一个好的选择,如

List<MoneyPo> queryByNameV2(Map<String, Object> params);

对应的sql如下,这里主要是为了演示这个使用姿势,StrTypeHandler是一个自定义的类型抓换,不管传参什么类型,都转成String

<parameterMap id="queryMap" type="java.util.Map">
    <parameter property="name" typeHandler="com.git.hui.boot.mybatis.handler.StrTypeHandler"/>
</parameterMap>

<select id="queryByNameV2" parameterMap="queryMap" resultMap="BaseResultMap">
    select * from money where `name` = #{name}
</select>

2. 最后验证一下我们的使用

db中核心数据如下图

public void testV4() {
    System.out.println(moneyMapperV4.queryByName("1"));

    Map<String, Object> map = new HashMap<>();
    map.put("id", 1L);
    map.put("name", "一灰灰blog");
    System.out.println(moneyMapperV4.queryByCondition(map));

    QueryBean queryBean = new QueryBean();
    queryBean.setId(1L);
    queryBean.setName("一灰灰blog");
    System.out.println(moneyMapperV4.queryByBean(queryBean));


    Map<String, Object> map2 = new HashMap<>();
    map2.put("name", 120L); // 即便传入的是Long类型,最终传递到mysql时,借助TypeHandler也会会转换成字符串类型
    System.out.println(moneyMapperV4.queryByCondition(map2));
}

输出如下

[]
[MoneyPo(id=1, name=一灰灰blog, money=100, isDeleted=0, createAt=2019-04-18 17:01:40.0, updateAt=2019-04-18 17:01:40.0, cnt=null, bank=null)]
[MoneyPo(id=1, name=一灰灰blog, money=100, isDeleted=0, createAt=2019-04-18 17:01:40.0, updateAt=2019-04-18 17:01:40.0, cnt=null, bank=null)]
[MoneyPo(id=120, name=120, money=200, isDeleted=0, createAt=2021-05-24 20:04:39.0, updateAt=2021-09-27 19:21:40.0, cnt=null, bank=null)]

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如无书,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

19 - 19.ResultMap、ResultType返回结果使用姿势

在使用mybatis进行数据库操作时,如果希望将返回结果映射为项目中定义的实体对象Entity时,ResultMap与ResultType就很重要了;它们两的主要区别在于ResultType指定指定实体对象,ResultMap则定义数据库字段与实体的映射关系

接下来通过简单的实例来看一下这两种的使用姿势

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库表

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. ResultMap & ResultType介绍

1. 使用区别

ResultMap:

  • 当数据库的字段与定义的实体对象不一致时(如下划线转驼峰,命名不一致等)通过<ResultMap>标签来定义映射关系,然后在sql查询标签中,通过resultMap来指定

ResultType:

  • db中的字段直接与实体对象进行映射时,选择ResultType,其value为实体类的全路径

2. 实例演示

注意上面的表结构,是以下划线的命名方式,接下来定义一个驼峰格式的实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MoneyPo {
    private Integer id;

    private String name;

    private Long money;

    private Integer isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

基于上面这个case,很明显当我们使用查询时,返回结果就需要做一个映射,此时就可以使用<ResultMap>方式

<resultMap id="BaseResultMap" type="com.git.hui.boot.mybatis.entity.MoneyPo">
    <id column="id" property="id" jdbcType="INTEGER"/>
    <result column="name" property="name" jdbcType="VARCHAR"/>
    <result column="money" property="money" jdbcType="INTEGER"/>
    <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
    <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
    <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="money_po">
  id, name, money, is_deleted, create_at, update_at
</sql>

<select id="queryByName"  parameterType="java.lang.String" resultMap="BaseResultMap">
    select * from money where `name` = #{name}
</select>

注意上面的select标签,通过resultMap来实现表字段与实体对象的转换关系(通过<resultMap>标签内的<result>来定义映射关系)

除了使用上面这种方式之外,也可以通过resultType来指定返回结果为Map,同样是可行的

<select id="queryMapsByName"  parameterType="java.lang.String" resultType="java.util.HashMap">
    select * from money where `name` = #{name}
</select>

对应的mapper接口内容如下

@Mapper
public interface MoneyMapperV4 {
    /**
     * int类型,最终的sql中参数替换的也是int
     * @param name
     * @return
     */
    List<MoneyPo> queryByName(@Param("name") String name);

    /**
     * 注意返回结果
     *
     * @param name
     * @return
     */
    List<HashMap<String, Object>> queryMapsByName(@Param("name") String name);
}

3. 测试验证

@Autowired
private MoneyMapperV4 moneyMapperV4;

public void testResQuery() {
    List<MoneyPo> list = moneyMapperV4.queryByName("一灰灰blog");
    System.out.println(list);
    List<HashMap<String, Object>> mapList = moneyMapperV4.queryMapsByName("一灰灰blog");
    System.out.println(mapList);
}

输出结果如下

[MoneyPo(id=1, name=一灰灰blog, money=100, isDeleted=0, createAt=2019-04-18 17:01:40.0, updateAt=2019-04-18 17:01:40.0, cnt=null, bank=null)]
[{is_deleted=false, money=100, name=一灰灰blog, update_at=2019-04-18 17:01:40.0, id=1, create_at=2019-04-18 17:01:40.0}]

请注意上面的输出,返回结果是Map时,key和db中的字段名完全一致

其次也可以从Mapper接口的返回定义上可以看出,虽然最终返回的是列表,但是我们定义的resultMap, resultType,都是对应的单个实体的映射关系

如何理解上面这句话呢?

  • 如果上面的sql改成只获取id,那么返回结果应该是定义为longe而不是List
<select id="queryIdByName" resultType="long">
    select id from money where `name` = #{name}
</select>

对应的mapper接口如下

List<Long> queryIdByName(String name);

4. 小结

ResultMap

  • 当希望实现sql返回的对象与项目中的实体类实现关联映射时,可以考虑通过resumtMap来实现

ResultType

  • 指定返回实体类型,可以是基础对象(long, int…) 也可以是Map,当指定一个具体的POJO时,db的表字段与pojo的field全名匹配映射

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

20 - 20.批量插入的几种姿势

在日常的业务需求开发过程中,批量插入属于非常常见的case,在mybatis的写法中,一般有下面三种使用姿势

  • 单个插入,业务代码中for循环调用
  • <foreach>标签来拼接批量插入sql
  • 复用会话,拆分小批量插入方式

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

2. 数据库表

用于测试的数据库

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. 批量插入

1. 单个插入,批量调用方式

这种方式理解起来最简单,一个单独的插入接口,业务上循环调用即可

@Mapper
public interface MoneyInsertMapper {
    /**
     * 写入
     * @param po
     * @return
     */
    int save(@Param("po") MoneyPo po);
}

对应的xml如下

<resultMap id="BaseResultMap" type="com.git.hui.boot.mybatis.entity.MoneyPo">
    <id column="id" property="id" jdbcType="INTEGER"/>
    <result column="name" property="name" jdbcType="VARCHAR"/>
    <result column="money" property="money" jdbcType="INTEGER"/>
    <result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
    <result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
    <result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<insert id="save" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" useGeneratedKeys="true" keyProperty="po.id">
  INSERT INTO `money` (`name`, `money`, `is_deleted`)
  VALUES
(#{po.name}, #{po.money}, #{po.isDeleted});
</insert>

使用姿势如下

private MoneyPo buildPo() {
    MoneyPo po = new MoneyPo();
    po.setName("mybatis user");
    po.setMoney((long) random.nextInt(12343));
    po.setIsDeleted(0);
    return po;
}

public void testBatchInsert() {
    for (int i = 0; i < 10; i++) {
        moneyInsertMapper.save(buildPo());
    }
}

小结

上面这种方式的优点就是简单直观,缺点就是db交互次数多,开销大

2. BATCH批处理模式

针对上面做一个简单的优化,使用BATCH批处理模式,实现会话复用,避免每次请求都重新维护一个链接,导致额外开销,可以如下操作

try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    MoneyInsertMapper moneyInsertMapper = sqlSession.getMapper(MoneyInsertMapper.class);
    for (int i = 0; i < 10; i++) {
        moneyInsertMapper.save(buildPo());
    }
    sqlSession.commit();
}

说明

  • sqlSession.commit若放在for循环内,则每保存一个就提交,db中就可以查询到
  • 若如上面放在for循环外,则所有的一起提交

3. foreach实现sql拼接

另外一种直观的想法就是组装批量插入sql,这里主要是借助foreach来处理

<insert id="batchSave" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo"  useGeneratedKeys="true" keyProperty="id">
    insert ignore into `money` (`name`, `money`, `is_deleted`)
    values
    <foreach collection="list" item="item" index="index" separator=",">
        (#{item.name}, #{item.money}, #{item.isDeleted})
    </foreach>
</insert>

对应的mapper接口如下

/**
 * 批量写入
 * @param list
 * @return
 */
int batchSave(@Param("list") List<MoneyPo> list);

实际使用case如下

List<MoneyPo> list = new ArrayList<>();
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
moneyInsertMapper.batchSave(list);

小结

使用sql批量插入的方式,优点是db交互次数少,在插入数量可控时,相比于前者开销更小

缺点也很明显,当一次插入的数量太多时,组装的sql既有可能直接超过了db的限制,无法执行了

4. 分批BATCH模式

接下来的这种方式在上面的基础上进行处理,区别在于对List进行拆分,避免一次插入太多数据,其次就是真个操作复用一个会话,避免每一次的交互都重开一个会话,导致额外的开销

其使用姿势如下

try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
    MoneyInsertMapper moneyInsertMapper = sqlSession.getMapper(MoneyInsertMapper.class);
    for (List<MoneyPo> subList : Lists.partition(list, 2)) {
        moneyInsertMapper.batchSave(subList);
    }
    sqlSession.commit();
}

与第二种使用姿势差不多,区别在于结合了第三种批量的优势,对大列表进行拆分,实现复用会话 + 批量插入

5. 如何选择

上面介绍了几种不同的批量插入方式,那我们应该选择哪种呢?

就我个人的观点来讲,2,3,4这三个在一般的业务场景下并没有太大的区别,如果已知每次批量写入的数据不多(比如几十条),那么使用3就是最简单的case了

如果批量插入的数据非常多,那么方案4可能更加优雅

如果我们希望开发一个批量导数据的功能,那么方案2无疑是更好的选择

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog