MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
create database mybatis_plus;表结构:

数据库 Schema 脚本
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);数据库 Data 脚本
DELETE FROM user;
INSERT INTO user (id, name, age, email)
VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');初始化工程
使用 Spring Initializr 快速初始化一个 Spring Boot 工程
添加依赖
添加 :spring-boot-starter、spring-boot-starter-test、 mybatis-plus-boot-starter、MySQL、lombok
lombok: 在项目中使用Lombok可以减少很多重复代码的书写。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>注意:

application.properties (请按版本来选择)
#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus
spring.datasource.username=root
spring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456这里的 url 使用了 ?serverTimezone=GMT%2B8 后缀,因为Spring Boot 2.1 集成了 8.0版本的jdbc驱动,这个版本的 jdbc 驱动需要添加这个后缀,否则运行测试用例报告如下错误:
java.sql.SQLException: The server time zone value ‘Öйú±ê׼ʱ¼ä’ is unrecognized or represents more
这里的 driver-class-name 使用了 com.mysql.cj.jdbc.Driver ,在 jdbc 8 中 建议使用这个驱动,之前的 com.mysql.jdbc.Driver 已经被废弃,否则运行测试用例的时候会有 WARN 信息
在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹
注意:扫描的包名根据实际情况修改
@SpringBootApplication
@MapperScan("com.oy.mp.mapper")
public class MPApplication {
public static void main(String[] args) {
SpringApplication.run(MPApplication.class, args);
}
}创建包 entity 编写实体类 User.java(此处使用了 Lombok 简化代码)
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}查看编译结果

创建包 mapper 编写Mapper 接口: UserMapper.java
@Component
public interface UserMapper extends BaseMapper<User> {
}添加测试类,进行功能测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class MPApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
public void testSelectList(){
// UserMapper 中的 selectList() 方法的参数为 MP 内置的条件封装器 Wrapper
// 所以不填写就是无任何条件
List<User> users = userMapper.selectList(null);
users.forEach(System.err::println);
}
} 
查看sql输出日志
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
@RunWith(SpringRunner.class)
@SpringBootTest
public class CRUDTests {
@Autowired
private UserMapper userMapper;
@Test
public void testInsert(){
User user = new User();
user.setName("Helen");
user.setAge(18);
user.setEmail("55317332@qq.com");
int result = userMapper.insert(user);
System.out.println(result); //影响的行数
System.out.println(user); //id自动回填
}
}注意: 数据库插入 id 值默认认为: 全局唯一的 id

工具类
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
/**
* <p>名称:IdWorker.java</p>
* <p>描述:分布式自增长ID</p>
* <pre>
* Twitter的 Snowflake JAVA实现方案
* </pre>
* 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
* 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
* 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
* 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
* 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
* 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
* 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
* <p>
* 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
*
* @author Polim
*/
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;
private final long workerId;
// 数据标识id部分
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}
}
User 配置注解
@Data
public class User {
// mp 默认id的生成策略是Long类型的 idWorker, 全局唯一的
@TableId(type = IdType.ID_WORKER)
private Long id;
private String name;
private Integer age;
private String email;
}注意:update时生成的sql自动是动态sql:UPDATE user SET age=? WHERE id=?
@Test
public void testUpdateById(){
User user = new User();
user.setId(1L);
user.setAge(28);
int result = userMapper.updateById(user);
System.err.println(result);
}项目中经常会遇到一些数据,每次都使用相同的方式填充,例如记录的创建时间,更新时间等。
我们可以使用MyBatis Plus的自动填充功能,完成这些字段的赋值工作:
在User表中添加datetime类型的新的字段 create_time、update_time

2. 实体上添加注解
@Component
public class DataMetaObjctHandler implements MetaObjectHandler {
// 在执行insert语句的时候被拦截操作的
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
// 修改语句
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}3. 测试

主要适用场景:当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新
乐观锁实现方式:

