在 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!