MyBatisPlus
一、导入依赖
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
我们通过在配置文件中添加日志配置,可以看到sql的信息
# 配置日志
mybatis-plus.configuration.logimpl=org.apache.ibatis.logging.stdout.StdOutImpl
二、基本用法
示例表结构:
-
实体类User
@Data @AllArgsConstructor @NoArgsConstructor public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @Version // 乐观锁注解 private Integer version; @TableLogic(value = "0",delval = "1") // 逻辑删除注解 , 0表示未删除,1表示删除, 默认值为0. 这里实际上可以设置,也可以不设置 private Integer deleted; }
-
创建UserMapper
传统的MyBatis的方式来做,我们需要创建Mapper层并且创建对应的Mapper.xml文件,而在适用MyBatisPlus时,我们只需要写一个mapper接口继承plus提供的BaseMapper接口,这个接口包含了大部分我们平时需要用到的CRUD操作。
@Mapper
public interface UserMapper extends BaseMapper<User> { // 这里User是实体类。本文都以User这个实体类为例
// 继承baseMapper,已经有了基本的增删改查方法
}
通过查看BaseMapper的源码,我们可以看出,大部分常用的操作已经被封装
-
开启扫描
为了能够让SpringBoot扫描并且识别此组件,我们需要在SpringBoot启动类上开启Mapper接口 扫描功能,添加@MapperScan()注解
//开启扫描,注意包名不要写错
@MapperScan("com.qf.mybatisplusdemo.mapper")
@SpringBootApplication
public class MyBatisPlusDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MyBatisPlusDemoApplication.class, args);
}
}
-
测试
此时,我们算是已经实现了基本的MyBatisplus的配置,可以进行运用 package com.mybatisplusdemo; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.mybatisplusdemo.Mapper.UserMapper; import com.mybatisplusdemo.entity.User; import com.mybatisplusdemo.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @SpringBootTest class MybatisPlusDemoApplicationTests { @Autowired private UserMapper userMapper; @Autowired private UserService userService; @Test void contextLoads() { // 查询全部用户 // selectList方法参数为null,表示查询全部用户 List<User> users = userMapper.selectList(null); users.forEach(System.out::println); } @Test public void deleteTest(){ // 根据map删除对应的数据 Map<String,Object> map = new HashMap<>(); map.put("name","Jone"); map.put("age",18); int num = userMapper.deleteByMap(map); System.out.println("删除的记录数:"+num); } @Test public void deleteBatchTest(){ // 批量删除 List<Long> ids = new ArrayList<>(); ids.add(2L); ids.add(3L); int num = userMapper.deleteBatchIds(ids); System.out.println("删除的记录数:"+num); } @Test public void updateTest(){ // 根据id去修改指定数据 User user = new User(); user.setId(5l); user.setName("Eason"); int num = userMapper.updateById(user); System.out.println("更新的记录数:"+num); } @Test public void selectTest() { // 根据map中传递的条件进行查询 Map<String, Object> map = new HashMap<>(); map.put("id",4L); map.put("name","Eason"); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); } }
三、主键生成策略
常见的一些主键策略有:UUID, Redis生成ID,雪花算法(snowflake)以及zookeeper。
在MyBatisplus中,主要是通过@TableId注解去设置主键生成策略。@TableId的源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {
// 指定数据库表的主键字段名。如果不设置,MyBatis-Plus 将使用实体类中的字段名作为数据库表的主键字段名。
String value() default "";
// 主键的生成策略。
IdType type() default IdType.NONE;
}
IdType的枚举类型定义:
在实体类中我们需要为主键字段加上这个注解,如果主键字段名为id,则可省略,比如这里我将数据库自增ID作为主键。
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
}
四、自定义CRUD
MyBatis-Plus的BaseMapper本身提供了很多通用的CRUD方法,极大的方便了我们的代码编写,当然,当我们遇到复杂的方法时,我们需要自定义一些方法,这个时候我们仍然要跟之前mybatis一样去实现sql编写。因为:MyBatis-Plus是在MyBatis的基础之上只做增强不做修改
我们需要配置mapper.xml的文件位置
# 指定mapper文件位置
mybatis-plus.mapper-locations = classpath*:/mapper/**/*.xml
但是实际上这个配置有一个默认的配置路径也就是classpath:/mapper/**/.xml。一般我们都是默认这个路径,可以不再配置
至于其他的操作,我们仍然和Mybatis一致即可,这里不多记录。
五、IService接口
IService 是 MyBatis-Plus 提供的一个通用 Service 层接口,它封装了常见的 CRUD 操作,包括插入、删除、查询和分页等。通过继承 IService 接口,可以快速实现对数据库的基本操作,同时保持代码的简洁性和可维护性。
IService 接口中的方法命名遵循了一定的规范,如 get 用于查询单行,remove 用于删除,list 用于查询集合,page 用于分页查询,这样可以避免与 Mapper 层的方法混淆。
其实一般情况下,由于项目业务的复杂程度,我们都会使用自定义Service方法,那么这些如果我 们想即使用通用的IService接口提供的方法,又有自定义的方法的话,我们可以参考IService接口 的实现类ServiceImpl。
IService源码:
可以看到,IService接口中提供了许多默认的方法供我们使用,我们的Service接口只需要继承它即可
// 继承IService接口,已经有了基本的增删改查方法
public interface UserService extends IService<User> {
}
同样的,在实现类中我们仍然需要继承ServiceImpl实现类,我们可以先看一下ServiceImpl实现类的源码:
那么当我们自定义实现类时,可以根绝ServiceImpl的定义方法去写:
这里 M extends BaseMapper<T> 指的是继承了BaseMapper的Mapper接口,
T 指的是一个实体类
所以,我们的UserServiceImpl可以这么写:
//按照ServiceImpl实现类编写自己的业务层实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
//自定义service方法实现
}
测试:
@SpringBootTest
class MyBatisPlusDemoApplicationTests {
@Autowired
private UserService service;
// 利用IService提供的saveBatch方法去进行批量插入
@Test
public void insertBatchTest() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("smlz_" + i);
user.setAge(20 + i);
user.setEmail("1666189" + i + "@qq.com");
users.add(user);
}
boolean flag = userService.saveBatch(users);
System.out.println("批量插入结果:" + flag);
}
}
六、自动填充处理
对于创建时间create_time 和 修改时间update_time,我们可以通过MyBatis-plus进行自动填充处理
自动填充功能通过实现 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
接口来实现。你需要创建一个类来实现这个接口,并在其中定义插入和更新时的填充逻辑。
-
实体类定义
首先我们需要用
@TableField
注解来标记哪些字段需要自动填充,并指定填充的策略.
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
填充策略FieldFill枚举类型主要包括以下几个:
他们的含义分别是: 默认不处理,插入时填充字段、更新时填充字段、插入和更新时都填充字段
-
实现 MetaObjectHandler
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
七、乐观锁与分页插件
乐观锁是一种并发控制机制,用于确保在更新记录时,该记录未被其他事务修改。MyBatis-Plus 提供了 OptimisticLockerInnerInterceptor
插件,使得在应用中实现乐观锁变得简单。
-
基本原理
乐观锁的实现通常包括以下步骤:
-
读取记录时,获取当前的版本号(version)。
-
在更新记录时,将这个版本号一同传递。
-
执行更新操作时,设置
version = newVersion
的条件为version = oldVersion
。 -
如果版本号不匹配,则更新失败。
-
-
配置乐观锁插件
首先我们需要在实体类中为version字段添加乐观锁注解@version
@Version//乐观锁注解
private Integer version;
然后,我们需要注册组件,也就是乐观锁拦截器,参考官网代码,我们可以创建一个MyBatisPlusConfig配置类:
我们可以将所有关于MyBatisPlus的配置放到这个配置类中,比如之前的MapperScan
@Configuration // mybatis-plus配置类, 所有的配置都在这里
@MapperScan("com.mybatisplusdemo.Mapper") // 扫描mapper文件夹
public class mybatisPlusConfig {
/**
* 乐观锁拦截器
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
return interceptor;
}
}
测试:
// 单线程测试
@Test
public void mybatisPlusOptimisticLockerTest(){
// 查询
User user = userMapper.selectById(1L);
// 修改用户信息
user.setName("Eason727");
user.setAge(28);
// 执行更新操作
userMapper.updateById(user);
}
// 多线程测试,模仿高并发场景
@Test
public void mybatisPlusOptimisticLockerTest2(){
// 查询
User user = userMapper.selectById(1L);
// 修改用户信息
user.setName("Eason1111");
user.setAge(28);
// 模拟另外一个线程执行了插队操作, 线程2插队---------------------
User user2 = userMapper.selectById(1L);
user2.setName("Eason2222");
user2.setAge(22);
userMapper.updateById(user2);
// 执行更新操作
userMapper.updateById(user);
}
在单线程测试中,并不会受到影响,而在多线程测试中,由于第一个线程还未结束就开始了第二个线程,这样会导致第一个线程并没有得到执行。
MyBatis-Plus 的分页插件 PaginationInnerInterceptor
提供了强大的分页功能,支持多种数据库,使得分页查询变得简单高效。
测试:
@Test
public void pageTest(){
// 简单分页模型
// current:当前页 size:每页显示的记录数
Page<User> page = new Page<>(2,5);
userMapper.selectPage(page,null);
// 获取记录
List<User> users = page.getRecords();
users.forEach(System.out::println);
// 获取总页数
System.out.println("总页数:"+page.getPages());
// 获取总记录数
System.out.println("总记录数:"+page.getTotal());
// 获取当前页
System.out.println("当前页:"+page.getCurrent());
// 上一页
System.out.println("是否有上一页:"+page.hasPrevious());
// 下一页
System.out.println("是否有下一页:"+page.hasNext());
}
八、逻辑删除
逻辑删除是一种优雅的数据管理策略,它通过在数据库中标记记录为“已删除”而非物理删除,来保留数据的历史痕迹,同时确保查询结果的整洁性。MyBatis-Plus 提供了便捷的逻辑删除支持,使得这一策略的实施变得简单高效。
逻辑删除的工作原理
MyBatis-Plus 的逻辑删除功能会在执行数据库操作时自动处理逻辑删除字段。以下是它的工作方式:
-
插入:逻辑删除字段的值不受限制。
-
查找:自动添加条件,过滤掉标记为已删除的记录。
-
更新:防止更新已删除的记录。
-
删除:将删除操作转换为更新操作,标记记录为已删除。
逻辑删除字段支持所有数据类型,但推荐使用 Integer
、Boolean
或 LocalDateTime
。如果使用 datetime
类型,可以配置逻辑未删除值为 null
,已删除值可以使用函数如 now()
来获取当前时间。
使用方法:
步骤 1: 配置全局逻辑删除属性
在 application.yml
中配置 MyBatis-Plus 的全局逻辑删除属性:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
步骤 2: 在实体类中使用 @TableLogic
注解
@TableLogic
private Integer deleted;
我们也可以直接在实体类中设置,不用配置yaml文件
// 逻辑删除注解 , 0表示未删除,1表示删除, 默认值为0. 这里实际上可以设置,也可以不设置
@TableLogic(value = "0",delval = "1")
private Integer deleted;
九、条件构造器
MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。
AbstractWrapper:这是一个抽象基类,提供了所有 Wrapper 类共有的方法和属性。此外,QueryWrapper、UpdateWrapper、LambdaQueryWrapper、LambdaUpdateWrapper.这个四个类分别实现了关于查询、更新条件的封装以及对应的具有Lambda语法的查询、更新条件封装。
一些基本用法:
@SpringBootTest
public class WrapperTest {
@Autowired
private UserMapper userMapper;
@Test
void selectTest1(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 查询条件: name不为空,age大于20,email不为空
wrapper.isNotNull("name")
.ge("age",20)
.isNotNull("email");
List<User> users = userMapper.selectList(wrapper);// 根据条件查询用户
users.forEach(System.out::println);
}
// 查询名字为eason的用户
@Test
void selectTest2(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 查询条件: name为Eason
wrapper.eq("name","Eason");
User user = userMapper.selectOne(wrapper); // selectOne方法返回查询得到的一条数据
System.out.println(user);
}
// 查询年龄在20-30之间的用户
@Test
void selectTest3(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 查询条件: age在20-25之间
wrapper.between("age",20,25);
userMapper.selectList(wrapper).forEach(System.out::println);
}
// 模糊查询, 查询名字中不包含e的用户. like就是包含,notLike就是不包含
@Test
void selectTest4(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 查询条件: name不包含e
wrapper.notLike("name","e");
userMapper.selectList(wrapper).forEach(System.out::println);
}
// 模糊查询,包含左侧,或者右侧
@Test
void selectTest5(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 查询条件: name的右侧包含e
wrapper.likeRight("name","e");
userMapper.selectList(wrapper).forEach(System.out::println);
}
// 查询用户名中包含e,年龄大于20或者邮箱为null的用户
@Test
void selectTest6(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 查询条件: name包含e,年龄大于20或者邮箱为null
wrapper.like("name","e")
.and(wq->wq.ge("age",20).or().isNull("email"));
userMapper.selectList(wrapper).forEach(System.out::println);
}
// 模糊查询, 查询名字中不包含e的用户. like就是包含,notLike就是不包含
// 这里尝试了添加Condition条件
//我们在写项目的时候,所有的条件都是由用户进行传递的,那么有的时候就无法避免参数出现空
//值null的情况,所以我们应该要做一些判断,其实很多方法都提供了boolean condition这个参
// 数,表示该条件是否加入最后生成的sql中,也就是可以通过它来进行判断
@Test
void selectTest7(){
// 假设用户传递了参数
String name = "e";
Integer age = null;
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 如果name不为空,就根据name模糊查询
wrapper.like(StringUtils.isNotBlank(name),"name",name)
.orderByAsc(age!=null,"age"); // 如果age不为空,就根据age升序排列
userMapper.selectList(wrapper).forEach(System.out::println);
}
// -------------------------QueryWrapper执行修改和删除操作----------------------------------
// 修改用户信息
@Test
void updateTest1(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("id",6L);
User user = new User();
user.setName("ChanEx");
// 根据条件更新用户信息
userMapper.update(user,wrapper);
}
// 删除用户信息
@Test
void deleteTest1(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("id",6L);
// 根据条件删除用户信息
userMapper.delete(wrapper);
}
//-------------------------------UpdateWrapper--------------------------------------------
// 修改年龄大于26,且name为theshy的用户邮箱为19999@163.com
@Test
public void updateTest2() {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.gt("age",26)
.eq("name","theshy")
.set("email","19999@163.com");
userMapper.update(null,wrapper); // 第一个参数为null,表示更新所有符合条件的记录
}
// -------------------------------LambdaQueryWrapper&LambdaUpdateWrapper-----------------------
//它们两个的主要目的是为了防止我们在编写的时候,字段名称编写错误,我们可以直接通过
//Lambda的方式来直接获取指定字段对应的实体类对应的名称
// 模糊查询, 查询名字中不包含e的用户. like就是包含,notLike就是不包含
@Test
void selectTest9(){
// 假设用户传递了参数
String name = "e";
Integer age = null;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name),User::getName,name)
.orderByAsc(age!=null,User::getAge); // 如果age不为空,就根据age升序排列
userMapper.selectList(wrapper).forEach(System.out::println);
}
@Test
void updateTest3(){
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.gt(User::getAge,26)
.eq(User::getName,"theshy")
.set(User::getEmail,"10086@gmali.como");
userMapper.update(null,wrapper);
}
// 通过子查询,查询id等于6的用户信息
@Test
void selectTest10(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// inSql方法可以传入一个子查询, 可以用于表关联查询
wrapper.inSql("id","select id from user where id = 7");
userMapper.selectObjs(wrapper).forEach(System.out::println);
}
}