2. 实体类添加 version 字段 并添加 @Version 注解
// 在User类中 乐观锁修改版本号
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;3. 元对象处理接口添加 version 的 insert 默认值
@Override
public void insertFill(MetaObject metaObject) {
......
this.setFieldValByName("version", 1, metaObject);
}特别说明:
创建配置类
@Configuration
@EnableTransactionManagement
@MapperScan("com.oy.mp.mapper")
public class MPConfig {
/***
* 乐观锁插件
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor(){
return new OptimisticLockerInterceptor();
}
}/**
* 测试 乐观锁插件
*/
@Test
public void testOptimisticLocker(){
// 查询
User user = userMapper.selectById(1L);
// 修改数据
user.setName("Helen Yao");
user.setEmail("helen@qq.com");
user.setAge(21);
// 执行更新
int result = userMapper.updateById(user);
System.err.println(result);
}
@Test
public void testSelectById(){
User user = userMapper.selectById(1L);
System.err.println(user);
} 动态sql的foreach的功能
@Test
public void testSelectBatchIds() {
List list = new ArrayList();
list.add(1L);
list.add(2L);
list.add(3L);
List users = userMapper.selectBatchIds(list);
users.forEach(System.err::println);
} 通过map 封装查询条件
@Test
public void testSelectByMap(){
Map<String, Object> map = new HashMap<>();
map.put("name","Helen");
map.put("age",18);
List<User> users = userMapper.selectByMap(map);
users.forEach(System.err::println);
}注意: map 中的key 对应的是数据库中的列名。例如数据库 user_id, 实体类是userId, 这时 map 的 key 需要填写 user_id。
Mybatis Plus 自带分页插件,只要简单的配置即可实现分页功能
创建配置类 此时是可以删除主类中的 @MapperScan 扫描注解
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}测试 selectPage 分页
测试:最终通过 page 对象获取相关数据
@Test
public void testSelectPage(){
Page<User> page = new Page<>(1,5);
userMapper.selectPage(page,null);
page.getRecords().forEach(System.err::println);
System.err.println("当前页:"+page.getCurrent());
System.err.println("总页数:"+page.getPages());
System.err.println("每页显示记录数:"+page.getSize());
System.err.println("总记录数:"+page.getTotal());
System.err.println("是否有下一页:"+page.hasNext());
System.err.println("是否有上一页:"+page.hasPrevious());
}
测试 selectMapsPage 分页: 结果集是 Map
@Test
public void testSelectMapsPage(){
Page<User> page = new Page<>(1,5);
IPage<Map<String, Object>> Imap = userMapper.selectMapsPage(page, null);
// 注意,此行必须使用 Imap 获或记录列表,否则会与数据类型的转换错误
Imap.getRecords().forEach(System.err::println);
System.err.println(page.getCurrent());
System.err.println(page.getSize());
System.err.println(page.getTotal());
System.err.println(page.hasNext());
System.err.println(page.hasPrevious());
}
@Test
public void testDeleteById(){
int result = userMapper.deleteById(1364225405027991553L);
System.err.println(result);
}@Test
public void testDeleteBatchIds(){
List<Long> list = new ArrayList<>();
list.add(1L);
list.add(2L);
list.add(3L);
int result = userMapper.deleteBatchIds(list);
System.err.println(result);
} @Test
public void testDeleteByMap(){
Map<String, Object> map = new HashMap<>();
map.put("name","Helen");
map.put("age",18);
int result = userMapper.deleteByMap(map);
System.err.println(result);
}在数据库中添加delete 字段

实体类添加deleted 字段
并加上 @TableLogic 注解 和 @TableField(fill = FieldFill.INSERT) 注解
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;元对象处理器接口添加deleted的insert默认值
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("deleted",0,metaObject);
}
application.properties 加入配置
此为默认值,如果你的默认值和mp默认的一样,该配置可无
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0在 MybatisPlusConfig 中注册 Bean
@Bean
public ISqlInjector sqlInjector(){
return new LogicSqlInjector();
}测试逻辑删除
注意:被删除数据的deleted 字段的值必须是0, 才能被选取出来执行逻辑删除的操作/**
* 测试 逻辑删除
*/
@Test
public void testLogicDelete(){
int result = userMapper.deleteById(1L);
System.err.println(result);
}

