目录
是什么?
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
官网地址:MyBatis-Plus (baomidou.com)
为什么要用?
因为在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 操作智能分析阻断,也可自定义拦截规则,预防误操作
使用
创建一个SpringBoot工程,导入依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<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>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
写配置文件:application.properties
# 四大金刚
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=用户名
spring.datasource.password=密码
创建启动类:
/**
* SpringBoot 的启动类
*/
@SpringBootApplication
public class MainApplication {
public static void main(String[] args){
SpringApplication.run(MainApplication.class,args);
}
}
创建表和插入数据的SQL语句:
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL auto_increment 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)
);
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');
SELECT * FROM user;
创建实体类:
/**
* 用户实体类
*/
@Data
@NoArgsConstructor
public class User {
@TableId(type = IdType.AUTO)
//主键
private Long id;
//名字
private String name;
//年龄
private Integer age;
//邮箱
private String email;
public User(Long id, String name, Integer age, String email) {
this.id = id;
this.name = name;
this.age = age;
this.email = email;
}
}
创建dao接口:需要继承 BaseMapper<T> 接口
/**
* 用户dao接口
*/
@Mapper
public interface UserDao extends BaseMapper<User> {
}
测试:
/**
* 用户的dao的测试类
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserDaoTest {
@Resource
private UserDao userDao;
/**
* 查询全部用户
*/
@Test
public void testListAll(){
List<User> users = userDao.selectList(null);
//输出
users.forEach(System.out::print);
}
}
结果:
可以看到我们什么dao里面什么也没写,可是结果已经出来了! 这只是最简单的演示,后面还有。
配置日志
在application.propeties 文件里面加一行这个就行了
# 打印日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
CRUD操作
添加操作
雪花算法生成的id
代码:
/**
* 测试添加
*/
@Test
public void testSave(){
//返回受影响的函数
int count = userDao.insert(new User(null,"zhangsan", 18, "zhangsan@qq.com"));
System.out.println("count = " + count);
}
结果:
可以看到此时我们并没有指定id,但是这里面却有id,这是它内部给我们生成的,是使用雪花算法算出来的。这个id几乎不会重复。可以去看下面这篇文章。
雪花算法的由来:
- 一:Twitter使用scala语言开源了一种分布式 id 生成算法——SnowFlake算法,被翻译成了雪花算法。
- 二:因为自然界中并不存在两片完全一样的雪花的,每一片雪花都拥有自己漂亮独特的形状、独一无二。雪花算法也表示生成的ID如雪花般独一无二。(有同学问为什么不是树叶,美团的叫树叶——Leaf)
雪花算法:雪花算法(详解) - 知乎 (zhihu.com)
使用数据库的主键自增
在 User 实体类上的 id 属性加一行这个就可以了:
@TableId(type = IdType.AUTO)
再去测试:
解释原因
我们点进 TableId 这个注解的源码可以发现 IdType 这个类就是指定主键的类型,我们再点进去 :可以发现实现雪花算法的是这个 ASSIGN_ID 而它的默认实现类是:DefaultIdentifierGenerator
@Getter
public enum IdType {
/**
* 数据库ID自增
*/
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),
}
DefaultIdentifierGenerator 类:
@Override
public Long nextId(Object entity) {
return sequence.nextId();
}
可以看到它点进去了一个 nextId() 的方法:Sequence 类中,想看的小伙伴可以去看一下
/**
* 分布式高效有序ID生产黑科技(sequence)
* <p>优化开源项目:https://gitee.com/yu120/sequence</p>
*
* @author hubin
* @since 2016-08-18
*/
public class Sequence {
/**
* 获取下一个 ID
*
* @return 下一个 ID
*/
public synchronized long nextId() {
long timestamp = timeGen();
//闰秒
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}
if (lastTimestamp == timestamp) {
// 相同毫秒内,序列号自增
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 同一毫秒的序列数已经达到最大
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒内,序列号置为 1 - 3 随机数
sequence = ThreadLocalRandom.current().nextLong(1, 3);
}
lastTimestamp = timestamp;
// 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
}
这大概就是那个id的由来了。而 IdType.AUTO 使用的就是 数据库自增的主键,那里面的注释写的也很清楚。
修改操作
我们修改最后一条记录的数据:
代码:
/**
* 测试修改
*/
@Test
public void testUpdate(){
int count = userDao.updateById(new User(1529446296992210946L, "wangwu", null, "wangwu@qq.com"));
System.out.println("count = " + count);
}
结果:
删除操作
删除最后一行的数据
代码:
/**
* 测试删除
*/
@Test
public void testDelete() {
int count = userDao.deleteById(1529446296992210946L);
System.out.println("count = " + count);
}
结果:
Wrapper 的使用
Wrapper 条件构造抽象类
-- AbstractWrapper 查询条件封装,用于生成 sql 中的 where 语句。
-- QueryWrapper Entity 对象封装操作类,用于查询。
-- UpdateWrapper Update 条件封装操作类,用于更新。
-- AbstractLambdaWrapper 使用 Lambda 表达式封装 wrapper
-- LambdaQueryWrapper 使用 Lambda 语法封装条件,用于查询。
-- LambdaUpdateWrapper 使用 Lambda 语法封装条件,用于更新。
QueryWrapper
/**
* 测试 queryWrapper
*/
@Test
public void testQueryWrapper(){
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
//模糊查询 %值%
queryWrapper.like("name", "zhang");
//大于
queryWrapper.gt("age", "18");
List<User> users = userDao.selectList(queryWrapper);
users.forEach(System.out::println);
}
结果:
插件与扩展
自动填充
介绍
阿里巴巴Java开发手册泰山版上这样写:
的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
gmt_create :添加记录时的时间 gmt_modified : 修改此记录的时间
方式一:在数据库上设置(不建议直接更改数据库)
实体类:
//添加时间 这里需要标记为填充字段 添加的时候填充
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
//修改时间 添加/修改的时候填充
@TableField(fill=FieldFill.INSERT_UPDATE)
private Date gmtModified;
试了一下图形化工具,我使不明白,还是用代码吧
SQL :
ALTER TABLE user ADD gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE user ADD gmt_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
结果:
执行修改代码之后:值已经改了
Java 代码层面(建议使用)
首先去除这两个字段的默认值:
ALTER TABLE user ALTER COLUMN gmt_create DROP DEFAULT;
ALTER TABLE user ALTER COLUMN gmt_modified DROP DEFAULT;
然后创建一个类:记得实现 MetaObjectHandler 接口
/**
* 元对象处理器
*/
@Component//注册到 IOC 容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 新增时的填充
*
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "gmt_create", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
}
/**
* 修改时的填充
*
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
}
}
测试:发现可以成功
乐观锁
介绍
乐观锁:在操作数据时非常乐观,认为被人不会同时修改数据。因此乐观锁不会上锁,只是在更新的时候判断一下在此期间别人是否修改了数据;如果别人修改了数据则放弃操作,否则执行操作
悲观锁:在操作数据时非常悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才释放锁;上锁期间其他人不能修改数据。
实现方式
- 取出记录时,获取当前 version
- 更新时,带上这个 version
- 执行更新时,set version = newVersion where version = oldVersion
- 如果 version 不对,就更新失败
解释:
# 线程 A
update user set name = 'zhaoliu',version = version+1 where id = 1 and version = 1;
# 线程 B
update user set name = 'tianqi',version = version+1 where id = 1 and version = 1;
如果线程 A 在修改此条数据的时候,线程 B 突然进来了把数据给修改了,并把 version 的值加了1,此时线程 A 再拿着 version=1 去更新此数据就不会成功。这就是乐观锁
实现
实体类:记得在属性上添加 @Version 注解
//乐观锁
@Version
private Integer version;
数据库添加一个 version 字段:
ALTER TABLE user ADD version DEFAULT 1;
创建一个 MybaitsPlus 的配置类:
/**
* MybatisPlus 的配置类
*/
@Configuration//声明此类为一个配置类
public class MybatisPlusConfig {
/**
* 设置拦截器
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//乐观锁的拦截器
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}
然后测试:
代码:
/**
* 测试乐观锁
*/
public void testOptimisticLock(){
User user1 = userDao.selectById(1529443905827876866L);
user1.setEmail("tianqi@qq.com");
User user2 = userDao.selectById(1529443905827876866L);
user2.setName("zhaoliu");
userDao.updateById(user2);
userDao.updateById(user1);
}
结果:发现 user2 被执行成功,而 user1 没有
原因:当第一次拿着 version 去更新时,version=5,更新完毕。然后第二条 SQL 语句还拿着 5 去更新,此时数据库中 version=6 ,所以就更新不成功。
==> Preparing: UPDATE user SET name=?, age=?, email=?, gmt_create=?, gmt_modified=?, version=? WHERE id=? AND version=?
==> Parameters: zhaoliu(String), 18(Integer), zhaoliu@qq.com(String), 2022-05-25 21:18:42.0(Timestamp), 2022-05-26 01:34:45.0(Timestamp), 6(Integer), 1529443905827876866(Long), 5(Integer)
<== Updates: 1
==> Preparing: UPDATE user SET name=?, age=?, email=?, gmt_create=?, gmt_modified=?, version=? WHERE id=? AND version=?
==> Parameters: zhaoliu(String), 18(Integer), tianqi@qq.com(String), 2022-05-25 21:18:42.0(Timestamp), 2022-05-26 01:34:45.0(Timestamp), 6(Integer), 1529443905827876866(Long), 5(Integer)
分页查询
在 MybatisPlusConfig 中的 mybatisPlusInterceptor 方法中增加几行代码即可:
//分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
//可以不写
paginationInnerInterceptor.setDbType(DbType.MYSQL);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
测试:
代码:
/**
* 测试分页
*/
@Test
public void testPage() {
//参数1:为第几页 参数2:一页显示多少条
Page<User> page = new Page<>(2, 5);
//调用mp分页查询的方法
//在调用mp分页查询过程中,底层将分页所有数据封装到page对象里面
page = userDao.selectPage(page, null);
System.out.println("page = " + page);
page.getRecords().forEach(System.out::println);
}
结果:
可以设置不查询总条数: Page<User> page = new Page(2,5,false); 加一个 false 的参数即可
但是一般都会查询总条数,都会有这个需求。
# 先查询出总条数
==> Preparing: SELECT COUNT(1) FROM user
==> Parameters:
<== Columns: COUNT(1)
<== Row: 10
<== Total: 1
# 再分页查询数据
==> Preparing: SELECT id,name,age,email,gmt_create,gmt_modified,version FROM user LIMIT ?,?
==> Parameters: 5(Long), 5(Long)
<== Columns: id, name, age, email, gmt_create, gmt_modified, version
<== Row: 1529443905827876865, zhangsan, 19, zhangsan@qq.com, 2022-05-25 21:18:42.0, 2022-05-26 01:31:02.0, 3
<== Row: 1529443905827876866, zhaoliu, 18, zhaoliu@qq.com, 2022-05-25 21:18:42.0, 2022-05-26 01:34:45.0, 8
<== Row: 1529443905827876867, lisi, 18, lisi@qq.com, 2022-05-25 21:18:42.0, 2022-05-25 21:32:05.0, 1
<== Row: 1529443905827876868, wangwu, 18, wangwu@qq.com, 2022-05-25 21:18:42.0, 2022-05-25 21:18:40.0, 1
<== Row: 1529446296992210947, zhangsan, 18, zhangsan@qq.com, 2022-05-25 23:13:38.0, 2022-05-25 23:13:38.0, 1
<== Total: 5