MyBatisPlus
# 简介
# 快速入门
这边可以参考官网的快速入门 (opens new window),按照教程创建好数据库和表之后,开始初始化工程,这边我们使用Springboot(以mysql做为默认的数据库)。
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
- 配置
spring:
# 引入MySQL配置
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_plus_learn?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8&tinyInt1isBit=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
server:
port: 8081
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- 书写代码
以前的情况,我们需要去配置mapper.xml,然后使得这个xml文件去映射到我对应实体类的mapper,这个过程中容易因为粗心,或者配置文件写错导致开发效率麻烦。
创建实体类
@Data
public class User
{
private Long id;
private String name;
private Integer age;
private String email;
}
创建mapper
// 继承基本的类BaseMapper<T>
public interface UserMapper extends BaseMapper<User>
{
// 这里已经封装好了所有简单的CRUD操作
// 如果有特殊的业务,就可以在这里写
}
测试
@SpringBootTest
public class SampleTest
{
@Autowired
private UserMapper userMapper;
@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
}
自此我们一个简单的查询操作就完成了,在这个过程中我们没有写一行sql语句,也没有配置任何的mapper文件,非常高效!
# 配置日志
在application.yml中配置如下,可以实现在控制台中打印日志。
mybatis-plus:
configuration:
# 配置日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# CRUD扩展
# 插入操作
- 首先我们进行一个简单的插入操作。
@Test
public void testInsert () {
// 这里我们没有设置ID
User testUser = new User();
testUser.setName("测试用户");
testUser.setAge(3);
testUser.setEmail("151515151@qq.com");
int insert = userMapper.insert(testUser);
System.out.println(insert);
}
- 这里看到控制台打印了:
Parameters: 1439073877085921281(Long), 测试用户(String), 3(Integer), 151515151@qq.com(String)
我们可以看到这个ID非常的长,下面我们对这个ID进行分析。
# 主键生成策略
数据库插入的id的默认值为: 全局的唯一ID
雪花算法:分布式唯一ID生成算法 (opens new window),几乎可以保证全球唯一
# 不同主键策略
这边我们先看一下IdType的源码
/**
* 生成ID类型枚举类
*
* @author hubin
* @since 2015-11-10
*/
@Getter
public enum IdType {
/**
* 数据库ID自增
* <p>该类型请确保数据库设置了 ID自增 否则无效</p>
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4);
private final int key;
IdType(int key) {
this.key = key;
}
}
值 | 描述 |
---|---|
AUTO | 数据库ID自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert前自行set主键值 |
ASSIGN_ID | 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator 的方法nextId (默认实现类为DefaultIdentifierGenerator 雪花算法) |
ASSIGN_UUID | 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator 的方法nextUUID (默认default方法) |
ID_WORKER | 分布式全局唯一ID 长整型类型(please use ASSIGN_ID ) |
UUID | 32位UUID字符串(please use ASSIGN_UUID ) |
ID_WORKER_STR | 分布式全局唯一ID 字符串类型(please use ASSIGN_ID ) |
可以发现,当我们插入对象的id为空的时候,这时候ID会被自动填充,而此时我们的User表是数字类型,所以会采用雪花算法,由此会为我们生成一个非常长的ID。
下面我们来试一下主键自增的策略:
- 实体类字段上配置:@TableId(type = IdType.AUTO)
- 在数据库里记得把主键字段选择自动递增(不然代码和数据库不统一,会报错)
再次运行我们的测试方法,可以得到以下结果:
1439073877085921282 测试用户 3 151515151@qq.com
再运行一次,得到以下结果:
1439073877085921283 测试用户 3 151515151@qq.com
自从我们就实现主键的自增策略!
# 更新操作
接下来,我们再来试一试更新操作,通过更新操作我们来认识以下MybatisPlus是如何实现动态SQL拼接的。
- baseMapper中封装了通用的更新方法
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(@Param(Constants.ENTITY) T entity);
注意这里虽然名称是根据根据ID修改,但是传入的参数是一个实体类
@Test
public void testUpdate() {
User updateUser = new User();
updateUser.setId(1439073877085921284L);
updateUser.setName("测试用户修改了");
updateUser.setAge(10);
userMapper.updateById(updateUser);
}
// 控制台打印的sql语句
// UPDATE user SET name=?, age=? WHERE id=?
- 现在我们再去添加一个修改的字段,把邮箱也修改一下,看看控制台打印了什么。
@Test
public void testUpdate() {
User updateUser = new User();
updateUser.setId(1439073877085921284L);
updateUser.setName("测试用户修改了");
updateUser.setAge(10);
updateUser.setEmail("846212939@qq.com");
userMapper.updateById(updateUser);
}
// 控制台打印的sql语句
// UPDATE user SET name=?, age=?, email=? WHERE id=?
通过这两个小例子发现所有的SQL语句都是自动帮我们动态配置了的。
# 自动填充
一般来说一个表需要创建时间、修改时间!这些操作都是自动化完成的,我们不希望在我操作的时候,我要手工的去录入这些时间,而是自动化完成
# 数据库的方式
工作中是不允许的,因为数据库已经设计好了,一般来说是不会去动数据库的
这边只是单纯演示一下。
- 首先在User表添加两个字段create_time,update_time:
ALTER TABLE `mybatis_plus_learn`.`user`
MODIFY COLUMN `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' AFTER `password`,
MODIFY COLUMN `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间' AFTER `create_time`;
- 在User实体类中添加两个属性:
private Date createTime;
private Date updateTime;
- 再次运行插入方法:
1439073877085921285 测试用户 3 151515151@qq.com 2021-09-19 10:14:37 2021-09-19 10:14:37
- 对该数据进行修改:
1439073877085921285 测试用户修改了 15 846212939@qq.com 2021-09-19 10:14:37 2021-09-19 10:29:06
这样可以看到数据在录入的时候,创建时间和修改时间都会自动填充,但是当数据在更新的时候,只有修改时间会更新。
# 代码的方式
MybatisPlus-自动填充 (opens new window)
- 首先把刚才对数据库的操作还原:
ALTER TABLE `mybatis_plus_learn`.`user`
MODIFY COLUMN `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间' AFTER `password`,
MODIFY COLUMN `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间' AFTER `create_time`;
- 接着在User实体类的属性上添加对应的注解:
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
- 这边查看一下源码:
/**
* 字段填充策略枚举类
*
* <p>
* 判断注入的 insert 和 update 的 sql 脚本是否在对应情况下忽略掉字段的 if 标签生成
* <if test="...">......</if>
* 判断优先级比 {@link FieldStrategy} 高
* </p>
*
* @author hubin
* @since 2017-06-27
*/
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入时填充字段
*/
INSERT,
/**
* 更新时填充字段
*/
UPDATE,
/**
* 插入和更新时填充字段
*/
INSERT_UPDATE
}
值 | 描述 |
---|---|
DEFAULT | 默认不处理 |
INSERT | 插入时填充字段 |
UPDATE | 更新时填充字段 |
INSERT_UPDATE | 插入和更新时填充字段 |
- 自定义实现类 MyMetaObjectHandler
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler
{
/**
* 插入元对象字段填充(用于插入时对公共字段的填充)
*
* @param metaObject 元对象
*/
@Override
public void insertFill(MetaObject metaObject)
{
log.info("start insert fill ....");
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
/**
* 更新元对象字段填充(用于更新时对公共字段的填充)
*
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject)
{
log.info("start update fill ....");
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
- 进行插入操作:
1439073877085921286 测试用户999 3 151515151@qq.com 2021-09-19 11:30:38 2021-09-19 11:30:38
- 进行修改操作:
1439073877085921286 测试用户修改了 15 846212939@qq.com 2021-09-19 11:30:38 2021-09-19 11:31:33
这样我们在不修改数据库情况下,就实现了创建时间、更新时间两个字段的自动填充操作。
# 乐观锁处理
MybatisPlus-乐观锁 (opens new window)
乐观锁:十分乐观,总是认为不会出现问题,无论干什么事情,都不会上锁
悲观锁:十分悲观,总是认为会出现问题,无论干什么事情,都会上锁
乐观锁实现方式:
- 取出记录时,获取当前version
- 更新时,带上这个version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果version不对,就更新失败
-- Thread A
update user set name = "wxx", version = version + 1
where id = 2 and version = 1
-- Thread B
update user set name = "wxx", version = version + 1
where id = 2 and version = 1
两个进程进行争夺,当线程B如果先完成了,那么线程A就会完成不了。
下面我们通过代码来实现一个简单的乐观锁案例:
- 对数据库进行修改:
ALTER TABLE `mybatis_plus_learn`.`user`
ADD COLUMN `version` int(20) DEFAULT '1' NULL COMMENT '乐观锁' AFTER `update_time`;
- 对实体类进行修改:
@Version
private Integer version;
- 配置插件
@Configuration
@MapperScan("com.xu.mybatis_plus_learn.mapper")
public class MyConfig
{
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
- 测试案例
@Test
public void testOptimisticLocker() {
// 单线程的情况
User updateUser = userMapper.selectById(1439073877085921286L);
updateUser.setName("乐观锁用户2");
userMapper.updateById(updateUser);
}
// 控制台SQL语句
// UPDATE user SET name=?, age=?, email=?, create_time=?, update_time=?, version=? WHERE id=? AND version=?
这里我们可以看到SQL语句已经不一样了,多了一个and version = ?
再看看数据库的内容:
乐观锁用户2 15 846212939@qq.com 2021-09-19 11:30:38 2021-09-19 15:03:38 2
下面演示一下多线程的情况:
@Test
public void testOptimisticLocker2() {
// 多线程的情况
// 线程1
User updateUser1 = userMapper.selectById(1439073877085921285L);
updateUser1.setName("乐观锁用户-多线程1");
// 线程2
User updateUser2 = userMapper.selectById(1439073877085921285L);
updateUser2.setName("乐观锁用户-多线程2");
// 这里模仿线程2抢先了
userMapper.updateById(updateUser2);
userMapper.updateById(updateUser1);
}
观察一下控制台的打印:
==> Parameters: 乐观锁用户-多线程2(String), 15(Integer), 846212939@qq.com(String), 2021-09-19 10:18:17.0(Timestamp), 2021-09-19 15:12:35.214(Timestamp), 2(Integer), 1439073877085921285(Long), 1(Integer) <== Updates: 1
==> Parameters: 乐观锁用户-多线程1(String), 15(Integer), 846212939@qq.com(String), 2021-09-19 10:18:17.0(Timestamp), 2021-09-19 15:12:35.237(Timestamp), 2(Integer), 1439073877085921285(Long), 1(Integer) <== Updates: 0
可以看到,我们的线程1执行了,但是并没有更新,因为此时version已经因为线程2执行之后更改为2了,而线程1的SQL语句UPDATE user SET name=?, age=?, email=?, create_time=?, update_time=?, version=? WHERE id=? AND version=?的version为1,因此无法进行修改。
# 查询操作
- 根据ID查询
@Test
public void testSelectById() {
User user = userMapper.selectById(1);
System.out.println(user);
}
查询结果:
User(id=1, name=Jone, age=18, email=test1@baomidou.com, password=123456, createTime=null, updateTime=null, version=1)
- 批量查询
@Test
public void selectBatchIds() {
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}
查询结果:
User(id=1, name=Jone, age=18, email=test1@baomidou.com, password=123456, createTime=null, updateTime=null, version=1) User(id=2, name=Jack, age=20, email=test2@baomidou.com, password=123456, createTime=null, updateTime=null, version=1) User(id=3, name=Tom, age=28, email=test3@baomidou.com, password=123456, createTime=null, updateTime=null, version=1)
- 条件查询
@Test
public void selectByMap() {
// 简单的条件查询,复杂的查询后面会讲述
HashMap<String, Object> map = new HashMap<>();
map.put("email", "151515151@qq.com");
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}
查询结果:
User(id=1439073877085921281, name=测试用户, age=3, email=151515151@qq.com, password=null, createTime=null, updateTime=null, version=1) User(id=1439073877085921282, name=测试用户, age=3, email=151515151@qq.com, password=null, createTime=null, updateTime=null, version=1) User(id=1439073877085921283, name=测试用户, age=3, email=151515151@qq.com, password=null, createTime=null, updateTime=null, version=1)
# 分页查询
MybatisPlus-分页插件 (opens new window)
- 注册分页插件
@Configuration
@MapperScan("com.xu.mybatis_plus_learn.mapper")
public class MyConfig
{
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
- 使用分页插件进行查询
@Test
public void selectByPage() {
Page<User> page = new Page<>(1, 5);
userMapper.selectPage(page, null);
page.getRecords().forEach(System.out::println);
}
==> Preparing: SELECT id,name,age,email,password,create_time,update_time,version FROM user LIMIT ?
==> Parameters: 5(Long)
<== Columns: id, name, age, email, password, create_time, update_time, version
<== Row: 1, Jone, 18, test1@baomidou.com, 123456, null, null, 1
<== Row: 2, Jack, 20, test2@baomidou.com, 123456, null, null, 1
<== Row: 3, Tom, 28, test3@baomidou.com, 123456, null, null, 1
<== Row: 4, 66666, 7, 6666, 123456, null, null, 1
<== Row: 5, Billie, 24, test5@baomidou.com, 123456, null, null, 1
<== Total: 5
# 删除操作
- 普通删除
@Test
public void deleteById() {
userMapper.deleteById(1439073877085921286L);
}
@Test
public void deleteMap() {
HashMap<String, Object> map = new HashMap<>();
map.put("name", "测试用户");
userMapper.deleteByMap(map);
}
- 逻辑删除
在工作中,我们的数据并不是真正的删除,在数据库中没有被移除,而是通过一个变量让他来失效。
MybatisPlus-逻辑删除 (opens new window)
比如我们文章的管理,一般删除一个文章不是真正的删除了,而是丢到了垃圾桶。
下面我们来书写一个逻辑删除的小案例:
- 修改数据库:
ALTER TABLE `mybatis_plus_learn`.`user`
ADD COLUMN `deleted` int(1) NULL DEFAULT 0 COMMENT '逻辑删除' AFTER `version`;
- 修改实体类字段:
@TableLogic
private Integer deleted;
- 配置文件:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
- 删除测试:
==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0
==> Parameters: 1439073877085921286(Long)
<== Updates: 1
可以看到逻辑删除本质上是一个更新操作,把逻辑删除这个字段进行了修改而已。
# 性能分析插件
在我们日常开发工作当中,避免不了查看当前程序所执行的SQL语句,有时候还需要根据SQL的执行时间来优化sql语句。
MybatisPlus-性能分析插件 (opens new window)
旧版的PerformanceInterceptor已经被弃用
- p6spy 依赖引入
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>最新版本</version>
</dependency>
- application.yml 配置
url和driver-class-name要修改,同时记得设置为了生产或者测试环境
spring:
# 引入MySQL配置
datasource:
url: jdbc:p6spy:mysql://127.0.0.1:3306/mybatis_plus_learn?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8&tinyInt1isBit=true
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
username: root
password: root
profiles:
active: dev
// 控制台打印
Consume Time:11 ms 2021-09-20 00:57:53
Execute SQL:SELECT id,name,age,email,password,create_time,update_time,version,deleted FROM user WHERE deleted=0
# 条件构造器
在日常的查询中,我们可能会遇到一些特殊条件,比如年龄在多少区间的、某某字段不为空的情况,这时候就需要我们用到条件构造器了。
下面我们通过书写几个案例来熟悉一下:
- 案例1-多条件
@Test
private void test1() {
// 查询name不为空的用户,并且邮箱不为空的用户,并且年龄大于12
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name").isNotNull("email").eq("age", 12);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
Consume Time:16 ms 2021-09-20 09:43:03
Execute SQL:SELECT id,name,age,email,password,create_time,update_time,version,deleted FROM user WHERE deleted=0 AND (name IS NOT NULL AND email IS NOT NULL AND age = 12)
- 案例2-区间查询
@Test
public void test2() {
// 查询一个区间的人数
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age", 20, 30);
Integer integer = userMapper.selectCount(wrapper);
System.out.println(integer);
}
Consume Time:17 ms 2021-09-20 09:52:35
Execute SQL:SELECT COUNT( * ) FROM user WHERE deleted=0 AND (age BETWEEN 20 AND 30)
- 案例3-模糊查询
@Test
public void test3() {
// 模糊查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.likeRight("email", "t");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
Consume Time:14 ms 2021-09-20 09:56:24
Execute SQL:SELECT id,name,age,email,password,create_time,update_time,version,deleted FROM user WHERE deleted=0 AND (email LIKE 't%')
# 代码生成器
MybatisPLus-代码生成器 (opens new window)
MybatisPlus-快速开发插件 (opens new window)