MyBatis-Plus
Mybatis-Plus(简称MP):是一个Mybatis的增强工具,在Mybatis的基础上只做增强不做改变,为简化开发、提高效率而生。
特性:
-
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
-
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
-
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
-
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
-
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可*配置,完美解决主键问题
-
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
-
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
-
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
-
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
-
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
-
内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
-
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
测试使用MP:
引入依赖:本次测试SpringBoot版本为2.2.2
<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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
准备数据库测试数据:
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)
);
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:
datasource:
url: jdbc:mysql://localhost:3306/mybatis-plus?serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置mybatis plus日志输出
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
测试连接:
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() throws SQLException {
// null,查询所有
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
测试添加用户:
可以发现MP会自动生成全局唯一的id。
分布式系统唯一ID生成方案:详见:博客园
1、数据库自增
2、UUID、UUID变种
3、Redis(单线程,原子操作)
4、SnowFlake(雪花算法)
5、Zookeeper(znode数据版本号)
6、MongDB(ObjectId)
MP自带的策略分析:
tips:mybatis-plus从3.3.0开始,默认使用雪花算法+UUID(没有-)
值 | 描述 |
---|---|
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(X) | 分布式全局唯一ID 长整型类型(please use ASSIGN_ID ) |
UUID(X) | 32位UUID字符串(please use ASSIGN_UUID ) |
ID_WORKER_STR(X) | 分布式全局唯一ID 字符串类型(please use ASSIGN_ID ) |
项目中经常会遇到一些数据,每次都使用相同的方式进行添加,例如创建/更新时间,这时候我们可以使用MP中的自动填充功能。
对于需要自动填充的字段:
// 插入自动填充字段
@TableField(fill = FieldFill.INSERT)
private Date createTime;
// 修改自动填充字段
@TableField(fill = FieldFill.UPDATE)
private Date updateTime;
// 插入和修改自动填充字段
// @TableField(fill = FieldFill.INSERT_UPDATE)
编写实现类:
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 我这里使用的版本比较旧,新版本使用对应的方法即可
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("*******start insert fill");
this.setFieldValByName("createTime", new Date(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("*******start update fill");
this.setFieldValByName("updateTime", new Date(),metaObject);
}
}
测试修改数据:修改时自动设置updateTime
丢失更新
(并发修改同一条记录,导致后提交的事务覆盖了之前的事务修改的数据)
解决方案:乐观锁、悲观锁。
这里介绍乐观锁:
主要适用场景:当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新。
实现方式:取出记录时,获取当前version,更新时带上这个version,执行更新时判断version是否改变,如果version不对就更新失败。
测试使用乐观锁:
1、为数据库中的表添加一个保存version的字段。
ALTER TABLE `user` ADD COLUMN `version` INT
2、为实体类的成员变量添加注解
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;
3、注入乐观锁插件(拦截器)
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor(){
return new OptimisticLockerInterceptor();
}
4、测试乐观锁
// 取出记录时,带上version
User user = userMapper.selectById(1362942307736760322L);
user.setAge(110);
// 更新时,对比version
userMapper.updateById(user);
可以发现修改成功后version自动加1了。
乐观锁修改失败:
// 取出记录时,带上version
User user = userMapper.selectById(1362942307736760322L);
user.setAge(999);
user.setVersion(user.getVersion() + 1); // 模拟已经被其他线程修改了数据
// 更新时,对比version
userMapper.updateById(user);
测试批量id查询和简单条件查询:
@Test
void testBatch(){
List<User> userList = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
userList.forEach(System.out::println);
Map<String,Object> map = new HashMap<>();
map.put("name","zhangsan");
map.put("age",110);
// name=zhangsan AND age=110
userMapper.selectByMap(map);
}
注入分页插件:
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
测试分页查询:
void testPage(){
Page<User> page = new Page<>(1,3); // 当前页,记录数
userMapper.selectPage(page, null);// 分页查询
System.out.println("当前页:"+page.getCurrent());
System.out.println("当前页所有数据:"+page.getRecords());
System.out.println("每页显示记录数:"+page.getSize());
System.out.println("总记录数:"+page.getTotal());
System.out.println("总页数:"+page.getPages());
System.out.println("是否有下一页:"+page.hasNext());
System.out.println("是否有上一页:"+page.hasPrevious());
}
物理删除(真实删除):将对应数据从数据库中删除,之后查询不到这条数据
逻辑删除(假删除):将对应数据中代表删除状态修改为被删除状态
,之后在数据库中仍能看到这条数据。
数据库添加deleted字段:
ALTER TABLE `user` ADD COLUMN `deleted` boolean
在成员属性上添加注解:
@TableLogic // 逻辑删除状态
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
注入逻辑删除插件:
@Bean
public ISqlInjector iSqlInjector(){
return new LogicSqlInjector();
}
测试删除数据:
userMapper.deleteById(1L);
发现只是将该条记录中的deleted字段修改为1。
测试查询该条数据:
被逻辑删除的数据也是查询不到的。
性能分析插件:用于输出每条SQL语句及执行时间,开发环境使用,超过指定时间,停止运行,有助于发现问题。
注入性能分析插件:
@Bean
@Profile(value = {"dev","test"}) // 开发、测试环境生效
public PerformanceInterceptor performanceInterceptor(){
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(500); // 超时时间,超过不执行sql
performanceInterceptor.setFormat(false); // 是否格式化
return performanceInterceptor;
}
测试:
复杂条件查询:更加复杂的条件查询需要使用wrapper(条件构造器)
Wrapper : 条件构造抽象类,最顶端父类
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : Entity 对象封装操作类,不是用lambda语法
- UpdateWrapper : Update 条件封装,用于Entity对象更新操作
- AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。
- LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
- LambdaUpdateWrapper : Lambda 更新封装Wrapper
/**
* 测试QueryWrapper基本使用
*/
@Test
void testWrapper() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
// ge(>=)、gt(>)、le(<=)、lt(<)、isNull、isNotNull
// wrapper.ge("age",10).gt("version",1)
// .le("deleted",10).lt("create_time",new Date())
// .isNotNull("name");
// eq(=)、ne(≠)
// wrapper.eq("name","zhangsan").eq("age",110)
// .ne("deleted",1);
// (包含边界)between、notBetween
// wrapper.between("age",110,120)
// .notBetween("version",1,2);
// allEq(每个字段的值都相等)
// Map<String,Object> map = new HashMap<>();
// map.put("name","zhangsan");
// map.put("age",110);
// map.put("version",3);
//
// wrapper.allEq(map);
// like、notLike、likeLeft、likeRight
// wrapper.like("name","zhangsan") // %zhangsan%
// .likeRight("email","lisi"); // lisi%
// in、notIn、inSql(子查询)、notInSql、exists、notExists
// wrapper.in("age",Collections.singletonList(110))
// .inSql("id","select id from user where id>3");
// or、and
// name=lisi OR age=110,不使用or就默认使用and
// wrapper.eq("name","lisi").or().eq("age",110);
// 嵌套or、嵌套and
// name=lisi OR(age=110 AND version=3)
// wrapper.eq("name","lisi")
// .or(i->i.eq("age",110).eq("version",3));
// orderBy、OrderByDesc(降序)、OrderByAsc(升序)
// wrapper.orderByDesc("id");
// List<User> users = userMapper.selectList(wrapper);
// System.out.println(users);
// last,直接拼接在sql的最后面,有sql注入风险,并且只能使用一次,以最后一次为准
// wrapper.last("limit 1");
// 指定要查询的列
// wrapper.select("name","age","version");
// set、setSql
// UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>();
// updateWrapper
// .like("name", "zhang")
// .set("name", "lisi") // 查询并设置值
// .setSql("email='lisi@qq.com'"); // 可以有子查询
// UPDATE user SET age=?, update_time=?, name=?, email = 'lisi@qq.com' WHERE deleted=0 AND name LIKE ?
User user = userMapper.selectOne(wrapper); // 有多个结果会报错
System.out.println(user);
}