在 Spring Boot 中整合 MyBatis Plus

MyBatis Plus 是 MyBatis 框架的一个增强。除了基本的 MyBatis 功能外,它还提供了快速的 CURD 方法,以及投影查询、分页查询、动态条件等等功能,极大的提高了开发效率。

本文将会通过案例教你如何在 Spring Boot 中整合 MyBatis Plus。

文中使用的软件版本如下:

  • Spring Boot:3.0.3
  • MySQL:8.0.0
  • MyBatis Plus:3.5.4

初始化演示数据

首先在本地数据库执行以下 SQL 脚本,创建一张名为 t_user 的数据表:

CREATE TABLE `t_user` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `enabled` tinyint unsigned NOT NULL COMMENT '是否启用。0:禁用,1:启用',
  `name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '名字',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户';

然后再执行如下脚本,创建初始记录:

INSERT INTO `demo`.`t_user` (`id`, `create_at`, `enabled`, `name`) VALUES (3, '2023-10-31 15:11:34', 1, '刘备');
INSERT INTO `demo`.`t_user` (`id`, `create_at`, `enabled`, `name`) VALUES (4, '2023-10-31 15:11:34', 1, '关羽');
INSERT INTO `demo`.`t_user` (`id`, `create_at`, `enabled`, `name`) VALUES (5, '2023-10-31 15:11:34', 1, '张飞');

创建应用

通过 Spring Initializer 快速初始化一个 Spring Boot 工程。添加 mybatis-plus-boot-starter 以及 mysql-connector-j (MySQL 驱动)依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</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.5.4</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

应用配置

application.yaml 中配置必要的基础配置信息:

spring:
  # 基本的数据源配置
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
    username: root
    password: root

# MyBatis Plus 的配置
mybatis-plus:
  # mybatis 配置文件的路径
  # config-location: "classpath:mybatis/mybatis.config"
  # mapper 映射文件的路径,可以有多个 
  mapper-locations:
    - "classpath*:mappers/**/*.xml"

除了必须的数据源配置外,还定义了 MyBatis Plus 的配置。

  • config-location:指定了 MyBatis 配置文件的路径(非必须的)。
  • mapper-locations:指定要加载 mapper 文件,支持使用通配符。本例中的配置表示加载 classpath 下 mappers 目录以及其所有子目录下所有以 xml 结尾的文件。该配置是一个数组,可以配置多个加载路径。

更多关于 MyBatis Plus 的可用配置,可以参考其 官方文档

实体类

创建一个实体类 User,对应上面的 t_user 表:

package cn.springdoc.demo.entity;

import java.time.LocalDateTime;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

@TableName("t_user")
public class User {

    @TableId(type = IdType.AUTO)
    private Long id;

    @TableField
    private String name;

    @TableField
    private Boolean enabled;

    @TableField
    private LocalDateTime createAt;

    // 省略 get/set/toString 等方法
}

@TableName 注解是必须的,用于指定数据库中的表名称。 @TableId 注解也是必须的,用户指定表的 ID 字段,并且通过 type 属性设置了 ID 值为 “数据库自增”。

使用 @TableField 注解来定义与数据表中对应的列。注意,表中的列名使用的是下划线,而实体类中字段名称使用的是驼峰。 框架会自动完成这个转换,你不用担心。

如果你的表列名和实体属性名称之间不能自动完成这种转换,需要通过该注解的 value 属性来定义列名,如:@TableField("u_nick_name")

@TableField 注解还有一个重要的 boolean 属性:exist,用于定义那些在实体中的 “非 DB 列” 字段。

例如:需要在实体中添加一个额外的 nickName 字段,用于封装检索的结果,这个字段并在表中并没有对应的数据列,此时就需要设置 exist 属性为 false

@TableField(exist = false)
private String nickName;

否则在运行时你可能会遇到 “Unknown column” 异常:

org.springframework.jdbc.BadSqlGrammarException: 
### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: Unknown column 'nick_name' in 'field list'
### The error may exist in cn/springdoc/demo/mapper/UserMapper.java (best guess)
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT  id,name,enabled,create_at,nick_name  FROM t_user
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'nick_name' in 'field list'
; bad SQL grammar []

更多可用的注解,可以参阅 官方文档

Mapper

创建 UserMapper 接口,继承 BaseMapper,并且通过泛型指定实体类类型:

package cn.springdoc.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.springdoc.demo.entity.User;

public interface UserMapper extends BaseMapper<User>{

   /**
    * 根据 name 检索一条记录
    * @param name
    * @return
    */
   User getByName (String name);
}

BaseMapper 已经预置了很多 CRUD 的方法,可以直接使用。

并且,还在这个接口中定义了一个自定义方法 getByName(),根据 name 检索一条记录。添加这个方法的目的是测试 Mapper 映射文件是否成功加载。

Mapper 映射文件

src/main/resources/mappers 目录下创建 UserMapper.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="cn.springdoc.demo.mapper.UserMapper">

    <select id="getByName" resultType="cn.springdoc.demo.entity.User">
        SELECT * FROM `t_user` WHERE `name` = #{name}
    </select>

</mapper>

UserMapper.xml 中,通过 select 节点实现了 UserMapper 接口中的 getByName 方法。

Service

MyBatis Plus 甚至还提供了一个 ServiceImpl<M extends BaseMapper<T>, T> 抽象类,它也预定义了很多 CRUD 的方法。

我们的 Service 类可以直接继承它,指定泛型为实体类的 Mapper 接口以及实体类类型。

package cn.springdoc.demo.service;

import org.springframework.stereotype.Service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import cn.springdoc.demo.entity.User;
import cn.springdoc.demo.mapper.UserMapper;

@Service
public class UserService extends ServiceImpl<UserMapper, User>{

}

Service 层抽象接口

如果你喜欢抽象出 Service 接口的话,MyBatis Plus 也提供了一个接口:IService<T> 可用于继承。

定义业务接口,继承 IService

import com.baomidou.mybatisplus.extension.service.IService;
import cn.springdoc.demo.entity.User;
// UserService 继承 IService 接口
public interface UserService  extends IService<User>{
}

业务接口实现类 UserServiceImpl,实现业务接口并且继承 ServiceImpl 抽象类:

package cn.springdoc.demo.service;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.springdoc.demo.entity.User;
import cn.springdoc.demo.mapper.UserMapper;

// UserServiceImpl 实现类,实现 UserService 接口,并且继承 ServiceImpl 抽象类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

我个人觉得除非实在是有必要,不然真的没必要在 Service 抽象出接口。写一辈子代码,也遇不到几次 Service 多实现的场景。

配置 Mapper 扫描包

在 main 类上添加 @MapperScan 注解,指定 mapper 接口所在的包:

package cn.springdoc.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("cn.springdoc.demo.mapper") // mapper 接口所在的包
public class DemoApplication {

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

在日志中输出 SQL

为了看到执行的 SQL 日志,可以在 application.yaml 把 mapper 所在包的日志级别设置为 DEBUG

logging:
  level:
    cn.springdoc.demo.mapper: DEBUG

至此,整合就完成了。

测试

创建测试类:

package cn.springdoc.demo.test;


import java.util.List;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

import cn.springdoc.demo.entity.User;
import cn.springdoc.demo.mapper.UserMapper;
import cn.springdoc.demo.service.UserService;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {

    static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);

    @Autowired
    UserService userService;

    @Test
    public void test() throws Exception {
        
        // 检索所有记录
        List<User> users = this.userService.list();
        users.stream().forEach(user -> {
            log.info("user = {}", user);
        });
        
        // 从 Service 中获取到注入的 Mapper,强制转换为具体的实体 Mapper
        UserMapper userMapper = (UserMapper) this.userService.getBaseMapper();
        // 调用 Mapper 中的方法
        User user = userMapper.getByName("刘备");
        
        log.info("user = {}", user);
    }
}

在测试类中注入了 UserService,执行了 2 个查询。

首先,使用 MyBatis Plus 提供的 list() 方法检索出表中的所有记录。

然后,再通过 getBaseMapper() 方法获取到 Service 中注入的 BaseMapper 接口,并且强制转换为对应的 UserMapper。然后调用我们在接口中自定义的方法。

执行测试,输出日志如下:

[           main] c.s.demo.mapper.UserMapper.selectList    : ==>  Preparing: SELECT id,name,enabled,create_at FROM t_user
[           main] c.s.demo.mapper.UserMapper.selectList    : ==> Parameters: 
[           main] c.s.demo.mapper.UserMapper.selectList    : <==      Total: 3
[           main] c.s.demo.test.DemoApplicationTests       : user = User [id=3, name=刘备, enabled=true, createAt=2023-10-31T15:11:34]
[           main] c.s.demo.test.DemoApplicationTests       : user = User [id=4, name=关羽, enabled=true, createAt=2023-10-31T15:11:34]
[           main] c.s.demo.test.DemoApplicationTests       : user = User [id=5, name=张飞, enabled=true, createAt=2023-10-31T15:11:34]
[           main] c.s.demo.mapper.UserMapper.getByName     : ==>  Preparing: SELECT * FROM `t_user` WHERE `name` = ?
[           main] c.s.demo.mapper.UserMapper.getByName     : ==> Parameters: 刘备(String)
[           main] c.s.demo.mapper.UserMapper.getByName     : <==      Total: 1
[           main] c.s.demo.test.DemoApplicationTests       : user = User [id=3, name=刘备, enabled=true, createAt=2023-10-31T15:11:34]

如你所见,一切OK!