MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
任何能使用
MyBatis
进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AAYqYRh2-1653734436948)(D:\Typora Data\MybatisPlus.assets\1653706687938.png)]
我们创建一个mybatisplus的数据库,并插入一张user表
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, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');
在真实开发中,还需要添加version(乐观锁)、deleted(逻辑删除)、gmt_create、gmt_modified
我们新建一个springboot基本项目即可,添加上我们的web依赖、和lombok插件以及mysql依赖。
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
3.4.2
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
说明:使用mybatis-plus可以节省我们大量的代码,尽量不要同时导入mybatis和mybatis-plus!因为会有版本差异
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
#高版本兼容低版本,所以使用8的驱动即可
driver-class-name: com.mysql.cj.jdbc.Driver
#url,如果你是mysql8需要添加时区设置serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
#数据库名字
username: root
#数据库密码
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 在application.yml中配置日志输出
global-config:
db-config:
id-type: auto
# table-prefix: t_user
这一步跟mybatis操作相同,mysql5 和mysql8驱动有所不同,8需要增加时区的配置
在IDEA中连接数据库就不做过多介绍了,在我博客关于mybatis中有详细介绍,当然,网上也有很多资料。
User
package com.cjh.mybatis_plus.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
UserMapper,在mybatis中,你还需要写相应的xml,但是在MyabtisPlus中,你完全不需要,你只需要继承一个接口BaseMapper即可,至此,所有的CRUD代码已经完成。
package com.cjh.mybatis_plus.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
//在对应的mapper上面实现基本的接口BaseMapper
@Repository //代表持久层
public interface UserMapper extends BaseMapper{
}
注意:这里要添加User的泛型!
package com.cjh.mybatis_plus;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.cjh.mybatis_plus.mapper")
public class MybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}
}
注意:扫描路径不要写错!
注意:我们在mapper中,没有写任何的CRUD代码
package com.cjh.mybatis_plus;
import com.cjh.mybatis_plus.mapper.UserMapper;
import com.cjh.mybatis_plus.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class MybatisPlusApplicationTests {
//继承了basemapper,所有的方法都来自与父类,我们也可以添加我们自定义的方法
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
//参数是一个wrapper,条件构造器,这里我们先不用条件,就用null,即查询所有的用户
//查询所有的用户
List user = userMapper.selectList(null);
user.forEach(System.out::println);
}
}
结果:
User(id=1, name=Jone, age=18, [email protected])
User(id=2, name=Jack, age=20, [email protected])
User(id=3, name=Tom, age=28, [email protected])
User(id=4, name=Sandy, age=21, [email protected])
User(id=5, name=Billie, age=24, [email protected])
2022-05-28 11:48:46.745 INFO 3600 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-05-28 11:48:46.755 INFO 3600 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
全部都查出来的,我们没有编写任何的CRUD代码
https://www.shuzhiduo.com/A/kmzL4NWKzG/
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.baomidou.mybatisplus.core.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> queryWrapper);
int deleteBatchIds(@Param("coll") Collection<?> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
List<T> ts = this.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(ts)) {
if (ts.size() != 1) {
throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records", new Object[0]);
} else {
return ts.get(0);
}
} else {
return null;
}
}
default boolean exists(Wrapper<T> queryWrapper) {
Long count = this.selectCount(queryWrapper);
return null != count && count > 0L;
}
Long selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);
}
我们所有的sql现在是不可见的,我们希望知道它是怎么执行的,所以我们必须要看日志
我们在application.yml中配置日志输出
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
//测试插入
@Test
public void testInsert(){
User user = new User();
user.setName("菜菜");
user.setAge(3);
user.setEmail("[email protected]");
int result = userMapper.insert(user);//帮我们自动生成id
System.out.println(result);
}
我们发现,它自动帮我们生成id了 1530399347068370946(Long)
Parameters: 1530399347068370946(Long), 菜菜(String), 3(Integer), 111111@qq.com(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@551e4c6d]
1
2022-05-28 12:03:32.910 INFO 13464 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-05-28 12:03:32.926 INFO 13464 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
Process finished with exit code 0
数据库插入的id的默认值为:全局的唯一id,我们需要看它的主键生成策略
默认ID_WORKED全局唯一id
对应数据库中的主键:uuid、自增id、雪花算法、redis、zookeeper
而Mybatisplus就是用了雪花算法
分布式系统唯一id生成:
https://blog.csdn.net/qq_37469055/article/details/118061067
雪花算法:
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。
其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。
这个算法单机每秒内理论上最多可以生成1000*(2^12),也就是409.6万个ID。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FstiFPtG-1653734436950)(D:\Typora Data\MybatisPlus.assets\1653711028311.png)]
雪花算法描述:
最高位是符号位,始终为0,不可用。
41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。
10位的机器标识,10位的长度最多支持部署1024个节点。10位器标识符一般是5位IDC+5位machine编号,唯一确定一台机器。
12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。
几乎可以保证全球唯一
我们需要配置主键自增:
1、实体类字段加上@TableId(type = IdType.AUTO)
2、数据库字段一定要是自增!
@Test
public void testUpdata(){
User user = new User();
user.setId(6L);
user.setName("洋洋子");
int i = userMapper.updateById(user);
System.out.println(i);
}
注意:updateById参数是一个对象!
并且,它可以通过自动拼接sql自动更新
创建时间、修改时间,这些操作一般都是自动化完成,我们不希望手动更新,阿里巴巴有明确的规定,所有的数据库表:gmt_create,gmt_modified,几乎所有的表都要配置上!而且需要自动化
方式一:数据库级别(工作中不允许你修改数据库)
在表中新增字段gmt_create,gmt_modified
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JtH1dyD6-1653734436951)(D:\Typora Data\MybatisPlus.assets\1653712551477.png)]
实体类添加字段
private Date gmt_create;
private Date gmt_modified;
方式二:代码级别
https://baomidou.com/pages/4c6bcf/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vdec3cmA-1653734436952)(D:\Typora Data\MybatisPlus.assets\1653713747522.png)]
我们恢复原始设置
//字段添加填充内容
@TableField(fill = FieldFill.INSERT)
private Date gmt_create;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmt_modified;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tzWztwmF-1653734436953)(D:\Typora Data\MybatisPlus.assets\1653721288294.png)]
MyMetaObjectHandler
package com.cjh.mybatis_plus.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Date;
@Slf4j
@Component //把它放入处理容器中去
public class MyMetaObjectHandler implements MetaObjectHandler {
//插入式时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.setFieldValByName("gmt_create",new Date(),metaObject);
this.setFieldValByName("gmt_modified",new Date(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.setFieldValByName("gmt_modified",new Date(),metaObject);
}
}
由于我们的myabtisplus是可以不用编写mapper以及xml的,所以配置文件也不需要相应的配置
所以,很大可能是由于依赖冲突了,记住,mybatis和mybatisplus的依赖不能同时导入,否则会出现版本问题。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version></dependency>
也不要使用最高版本,最高版本会有问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQG8YwBx-1653734436954)(D:\Typora Data\MybatisPlus.assets\1653720238091.png)]
这是因为我们的数据库表中的id没有设置自动自增导致的。
在面试的过程中,我们经常会被问到乐观锁,对应的还有悲观锁,这个其实非常简单!
version、new version
乐观锁实现方式:
乐观锁:先查询,获取版本号version =1,更新的时候version+1再附带进去
什么意思呢?
好比有两个人,同时执行更新操作,A比B更快完成了这个操作,那么这个时候B的version就不是1了,因为A先执行了这个操作,version=2≠1,所以B更新失败了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uG1Jv9zn-1653734436954)(D:\Typora Data\MybatisPlus.assets\1653720801742.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yYe2gSWH-1653734436955)(D:\Typora Data\MybatisPlus.assets\1653720934642.png)]
@Version
private Integer version;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D9gJwyax-1653734436956)(D:\Typora Data\MybatisPlus.assets\1653721267627.png)]
MyBatisPlusConfig
package com.cjh.mybatis_plus.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//代表这是一个配置类
@Configuration
//自动管理事务
@EnableTransactionManagement
@MapperScan("com.cjh.mybatis_plus.mapper")
public class MyBatisPlusConfig {
//注册乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
//测试乐观锁成功
@Test
public void testOptionmisticLocker(){
//查询用户信息
User user = userMapper.selectById(1L);
//修改用户信息
user.setName("cjh");
user.setEmail("[email protected]");
//执行更新操作
userMapper.updateById(user);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZIbpBev-1653734436956)(D:\Typora Data\MybatisPlus.assets\1653721609779.png)]
JDBC Connection [HikariProxyConnection@1698746141 wrapping com.mysql.cj.jdbc.ConnectionImpl@ec8f4b9] will not be managed by Spring
==> Preparing: SELECT id,name,age,email,gmt_create,gmt_modified,version FROM user WHERE id=?
==> Parameters: 1(Long)
<== Columns: id, name, age, email, gmt_create, gmt_modified, version
<== Row: 1, Jone, 18, test1@baomidou.com, 0000-00-00 00:00:00, 2022-05-28 12:33:29, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@13da7ab0]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58fef7f7] was not registered for synchronization because synchronization is not active
2022-05-28 15:05:26.446 INFO 27992 --- [ main] c.c.m.handler.MyMetaObjectHandler : start update fill ....
JDBC Connection [HikariProxyConnection@835070183 wrapping com.mysql.cj.jdbc.ConnectionImpl@ec8f4b9] will not be managed by Spring
==> Preparing: UPDATE user SET name=?, age=?, email=?, gmt_modified=?, version=? WHERE id=? AND version=?
==> Parameters: cjh(String), 18(Integer), 22222222@qq.com(String), 2022-05-28 15:05:26.446(Timestamp), 2(Integer), 1(Long), 1(Integer)
<== Updates: 1
我们发现,它首先是先查询了信息包括version信息,然后要执行更新操作的时候,把version+1一同更新上去了。
现在,我们执行失败的情况:
//测试乐观锁失败,多线程情况下
@Test
public void testOptionmisticLocker2(){
//线程 1
//查询用户信息
User user1 = userMapper.selectById(1L);
//修改用户信息
user1.setName("cjh111");
user1.setEmail("[email protected]");
//模拟另外一个线程执行了插队操作
//执行更新操作
User user2 = userMapper.selectById(1L);
//修改用户信息
user2.setName("cjh222");
user2.setEmail("[email protected]");
userMapper.updateById(user2);
userMapper.updateById(user1);//如果没有乐观锁就会覆盖插队线程的值!
}
我们发现user1没有成功,我们也可以使用自旋锁赖重复提交,多线程一定要加锁!
//测试查询
@Test
public void testSelectById(){
User user = userMapper.selectById(1L);
System.out.println(user);
}
//测试批量查询
@Test
public void testSelectByBatchId(){
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}
//多条件查询之一map
@Test
public void testSelectByBatchIds(){
HashMap<String, Object> map = new HashMap<>();
//自定义要查询
map.put("name","菜菜");
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}
1、原始的limit进行分页
2、pageHelper第三方插件
3、MP内置分页插件
https://baomidou.com/pages/97710a/#%E5%B1%9E%E6%80%A7%E4%BB%8B%E7%BB%8D
//分页插件
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor(){
return new PaginationInnerInterceptor();
}
@Test
public void testPage(){
//查询第一页,并且当前5个数据
Page<User> page = new Page<>(1,5);
userMapper.selectPage(page,null);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vYaf20bu-1653734436958)(D:\Typora Data\MybatisPlus.assets\1653723520187.png)]
删除操作跟查询的操作是一样的,这里就不讲了
我们这里讲讲逻辑删除:
物理删除:从数据库中直接移除
逻辑删除:没有从数据库中移除,而是通过一个变量让他失效!(你以为你注销了就没有你的信息了吗,不,只是你看不到了而已)
逻辑删除是为了防止数据的丢失,类似于回收站!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dcyiFoq-1653734436959)(D:\Typora Data\MybatisPlus.assets\1653723974562.png)]
默认0代表没有被删除,1代表被删除
@TableLogic //逻辑删除注解
private Integer deleted;
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)这一句的意思就是扫描全局的逻辑删除的实体字段名,我们上面用的是 deleted,所以我们可以把flag改成deleted,这样就不用写实体类了。
@Test
public void testDeleteById(){ userMapper.deleteById(1L);}
你以为删除了吗,其实只是执行了更新操作,记录依旧在数据库中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fk8eIg4i-1653734436960)(D:\Typora Data\MybatisPlus.assets\1653724470010.png)]
但是查询的时候,会自动过滤被逻辑删除的对象,大大减少开发量。
十分重要,我们写一些复制的sql可以使用它来替代!
AbstractWrapper
allEq
eq
ne
gt
ge
lt
le
between
notBetween
like
notLike
likeLeft
likeRight
isNull
isNotNull
in
notIn
inSql
notInSql
groupBy
orderByAsc
orderByDesc
orderBy
having
func
or
and
nested
apply
last
exists
notExists
QueryWrapper
select
UpdateWrapper
set
setSql
lambda
使用 Wrapper 自定义SQL
kotlin持久化对象定义最佳实践
用注解
用XML
kotlin使用wrapper
链式调用 lambda 式
@Test
void contextLoads() {
//查询name不为空的用户,并且邮箱不为空,年龄大于等于12岁
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age", 18);
userMapper.selectList(wrapper).forEach(System.out::println);
}
其他的操作都根据官方文档来就行
MybatisX快速开发插件
#updatewrapper)
set
setSql
lambda
使用 Wrapper 自定义SQL
kotlin持久化对象定义最佳实践
用注解
用XML
kotlin使用wrapper
链式调用 lambda 式
@Test
void contextLoads() {
//查询name不为空的用户,并且邮箱不为空,年龄大于等于12岁
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age", 18);
userMapper.selectList(wrapper).forEach(System.out::println);
}
其他的操作都根据官方文档来就行
MybatisX快速开发插件