测试逻辑删除后的查询
MyBatis Plus中查询操作也会自动添加逻辑删除字段的判断
/**
* 测试 逻辑删除后的查询:
* 不包括被逻辑删除的记录
*/
@Test
public void testLogicDeleteSelect() {
User user = new User();
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
测试后分析打印的sql语句,包含 WHERE deleted=0
SELECT id,name,age,email,create_time,update_time,deleted FROM user WHERE deleted=0
性能分析拦截器,用于输出每条 SQL 语句及其执行时间
SQL 性能执行分析,开发环境使用,超出指定时间,停止运行。有助于发现问题
参数说明
在 MybatisPlusConfig 中配置
@Bean
@Profile({"dev","test"}) // 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor(){
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
// performanceInterceptor.setMaxTime() ms, 超出此设置的ms则sql不执行
performanceInterceptor.setMaxTime(100);
performanceInterceptor.setFormat(true);
return performanceInterceptor;
}Spring Boot 中设置dev环境
#环境设置: dev、test、prod
spring.profiles.active=dev可以针对各环境新建不同的配置文件 application-dev.properties、application-test.properties、application-prod.properties
也可以自定义环境名称: 如test1、test2
常规测试
/**
* 测试 性能分析插件
*/
@Test
public void testPerformance(){
User user = new User();
user.setName("Jarry");
user.setEmail("Jarry@gmail.com");
user.setAge(23);
userMapper.insert(user);
}
将maxTime 改小之后再次进行测试
performanceInterceptor.setMaxTime(5);//ms,超过此处设置的ms不执行
如果执行时间过长,则抛出异常:The SQL execution time is too large,

如果想进行复杂条件查询,那么需要使用条件构造器 Wapper,涉及到如下方法

查询方式 | 说明 |
|---|---|
setSqlSelect | 设置 SELECT 查询字段 |
where | WHERE 语句,拼接 + WHERE 条件 |
and | AND 语句,拼接 + AND 字段=值 |
andNew | AND 语句,拼接 + AND (字段=值) |
or | OR 语句,拼接 + OR 字段=值 |
orNew | OR 语句,拼接 + OR (字段=值) |
eq | 等于= |
allEq | 基于 map 内容等于= |
ne | 不等于<> |
gt | 大于> |
ge | 大于等于>= |
lt | 小于< |
le | 小于等于<= |
like | 模糊查询 LIKE |
notLike | 模糊查询 NOT LIKE |
in | IN 查询 |
notIn | NOT IN 查询 |
isNull | NULL 值查询 |
isNotNull | IS NOT NULL |
groupBy | 分组 GROUP BY |
having | HAVING 关键词 |
orderBy | 排序 ORDER BY |
orderAsc | ASC 排序 ORDER BY |
orderDesc | DESC 排序 ORDER BY |
exists | EXISTS 条件语句 |
notExists | NOT EXISTS 条件语句 |
between | BETWEEN 条件语句 |
notBetween | NOT BETWEEN 条件语句 |
addFilter | 自由拼接 SQL |
last | 拼接在最后,例如:last(“LIMIT 1”) |
ge、gt、le、lt、isNull、isNotNull
@Test
public void testDelete(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("name")
.ge("age",12)
.isNotNull("email");
int result = userMapper.delete(queryWrapper);
System.err.println("delete return count=" + result);
}测试结果:
SQL:UPDATE user SET deleted=1 WHERE deleted=0 AND name IS NULL AND age >= ? AND email IS NOT NULLeq、ne
@Test
public void testSelectOne(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name","Jarry");
User user = userMapper.selectOne(queryWrapper);
System.err.println(user);
}注意: selectOne 返回的是一条实体记录,当出现多条时会报错
测试结果:
SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND name = ?