一.快速入门
(一)简介
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
(二)快速入门
1.准备数据库脚本
2.准备boot工程
(1)创建空项目
(2)创建maven模块
3.导入依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
</parent>
<groupId>com.yan</groupId>
<artifactId>mybatis-plus-base-quick-01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<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>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 数据库相关配置启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid启动器的依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.18</version>
</dependency>
<!-- 驱动类-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
</dependencies>
<!-- SpringBoot应用打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.配置文件和启动类
完善连接池配置:
通过源码分析,druid-spring-boot-3-starter目前最新版本是1.2.18,虽然适配了SpringBoot3,但缺少自动装配的配置文件,需要手动在resources目录下创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,文件内容如下!
com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure
连接池配置
resources/application.yml
# 连接池配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql:///day01
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
启动类
@MapperScan("com.xx.mapper")
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}
5.编写实体类和Mapper接口
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
继承mybatis-plus提供的基础Mapper接口,自带crud方法!
public interface UserMapper extends BaseMapper<User> {
//定义方法
}
6.编写测试类
@SpringBootTest
public class SpringBootMybatisPlusTest {
@Autowired
private UserMapper userMapper;
@Test
public void test() {
List<User> list = userMapper.selectList(null);
System.out.println("users:" + list);
}
}
二.核心功能
(一)基于Mapper接口的crud
通用 CRUD 封装BaseMapper接口,Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器! 内部包含常见的单表操作!
Insert方法
// 插入一条记录
// T 就是要插入的实体对象
// 默认主键生成策略为雪花算法(后面讲解)
int insert(T entity);
Delete方法
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
Update方法
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity,
@Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改 主键属性必须值
int updateById(@Param(Constants.ENTITY) T entity);
实体类属性值为Null则不会修改,所以,实体类的属性必须是包装类型,如果不是,例如int类型,会赋初值为0,会将数据库的年龄修改为0
whereWrapper为null,表示全部修改
Select方法
// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
(二)自定义和多表映射
mybatis-plus: # mybatis-plus的配置
# 默认位置 private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};
mapper-locations: classpath:/mapper/*.xml
public interface UserMapper extends BaseMapper<User> {
//正常自定义方法!
//可以使用注解@Select或者mapper.xml实现
List<User> queryAll();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace = 接口的全限定符 -->
<mapper namespace="com.xxx.mapper.UserMapper">
<select id="queryAll" resultType="user" >
select * from user
</select>
</mapper>
(三)基于Service接口CRUD
1.接口继承IServce接口
public interface UserService extends IService<User> {
}
2.类继承ServiceImpl实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
}
3.测试
@SpringBootTest
public class SpringBootMybatisPlusTest {
@Autowired
private UserService userService;
@Test
public void test_save() {
ArrayList<User> lists = new ArrayList<>();
User user = new User(null, "张三三", 32, "22445432@qq.com");
User user2 = new User(null, "李四四", 32, "3423423@qq.com");
lists.add(user);
lists.add(user2);
userService.saveBatch(lists);
}
@Test
public void test_saveOrUpdate() {
User user = new User(null, "王五", 23, "234234@qq.com");
//id有值则为修改,否则为更新
boolean b = userService.saveOrUpdate(user);
}
@Test
public void test_getOrList() {
User byId = userService.getById(1);//返回单个对象
List<User> list = userService.list(null);//查询全部
}
}
(四)分页查询实现
1.导入分页插件
@SpringBootApplication
@MapperScan("com.yan.mapper")
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
@Bean
public MybatisPlusInterceptor plusInterceptor() {
//mybaits-plus的插件集合[加入到这个集合中即可,分页插件.....]
MybatisPlusInterceptor mybatisPlusInterceptor
= new MybatisPlusInterceptor();
//分页插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
2.非自定义的mapper方法使用分页
@SpringBootTest
public class SpringBootMybatisPlusTest {
@Autowired
private UserMapper userMapper;
@Test
public void testPage(){
Page<User> page = new Page<>(1,3);
userMapper.selectPage(page,null);
//结果 page最后也会被封装结果
long current = page.getCurrent();
long size = page.getSize();
List<User> records = page.getRecords();
long total = page.getTotal();
}
}
3.自定义的mapper方法使用分页
(1)定义接口
传入参数携带Ipage接口
返回结果为IPage
public interface UserMapper extends BaseMapper<User> {
//定义一个根据年龄参数查询,并且分页的方法
IPage<User> queryByAge(IPage<User> page,@Param("age") Integer age);
}
(2)接口实现
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yan.mapper.UserMapper">
<select id="queryByAge" resultType="com.yan.pojo.User">
select *
from user
where age > #{age}
</select>
</mapper>
(3)测试
@Test
public void testPage2(){
Page<User> page = new Page<>(1,3);
IPage<User> userIPage = userMapper.queryByAge(page, 4);
}
(五)条件构造器的使用
1.基于QueryWrapper组装条件
(1)案例
查询用户名包含a,年龄在20--30之间,邮箱不为Null的,按年龄降序查询用户,如果年龄相同则按id升序排列
@Test
public void test_01() {
//查询用户名包含a,年龄在20--30之间,邮箱不为Null的,按年龄降序查询用户,如果年龄相同则按id升序排列
QueryWrapper<User> queryWrapper
= new QueryWrapper<>();
queryWrapper.like("name", "a")
.between("age", 20, 30)
.isNotNull("email")
.orderByDesc("age")
.orderByAsc("id");
userMapper.selectList(queryWrapper);
}
删除email为空的用户
@Test
public void test2() {
//删除邮箱为空的
QueryWrapper<User> queryWrapper
= new QueryWrapper<>();
queryWrapper.isNull("email");
int delete = userMapper.delete(queryWrapper);
}
将年龄大于20并且用户名中包含有a或邮箱为null的用户信息修改
@Test
public void test3() {
QueryWrapper<User> queryWrapper
= new QueryWrapper<>();
queryWrapper.gt("age", 20)
.like("name", "a")
.or()
.isNull("email");
User user = new User(null, "test", 88, "fdfsdf");
userMapper.update(user, queryWrapper);
}
(2)组装条件
(3)指定列映射
要给User对象设置无参构造器!
@Test
public void test4() {
QueryWrapper<User> queryWrapper
= new QueryWrapper<>();
queryWrapper.gt("id", 1)
.select("name", "age");
List<User> users = userMapper.selectList(queryWrapper);
System.out.println(users);
}
(4)condition判断组织条件
public Children eq(boolean condition, R column, Object val) { return this.addCondition(condition, column, SqlKeyword.EQ, val); }
@Test
public void test6() {
//前端传入两个参数
//name不为空,作为条件查询
//age>18 作为条件,查询=
String name = "";
Integer age = 19;
QueryWrapper<User> objectQueryWrapper = new QueryWrapper<>();
objectQueryWrapper.eq(StringUtils.isNotBlank(name), "name",name)
.eq(age != null && age > 18, "age", age);
userMapper.selectList(objectQueryWrapper);
}
2.UpdateWrapper组装条件
使用updateWrapper可以随意设置列的值!!
使用queryWrapper + 实体类形式可以实现修改,但是无法将列值修改为null值!
将年龄大于20并且用户名中包含有a或邮箱为null的用户信息修改
@SpringBootTest
public class MybatisPlusUpdateWrapper {
@Autowired
private UserMapper userMapper;
@Test
public void test1() {
UpdateWrapper<User> objectUpdateWrapper = new UpdateWrapper<>();
objectUpdateWrapper.gt("age", 20)
.like("name", "a")
.or().isNotNull("email")
.set("email", null)
.set("age", 90);
userMapper.update(null, objectUpdateWrapper);
}
}
3.LambdaQueryWrapper组装条件
Java 8 支持以下几种方法引用的形式:
1.静态方法引用: 引用静态方法,语法为 `类名::静态方法名`。
2.实例方法引用: 引用实例方法,语法为 `实例对象::实例方法名`。
3.对象方法引用:引用特定对象的实例方法,语法为 `类名::实例方法名`。
4.构造函数引用: 引用构造函数,语法为 `类名::new`。
相比于 QueryWrapper,LambdaQueryWrapper 使用了实体类的属性引用(例如User::getName、User::getAge),而不是字符串来表示字段名,这提高了代码的可读性和可维护性
@SpringBootTest
public class LambdaQueryWrapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void test1() {
//将年龄在20到100,并且用户名中包含a或者邮箱不为Null 的用户信息修改
LambdaQueryWrapper<User> objectUpdateWrapper = new LambdaQueryWrapper<>();
objectUpdateWrapper.between(User::getAge, 20,100)
.like(User::getName, "a")
.or().isNotNull(User::getEmail);
User user = new User(null, "test1", 98, "eefe");
userMapper.update(user, objectUpdateWrapper);
}
}
4.LambdaUpdateWrapper
@SpringBootTest
public class LambdaQueryWrapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void test1() {
//将年龄在20到100,并且用户名中包含a或者邮箱不为Null 的用户信息修改
LambdaUpdateWrapper<User> objectUpdateWrapper = new LambdaUpdateWrapper<>();
objectUpdateWrapper.between(User::getAge, 20,100)
.like(User::getName, "a")
.or().isNotNull(User::getEmail)
.set(User::getEmail, null)
.set(User::getAge, 91);
userMapper.update(null, objectUpdateWrapper);
}
}
5.核心注解使用
(1)@TableName注解
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
特殊情况:如果表名和实体类名相同(忽略大小写)可以省略该注解!
其他解决方案:全局设置前缀
mybatis-plus: # mybatis-plus的配置
global-config:
db-config:
table-prefix: sys_ # 表名前缀字符串
(2)@TableId 注解
- 描述:主键注解
- 使用位置:实体类主键字段
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 主键字段名 |
type | Enum | 否 | IdType.NONE | 指定主键类型 |
IdType属性可选值:
值 | 描述 |
---|---|
AUTO | 数据库 ID 自增 (mysql配置主键自增长) |
ASSIGN_ID(默认) | 分配 ID(主键类型为 Number(Long )或 String)(since 3.3.0),使用接口IdentifierGenerator 的方法nextId (默认实现类为DefaultIdentifierGenerator 雪花算法) |
@TableName("sys_user")
public class User {
@TableId(value="主键列名",type=主键策略)
private Long id;
private String name;
private Integer age;
private String email;
}
mybatis-plus:
configuration:
# 配置MyBatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 配置MyBatis-Plus操作表的默认前缀
table-prefix: t_
# 配置MyBatis-Plus的主键策略
id-type: auto
在以下场景下,添加`@TableId`注解是必要的:
1. 实体类的字段与数据库表的主键字段不同名:如果实体类中的字段与数据库表的主键字段不一致,需要使用`@TableId`注解来指定实体类中表示主键的字段。
2. 主键生成策略不是默认策略:如果需要使用除了默认主键生成策略以外的策略,也需要添加`@TableId`注解,并通过`value`属性指定生成策略。
雪花算法(Snowflake Algorithm)是一种用于生成唯一ID的算法。它由Twitter公司提出,用于解决分布式系统中生成全局唯一ID的需求。
在传统的自增ID生成方式中,使用单点数据库生成ID会成为系统的瓶颈,而雪花算法通过在分布式系统中生成唯一ID,避免了单点故障和性能瓶颈的问题。
(3)@TableField
描述:字段注解(非主键)
属性 | 类型 | 必须指定 | 默认值 | 描述 |
value | String | 否 | "" | 数据库字段名 |
exist | boolean | 否 | true | 是否为数据库表字段 |
MyBatis-Plus会自动开启驼峰命名风格映射!!!
@TableName("sys_user")
public class User {
@TableId
private Long id;
@TableField("nickname")
private String name;
private Integer age;
private String email;
}
三.高级拓展
(一)逻辑删除字段
1.概念
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
- 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录
逻辑删除以后,没有真正的删除语句,删除改为修改语句!
2.实现
(1)数据库添加逻辑删除字段
数据库和实体类添加逻辑删除字段
ALTER TABLE USER ADD deleted INT DEFAULT 0 ; # int 类型 1 逻辑删除 0 未逻辑删除
(2)实体类添加逻辑删除属性
@Data
public class User {
// @TableId
private Integer id;
private String name;
private Integer age;
private String email;
@TableLogic
//逻辑删除字段 int mybatis-plus下,默认 逻辑删除值为1 未逻辑删除 1
private Integer deleted;
}
(3) 指定逻辑删除字段和属性值
a.单一指定
@Data
public class User {
// @TableId
private Integer id;
private String name;
private Integer age;
private String email;
@TableLogic
//逻辑删除字段 int mybatis-plus下,默认 逻辑删除值为1 未逻辑删除 1
private Integer deleted;
}
b.全局指定
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)
(4)测试
@Test
public void test_02() {
userMapper.deleteById(1);
}
(二)乐观锁实现
1.思路
乐观锁:
乐观锁的基本思想是,认为并发冲突的概率较低,因此不需要提前加锁,而是在数据更新阶段进行冲突检测和处理。乐观锁的核心思想是"先修改,后校验"。在乐观锁的应用中,线程在读取共享资源时不会加锁,而是记录特定的版本信息。当线程准备更新资源时,会先检查该资源的版本信息是否与之前读取的版本信息一致,如果一致则执行更新操作,否则说明有其他线程修改了该资源,需要进行相应的冲突处理。乐观锁通过避免加锁操作,提高了系统的并发性能和吞吐量,但是在并发冲突较为频繁的情况下,乐观锁会导致较多的冲突处理和重试操作。
悲观锁:
悲观锁的基本思想是,在整个数据访问过程中,将共享资源锁定,以确保其他线程或进程不能同时访问和修改该资源。悲观锁的核心思想是"先保护,再修改"。在悲观锁的应用中,线程在访问共享资源之前会获取到锁,并在整个操作过程中保持锁的状态,阻塞其他线程的访问。只有当前线程完成操作后,才会释放锁,让其他线程继续操作资源。这种锁机制可以确保资源独占性和数据的一致性,但是在高并发环境下,悲观锁的效率相对较低。
2.解决方案
乐观锁
a.版本号/时间戳:为数据添加一个版本号或时间戳字段,每次更新数据时,比较当前版本号或时间戳与期望值是否一致,若一致则更新成功,否则表示数据已被修改,需要进行冲突处理。
b.CAS(Compare-and-Swap):使用原子操作比较当前值与旧值是否一致,若一致则进行更新操作,否则重新尝试。
c.无锁数据结构:采用无锁数据结构,如无锁队列、无锁哈希表等,通过使用原子操作实现并发安全。
悲观锁
a.锁机制:使用传统的锁机制,如互斥锁(Mutex Lock)或读写锁(Read-Write Lock)来保证对共享资源的独占访问。
b.数据库锁:在数据库层面使用行级锁或表级锁来控制并发访问。
c.信号量(Semaphore):使用信号量来限制对资源的并发访问。
3.使用mybatis-plus数据实现乐观锁
(1) 添加版本号更新插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
(2)乐观锁字段添加@Version注解
数据库也需要添加version字段
@Version
private Integer version;
ALTER TABLE USER ADD VERSION INT DEFAULT 1 ; # int 类型 乐观锁字段
(3)正常更新使用即可
@Test
public void testQuick7(){
//步骤1: 先查询,在更新 获取version数据
//同时查询两条,但是version唯一,最后更新的失败
User user = userMapper.selectById(5);
User user1 = userMapper.selectById(5);
user.setAge(20);
user1.setAge(30);
userMapper.updateById(user);
//乐观锁生效,失败!
userMapper.updateById(user1);
}
(三)防止全表更新和删除实现
针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除
添加防止全表更新和删除拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}
当发生全表删除和全表更新时,mybatis会自动阻止