MybatisPlus教程-从入门到进阶

embedded/2025/2/22 21:43:33/

前言

首先它是国产的,所以直接用官网的简介。

  1. 简介

MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
在这里插入图片描述

  1. 特性
  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作
  1. 支持数据库

在这里插入图片描述

  1. 框架结构

在这里插入图片描述

好了,上面的都来自于官网,我们要做的就是把官网中各种零散的教程,整合成一个个通俗易懂的案例。

使用步骤

拢共就两步

  1. 引入MybatisPlus依赖,代替Mybatis依赖
 <!-- mybatis依赖直接移除 --><!--        <dependency>--><!--            <groupId>org.mybatis.spring.boot</groupId>--><!--            <artifactId>mybatis-spring-boot-starter</artifactId>--><!--            <version>3.x.xx</version>--><!--        </dependency>--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.10.1</version></dependency>
  1. 定义Mapper接口并继承BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {}

快速入门-入门案例

需求:基于课前资料提供的项目,实现下列功能:

  • 新增用户功能
  • 根据id查询用户
  • 根据id批量查询用户
  • 根据id更新用户
  • 根据id删除用户
入门准备
  1. 创建一个springboot项目

详情参考SpringBootWeb快速入门。

  1. 创建mysql用户表
create table user
(id          bigint primary key auto_increment,username    varchar(50),password    varchar(50),phone       varchar(50),status      int default 1,balance     int,info        varchar(255),create_time datetime,insert_time datetime
)
  1. 创建对应的实体类
package com.example.javawebdemo.entity;import lombok.Data;import java.time.LocalDateTime;@Data
public class User {private Long id;private String username;private String password;private String phone;private String info;private Integer status;private Integer balance;private LocalDateTime createTime;private LocalDateTime insertTime;
}
  1. 创建数据库配置文件
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: rooturl: jdbc:mysql://localhost:3306/demo

为了可以在控制台日志中看到打印SQL语句,其实我们可以设置的,在application.yml中添加

mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mybatisplus_142">套用mybatis-plus上面两个步骤
  1. 引入依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.10.1</version>
</dependency>
  1. 定义Mapper接口并继承BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {}

BaseMapper定义了大量的增删改查方法,我们继承它就实现了 user 表的 CRUD 功能,甚至连 XML 文件都不用编写!

这里还需要在启动类上面加一个@MapperScan注解,来标注mapper包的位置

package com.example.javawebdemo;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.example.javawebdemo.mapper")
@SpringBootApplication
public class JavawebDemoApplication {public static void main(String[] args) {SpringApplication.run(JavawebDemoApplication.class, args);}
}
编写测试类实现入门案例
  1. 新增用户
package com.example.javawebdemo;import com.example.javawebdemo.mapper.UserMapper;
import com.example.javawebdemo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class JavawebDemoApplicationTests {@Autowiredprivate UserMapper userMapper;@Testpublic void insertUser() {User user = new User();user.setId(1L);user.setUsername("Jack");user.setPassword("123456");user.setPhone("13111111111");user.setInfo("{\"age\":20,\"gender\":\"male\"}");user.setStatus(1);user.setBalance(200);user.setCreateTime(LocalDateTime.now());user.setInsertTime(LocalDateTime.now());userMapper.insert(user);}
}

可以看到我们根本没有配置xml,甚至连sql语句都没写就实现了插入数据的功能。

  1. 根据id查询用户
    @Testpublic void selectUserById() {User user = userMapper.selectById(1);System.out.println(user);}
  1. 根据id批量查询用户

目前表中只有1条数据,我们再插入id=2的一条数据,来测试批量查询:

    @Testpublic void selectUserByIds() {List<User> users = userMapper.selectByIds(Arrays.asList(1, 2));for (User user : users) {System.out.println(user);}}

在这里插入图片描述

  1. 根据id更新用户
    @Testpublic void updateUserById() {User user = new User();user.setId(1);user.setBalance(20000);userMapper.updateById(user);}

根据执行日志,可以看出会根据id只更新balance字段:
在这里插入图片描述
在这里插入图片描述

  1. 根据id删除用户
    @Testpublic void deleteUserById() {userMapper.deleteById(1);}

就这样单表的增删改查就完成了,的确是省事啊。但是它是怎么知道访问哪张表,以及表里有哪些字段的呢?继续往下看…

快速入门-常见注解

MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
在这里插入图片描述

并且有下面的约定:

  • 类名驼峰转下划线作为表名
  • 名为id的字段作为主键
  • 变量名驼峰转下划线作为表的字段名

所以我们在写实体类的时候尽量根据表的DDL来创建,简单来说约定大于配置。

但是如果我们实体类不符合这些约定怎么办?

不符合就得根据提供的注解来处理这些表名和字段名的映射了。

最常见的注解如下:

  • @TableName:用来指定表名
  • @TableId:用来指定表中的主键字段信息
  • @TableField:用来指定表中的普通字段信息

接下来我们详细讲解一下常见的注解。

TableName

用来映射数据库的表名。

上面的例子中我们可以看到,为什么MP会准确找到user这张表进行查询?是不是就是因为我们创建的实体类,名字就叫User,所以可以和数据库中的user表对应起来的关系。

那假如现在我的实例类名称换成Student,测试一下刚才写的根据id查用户的方法,看下什么效果:

Caused by: java.sql.SQLSyntaxErrorException: Table 'demo.student' doesn't exist

那么有时候我的实体类名称不一定和数据库中的表名能完全对应上,这时候@TableName注解就派上用场了。

我们可以在Student实体类加上这个注解​​​​​​​:

@TableName("user")
@Data
public class Student {

再查就没有问题了。

@TableField

@TableName注解是把实体类和表做映射,那么表里面还有字段信息,类属性和表字段肯定也是一一映射的。
如果类属性和表字段无法一一对应时,假如我将 ”username“ 这个属性修改为 ”name“,调用查询方法就会报错:

Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'name' in 'field list'

就需要@TableField注解的 value 属性即可解决。

    @TableField("username")private String name;

除了value属性之外,还有两个我们需要关注的属性。

  1. exist

当我们的实体类中定义了一些数据库中不存在的字段时​​​​​​​:

private String nickname;

调用查询接口就会报错:

Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'nickname' in 'field list'

这种情况只需要设置一下exist属性即可解决​​​​​​:

    @TableField(exist = false)private String nickname;
  1. select

该属性表示是否查询该字段。

比如我们给balance字段设置了select=false:

    @TableField(select = false)private Integer balance;

我们调用查询语句就会发现SQL语句中就不再查询balance这个字段了。

==>  Preparing: SELECT id,username AS name,password,phone,info,status,create_time,insert_time FROM user WHERE id=?
==> Parameters: 1(Integer)
<==    Columns: id, name, password, phone, info, status, create_time, insert_time
<==        Row: 1, Jack, 123456, 13111111111, {"age":20,"gender":"male"}, 1, 2025-02-19 14:33:54, 2025-02-19 14:33:54
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@11f9535b]
Student(id=1, name=Jack, nickname=null, password=123456, phone=13111111111, info={"age":20,"gender":"male"}, status=1, balance=null, createTime=2025-02-19T14:33:54, insertTime=2025-02-19T14:33:54)
  1. fill

表示是否自动填充。参考官网
在我们日常的开发工作中,数据库表里面通常都会有create_time、update_time字段,而这两个字段通常都是取当前时间来插入,我们就需要使用到自动填充。

    @TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime insertTime;

FieldFill是个枚举类​​​,有下面几种类型:

  • DEFAULT:不填充
  • INSERT:在插入的时候填充
  • UPDATE:在更新的时候填充
  • INSERT_UPDATE:在插入或者更新时填充​​​​

做完这些,程序还不会帮我们自动去填充这两个字段,我们需要创建一个处理器​​​​​​​:

package com.example.javawebdemo.handler;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {// 此处的metaObject代表当前对象this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);this.setFieldValByName("insertTime", LocalDateTime.now(), metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.setFieldValByName("insertTime", LocalDateTime.now(), metaObject);}
}

我们再来调用保存用户的方法进行测试:

    @Testpublic void insertUser() {Student student = new Student();student.setId(2L);student.setName("Rose");student.setPassword("123456");student.setPhone("13222222222");student.setInfo("{\"age\":18,\"gender\":\"female\"}");student.setStatus(1);student.setBalance(2000);
//        student.setCreateTime(LocalDateTime.now());
//        student.setInsertTime(LocalDateTime.now());userMapper.insert(student);}

可以看到两个时间被插入进来了:

==>  Preparing: INSERT INTO user ( id, username, password, phone, info, status, balance, create_time, insert_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 2(Long), Rose(String), 123456(String), 13222222222(String), {"age":18,"gender":"female"}(String), 1(Integer), 2000(Integer), 2025-02-19T15:33:55.894(LocalDateTime), 2025-02-19T15:33:55.894(LocalDateTime)
@TableId

@TableField注解是用来处理数据库中普通字段的,@TableId是用于处理主键字段的。
这注解中有几个属性需要重点关注一下:

  • value

映射主键id的字段名,这个和TableField中的value类似。

  • type

设置主键类型,主键的生成策略。这个type的值是使用一组枚举类型来表示的,它一共有五种类型​​​​​​​:

类型描述
AUTO数据库自增
NONE(默认)MP来设置主键,使用雪花算法实现
INPUT需要自己给主键赋值
ASSIGN_IDMP使用Long、Integer、String来分配ID
ASSIGN_UUIDMP分配UUID
  • NONE

我们的id字段是自增主键,但是我们上面测试插入数据的方法的时候,依然手动指定了,如果我们不指定默认情况就会是雪花算法实现的主键:

    @Testpublic void insertUser() {Student student = new Student();student.setName("Lucy");student.setPassword("123456");student.setPhone("13333333333");student.setInfo("{\"age\":18,\"gender\":\"female\"}");student.setStatus(1);student.setBalance(2000);userMapper.insert(student);}

我们看一下这个主键id是1892117103913984001是雪花算法生产的数字:

==>  Preparing: INSERT INTO user ( id, username, password, phone, info, status, balance, create_time, insert_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 1892117103913984001(Long), Lucy(String), 123456(String), 13333333333(String), {"age":18,"gender":"female"}(String), 1(Integer), 2000(Integer), 2025-02-19T15:40:46.071(LocalDateTime), 2025-02-19T15:40:46.071(LocalDateTime)
  • INPUT​​​​​​​

这种类型比较简单,就是你手工在程序里给主键赋值。

  • AUTO

这种方式就是交给数据库去处理,该自增自增。开发者无需处理了(就算你在程序里面手工给主键赋值,也只能按照数据库的自增来,相当于赋值无用)。

    @TableId(value = "id", type = IdType.AUTO)private Long id;

我们表中就是自增id,因此一般可以用这种类型。

  • ASSIGN_ID 和 ASSIGN_UUID

当使用该属性,同样是MP采用雪花算法生成一个随机值,赋值给你类中id属性,并把该属性插入到数据库,和ASSIGN_UUID的区别在于,ASSIGN_UUID规定id必须是String类型,数据库字段必须是varchar类型,而且数据库原先的主键自增得去掉,否则也会报错,字符串类型是无法自增的。

快速入门-常用配置

MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。比如:

mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true # 是否开启下划线和驼峰映射cache-enabled: false # 是否开启二级缓存global-config:db-config:id-type: autoupdate-strategy: not_null # 更新策略:只更新非空字段type-aliases-package: com.example.javawebdemo.entity # 别名扫描包mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值

其他配置参考官网:https://baomidou.com/reference/

  • type-aliases-package

MyBatis-Plus 会自动为 com.example.javawebdemo.entity 包下的每个类创建一个别名。默认别名是类名的 首字母小写 形式。例如,包中有一个名为 User 的类,则 MyBatis 会自动为它生成别名 user。

配置别名后,你可以在 MyBatis 的 XML 映射文件中直接使用这些别名,而不需要全限定类名,例如:

<resultMap id="UserResultMap" type="user"><id property="id" column="id"/><result property="username" column="username"/><result property="password" column="password"/>
</resultMap>

在 type 属性中使用 user 别名,代替了 com.example.javawebdemo.entity.User,从而使 XML 文件更简洁,提升代码可读性和可维护性。

核心功能-条件构造器

上面的查询都是基于id进行的,有些查询场景可能比较复杂,MP就提供了条件构造器来解决这类问题。

MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。
在这里插入图片描述
这里的这些方法中需要传入Wrapper就是条件构造器,下面的Wrapper的继承体系:

在这里插入图片描述
我们这里截了有些Wrapper使用方法的图:
在这里插入图片描述

QueryWrapper顾名思义是做查询的,select * from tb where xxx,父类解决了where后面的条件,QueryWrapper拓展了select功能:
在这里插入图片描述
UpdataWrapper拓展了update功能:
在这里插入图片描述

  1. 基于QueryWrapper的查询案例
  • 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
    @Testpublic void testQueryWrapper(){// 1. 构建查询条件QueryWrapper<User> wrapper = new QueryWrapper<User>().select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2. 查询List<User> users = userMapper.selectList(wrapper);for (User user : users) {System.out.println(user);}}

我们看一下查询日志:

==>  Preparing: SELECT id,username,info,balance FROM user WHERE (username LIKE ? AND balance >= ?)
==> Parameters: %o%(String), 1000(Integer)
<==    Columns: id, username, info, balance
<==        Row: 2, Rose, {"age":18,"gender":"female"}, 2000
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44a085e5]
User(id=2, username=Rose, password=null, phone=null, info={"age":18,"gender":"female"}, status=null, balance=2000, createTime=null, insertTime=null)
  • 更新用户名为Jack的用户的余额为2000
    @Testpublic void testQueryWrapper() {// 1. 要更新的数据User user = new User();user.setBalance(2000);// 2. 更新的条件QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");// 3. 执行更新userMapper.update(user, wrapper);}

我们看看执行过程日志:

==>  Preparing: UPDATE user SET balance=?, insert_time=? WHERE (username = ?)
==> Parameters: 2000(Integer), 2025-02-19T16:50:01.476(LocalDateTime), Jack(String)

在这里插入图片描述

  1. 基于UpdateWrapper的更新
  • 更新id为1,2的用户的余额,扣200
    @Testpublic void testUpdateWrapper() {List<Integer> list = Arrays.asList(1, 2);UpdateWrapper<User> wrapper = new UpdateWrapper<User>().setSql("balance = balance - 200").in("id", list);userMapper.update(wrapper);}

查看执行过程:

==>  Preparing: UPDATE user SET balance = balance - 200 WHERE (id IN (?,?))
==> Parameters: 1(Integer), 2(Integer)

在这里插入图片描述

我们这里再演示一下LambdaQueryWrapper的用法,其实和QueryWrapper是类似的,区别在于它在构造条件时是基于lambda语法,这样就可以在选择字段的时候不用硬编码了,我们现在都是把字段名称写死了:

    @Testpublic void testLambdaQueryWrapper() {// 1. 构建查询条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2. 查询List<User> users = userMapper.selectList(wrapper);for (User user : users) {System.out.println(user);}}
  1. 条件构造器的用法:
  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码

核心功能-自定义SQL

我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。

我们用案例来体验一下:

  1. 需求:将id在指定范围的用户(例如1、2)的余额扣减指定值

这个需求我们上面就做过:

    @Testpublic void testUpdateWrapper() {List<Integer> list = Arrays.asList(1, 2);UpdateWrapper<User> wrapper = new UpdateWrapper<User>().setSql("balance = balance - 200").in("id", list);userMapper.update(wrapper);}

上面的代码有什么问题呢?我们现在写的逻辑是业务逻辑,将来是在service层定义,这就相当于我们把一部分sql语句写到service层了,这在很多企业开发规范中是不允许的,他们会要求只能在mapper和mapper.xml中定义SQL语句,所以上面的写法就得改造了。

既然MP擅长where条件的构建,一旦出现复杂场景,比如:
在这里插入图片描述
在这里插入图片描述
那就把where条件交给MP做,剩下的自定义。把MP构建好的where条件传递给mapper层,在mapper层或者mapper.xml中实现SQL组装,那具体怎么实现传递和组装呢?需要三步:

  • 基于Wrapper构建where条件

在这里插入图片描述

  • 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew

在这里插入图片描述

  • 自定义SQL,并使用Wrapper条件

在这里插入图片描述

我们来跟着这三个步骤实现一下:

(1)基于Wrapper构建where条件

    @Testpublic void testCustomSqlUpdate() {// 更新条件List<Integer> ids = Arrays.asList(1, 2);int amount = 200;// 定义条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 自定义SQLuserMapper.updateBalanceByIds(wrapper,amount);}

(2)在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew

package com.example.javawebdemo.mapper;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.example.javawebdemo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface UserMapper extends BaseMapper<User> {void updateBalanceByIds(@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);}

(3)自定义SQL,并使用Wrapper条件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.javawebdemo.mapper.UserMapper"><update id="updateBalanceByIds">UPDATE user SET balance = balance - #{amount} $(ew.customSqlSegment)</update>
</mapper>

接下来我们测试一下:

==>  Preparing: UPDATE user SET balance = balance - ? WHERE (id IN (?,?))
==> Parameters: 200(Integer), 1(Integer), 2(Integer)

核心功能-IService接口基本用法

MP还提供了IServcie接口,只要继承了它,连Service代码都可以不用写了。

下面我们看看IService提供的一些方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到把常用的增删改查都封装好了。

使用IService之前,看看我们之前开发流程:
在这里插入图片描述

需要自定义Service接口,并写一个实现类。

现在IService中有大量封装好的方法,我们只要继承它就可以,现在的继承体系如下:
在这里插入图片描述

总结下来MP的Service接口使用流程就是两步:

  1. 自定义Service接口继承IService接口

在这里插入图片描述

  1. 自定义Service实现类,实现自定义接口并继承ServiceImpl类

在这里插入图片描述

接下来我们按照这两步骤,代码中写一下:

(1)自定义Service接口继承IService接口

package com.example.javawebdemo.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.javawebdemo.entity.User;public interface IUserService extends IService<User> {
}

(2)自定义Service实现类,实现自定义接口并继承ServiceImpl类

package com.example.javawebdemo.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.javawebdemo.entity.User;
import com.example.javawebdemo.mapper.UserMapper;
import com.example.javawebdemo.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

然后我们写逻辑测试一下:

  • 新增用户
    @Autowiredprivate IUserService userService;@Testpublic void testSaveUser(){User user = new User();user.setUsername("Curry");user.setPassword("123456");user.setPhone("18888888888");user.setInfo("{\"age\":30,\"gender\":\"male\"}");user.setStatus(1);user.setBalance(20000000);userService.save(user);}

执行日志可以看到插入成功了:

==>  Preparing: INSERT INTO user ( username, password, phone, info, status, balance, create_time, insert_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: Curry(String), 123456(String), 18888888888(String), {"age":30,"gender":"male"}(String), 1(Integer), 20000000(Integer), 2025-02-20T09:54:39.429(LocalDateTime), 2025-02-20T09:54:39.429(LocalDateTime)

在这里插入图片描述

这里有个问题,就是我们在配置文件中配了全局id自增策略,由于刚开始默认是雪花的,所以后面就算是已经设置了自增,也是基于之前ID数,自增了。

  • 根据id查询
    @Testpublic void testQuery(){List<User> users = userService.listByIds(Arrays.asList(1, 2));for (User user : users) {System.out.println(user);}}

执行日志:

==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,insert_time FROM user WHERE id IN ( ? , ? )
==> Parameters: 1(Integer), 2(Integer)
<==    Columns: id, username, password, phone, info, status, balance, create_time, insert_time
<==        Row: 1, Jack, 123456, 13111111111, {"age":20,"gender":"male"}, 1, 1600, 2025-02-19 14:33:54, 2025-02-19 16:54:06
<==        Row: 2, Rose, 123456, 13222222222, {"age":18,"gender":"female"}, 1, 1600, 2025-02-19 15:33:56, 2025-02-19 15:33:56
<==      Total: 2

核心功能-IService开发基础业务接口

我们发现MP提供的BaseMapper和IService中的方法很多是重复的,那实际业务开发中,到底该使用哪个接口提供的方法呢?接下来用几个案例来看看实际开发如何使用。

  1. 基于Restful风格实现下列接口

在这里插入图片描述

这个不是像上面做单元测试,而是写接口,做完成的业务开发。在实现之前要做点准备工作:

(1)引入相关依赖

  • 引入Swagger依赖,方便做单元测试
  • 引入Web依赖,编写Swagger相关功能
        <!--  swagger--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.1.0</version></dependency><!-- web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- hutool用到BeanUtil等工具类--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.21</version></dependency>

(2)swagger相关配置

knife4j:enable: trueopenapi:title: 用户接口管理文档description: "用户接口管理文档"email: xuec_7@163.comconcat: sunnyurl: https://sunnyrivers.blog.csdn.net/version: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.example.javawebdemo.controller

(3)实体类

我们把所有实体相关类都放在domain包:

在这里插入图片描述

package com.example.javawebdemo.domain.dto;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户表实体")
public class UserFromDTO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("密码")private String password;@ApiModelProperty("注册手机号")private String phone;@ApiModelProperty("详细信息,JSON风格")private String info;@ApiModelProperty("账户余额")private Integer balance;
}

新增用户接口开发

package com.example.javawebdemo.controller;import cn.hutool.core.bean.BeanUtil;
import com.example.javawebdemo.domain.dto.UserFromDTO;
import com.example.javawebdemo.domain.entity.User;
import com.example.javawebdemo.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {// @Autowired   Spring不推荐用这种方法注入:  Field injection is not recommended ,推荐用构造函数// 构造函数可以用lombok简写,但是用哪个构造注解呢?一个类的成员变量有很多,但并不是所有的都需要注入// 首先给类加上final,也就是必须在初始化的时候,给变量初始化,// 然后用@RequiredArgsConstructor注解,它只会对一些需要一开始初始化的变量构造private final IUserService userService;@ApiOperation("新增用户接口")@PostMappingpublic void saveUser(@RequestBody UserFromDTO userDTO) {// 以前我们都是先写service方法,然后再写mapper方法,现在我们有了MP后,我们写接口的时候,// 我们先考虑这个功能MP是否已经提供了?// 1.把DTO拷贝到ENTITYUser user = BeanUtil.copyProperties(userDTO, User.class);// 2. 保存userService.save(user);}
}

我们一行service代码都没有编写,一个新增用户的接口就开发完成了。

同理删除用户接口

    @ApiOperation("删除用户接口")@DeleteMapping("{id}")public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {userService.removeById(id);}

根据id查询用户接口

    @ApiOperation("根据id查询用户接口")@GetMapping("{id}")public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {User user = userService.getById(id);// 把entity拷贝到VOreturn BeanUtil.copyProperties(user, UserVO.class);}

根据id批量查询接口

    @ApiOperation("根据id批量查询用户接口")@GetMapping()public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {List<User> users = userService.listByIds(ids);return BeanUtil.copyToList(users, UserVO.class);}

上面的接口都比较简单,没什么业务逻辑,都直接在controller里面都搞定了,有些接口有复杂的业务逻辑,就需要自定义service,当然还有些复杂的,我们还要在mapper中自定sql语句。

根据id扣减余额接口

这个接口需要写业务逻辑,比如拿到用户的id,先要判断状态是否正常,只有正常才扣减,还要判断余额是否充足,只有充足才能扣减,所以这里面有很多业务判断,而MP中的方法是没有业务逻辑的。
还有我们更新余额的sql语句,不建议在业务层来写,需要自定义SQL语句完成更新。

controller层:

    @ApiOperation("扣减余额接口")@PutMapping("/{id}/deduction/{money}")public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id,@ApiParam("扣减的金额") @PathVariable("money") Integer money) {userService.deductBalance(id, money);}

service层:

package com.example.javawebdemo.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.javawebdemo.domain.entity.User;
import com.example.javawebdemo.mapper.UserMapper;
import com.example.javawebdemo.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic void deductBalance(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常");}// 检验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足");}// 扣减余额baseMapper.deductBalance(id, money);}
}

mapper层:

package com.example.javawebdemo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.javawebdemo.domain.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface UserMapper extends BaseMapper<User> {void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}

mapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.javawebdemo.mapper.UserMapper"><update id="deductBalance">UPDATE user SET balance = balance - #{money}<where>id = #{id}</where></update>
</mapper>

写完代码,我们启动服务进行测试,先打开swagger:
http://localhost:8080/doc.html
在这里插入图片描述
接下来就可以在swagger中进行接口测试了。

在这里插入图片描述
其他接口可以进行测试,这里就不演示了。

核心功能-IService提供的lambda方法

我们依然是通过案例来讲解

  1. 需求一:实现一个根据复杂条件查询用户的接口,查询条件如下:
    (1)name:用户名关键字,可以为空
    (2)status:用户状态,可以为空
    (3)minBalance:最小余额,可以为空
    (4)maxBalance:最大余额,可以为空

这个需求如果我们按照之前mybatis的写法将会是这样的:

    <select id="queryUsers" resultType="com.example.javawebdemo.domain.entity.User">SELECT * FROM user<where><if test="name != null">AND username LIKE #{name}</if><if test="status != null">AND status = #{status}</if><if test="minBalance != null and maxBalance != null">AND balance between #{minBalance} AND #{maxBalance}</if></where></select>

可以看到这种写法非常麻烦,我们看看IService的Lambda是如何做的。

这里写的方法中参数可以用@RequestParam(value = “xxx”,required = false)来注解,但是如果参数很多就推荐用定义一个对象来接收:

package com.example.javawebdemo.domain.query;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {@ApiModelProperty("用户名关键字")private String name;@ApiModelProperty("用户状态:1-正常,2-冻结")private Integer status;@ApiModelProperty("余额最小值")private Integer minBalance;@ApiModelProperty("余额最大值")private Integer maxBalance;
}

controller层:

    @ApiOperation("根据复杂条件查询用户接口")@GetMapping("/list")public List<UserVO> queryUser(UserQuery query) {List<User> users = userService.queryUser(query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());return BeanUtil.copyToList(users, UserVO.class);}

service层:

    @Overridepublic List<User> queryUser(String name, Integer status, Integer minBalance, Integer maxBalance) {return lambdaQuery().like(name != null, User::getUsername, name).eq(status!= null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance).list();}

重启服务进行测试:
在这里插入图片描述

我们查询名字中带o的用户:
在这里插入图片描述

除了查询外还提供了一个更新的方法,也很方便,我们用案例讲解。

  1. 需求:改造根据id修改用户余额的接口,要求如下
    (1)完成对用户状态校验
    (2)完成对用户余额校验
    (3)如果扣减后余额为0,则将用户status修改为冻结状态-2

controller层:

    @ApiOperation("扣减余额接口")@PutMapping("/{id}/deduction/{money}")public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id,@ApiParam("扣减的金额") @PathVariable("money") Integer money) {userService.deductBalance(id, money);}

service层:

    @Transactionalpublic void deductBalance(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常");}// 检验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足");}int remainBalance = user.getBalance() - money;// 扣减余额lambdaUpdate().set(User::getBalance, remainBalance).set(remainBalance == 0, User::getStatus, 2).eq(User::getId, id).eq(User::getBalance, user.getBalance()) // 乐观锁.update();}

重启服务进行测试:
在这里插入图片描述
Lucy余额原来的1800,扣减1800后余额为0,状态也改成2了:
在这里插入图片描述

  1. 需求3:批量插入10万条用户数据,并作出对比:
    (1)普通for循环插入
    (2)IService的批量插入
    (3)开启rewriteBatchedStatements=true参数

我们先写个方法,给数据库中插入10W条数据,用普通for循环插入:

    private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123456");user.setPhone("" + (18688190000L + i));user.setInfo("{\"age\":30,\"gender\":\"male\"}");user.setBalance(2000);return user;}@Testvoid testSaveOneByOne() {long b = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {userService.save(buildUser(i));}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));}

在这里插入图片描述
接下来我们把数据清空,接下来我们测试一下批处理:

    @Testvoid testSaveBatch() {// 每次批量插入1000条,插入100次List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {list.add(buildUser(i));if (i % 1000 == 0) {userService.saveBatch(list);list.clear();}}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));}

在这里插入图片描述
可以看到效率提升还是比较明显,核心就是减少网络请求的次数。如果把sql语句变成类似于:
在这里插入图片描述

那句只需要1次网络请求,效率会更快。

其实只要把mysql的参数rewriteBatchedStatements=true即可。

在这里插入图片描述

默认情况下,MySQL JDBC驱动(Connector/J)的addBatch()方法仅作语句缓存,实际执行时会逐条发送SQL语句到数据库,开启rewriteBatchedStatements=true后,驱动会重写批量语句为更高效的格式:

INSERT INTO table (col1,col2) VALUES (1,2),(3,4),(5,6)  -- 合并为多值语句
UPDATE table SET col=val WHERE id=1; UPDATE table SET col=val WHERE id=2  -- 分号连接多语句

接下来我们重新执行批处理的代码,发现10W条数据大约用了7s就插入完成了。

总结一下批处理的三种方案:
在这里插入图片描述

扩展功能-静态工具

静态工具提供的方法和IService方法基本上一样,只不过IService方法中的接口都是非静态的,而Db静态工具里面的方法都是静态的,我们用IService的时候就需要继承它,还要传入实体类,这样IService就能通过反射得到实体类的字节码,得到表信息从而实现增删改查;而静态工具的每个方法都需要传入实体类的字节码:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

那既然两个功能相似为什么要搞两个呢?
开发业务的时候,有的时候会出现多个service相互调用的场景,如果采用传统的方式,也就是用@Autowired来注入,那Service之间相互注入就会出现循环依赖,因此此时就直接用静态工具。

扩展功能-逻辑删除

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为1
  • 查询时只查询标记为0的数据

例如逻辑删除字段为deleted:

  • 删除操作:
    在这里插入图片描述

  • 查询操作:

在这里插入图片描述

MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:

在这里插入图片描述

在这里插入图片描述

扩展功能-枚举处理器

枚举处理器可以实现java中的枚举类型和数据库之间的相互转换。
比如现在有下面User实体类,用户状态1代表正常、2代表冻结:
在这里插入图片描述
我们在比较或者赋值的时候都是基于数字进行的,如果类型有很多的情况下,那我们就不知道哪个数字对应什么状态了,这个时候我们就可以用枚举来表示:
在这里插入图片描述
那将来不管有多少枚举项,都可以在这写出来,这样一看到枚举对于每个数字代表什么状态就一目了然了。

现在状态字段是Integer类型,我们在用set赋值的时候依然得赋1和2,这样还存在两个问题:

  1. 每次比较或赋值的时候还得查一下枚举
  2. 代码的可读性很差

为了解决这个问题,这里的状态字段就不要再用Integer类型了,就应该用枚举类型:

private UserStatus status;

但是这样又有一个问题:我们数据表中status依然是int类型,这就存在java中的枚举类型和数据库之间相互转换的问题。其实不仅仅是枚举类型,java中的所有类型都需要和数据库类型进行相互转换,只不过底层都是由mybatis帮我们做了:
在这里插入图片描述
我们看到上面也有枚举类型,但是它的功能有限,仅支持枚举的序数(ordinal())或名称(name())与数据库字段的简单映射。例如,将枚举的ordinal()值(如0,1,2)存入数据库,功能较为基础:

public enum UserStatus {ENABLED,  // ordinal = 0 DISABLED; // ordinal = 1 
}
  • 插入数据
    当插入一个 User 对象时,若 status = UserStatus.ENABLED,MyBatis 会将其转换为 0 存入数据库。
  • 查询数据
    若数据库中的 status 值为 1,MyBatis 会将其转换为 UserStatus.DISABLED。
  • 存在问题
    如果调整枚举定义的顺序(例如将 DISABLED 定义在 ENABLED 之前),ordinal() 值会变化,导致历史数据错乱。

所以上面有两个是mybatisplus进行扩展的类:MybatisEnumTypeHandler和AbstractJsonTypeHandler。

如何实现PO类中的枚举类型变量与数据库字段的转换?

  1. 给枚举中的与数据库对应value值添加@EnumValue注解

在这里插入图片描述

  1. 在配置文件中配置统一的枚举处理器,实现类型转换

在这里插入图片描述

我们做个测试:

定义枚举类:

package com.example.javawebdemo.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FROZEN(2, "冻结"),;@EnumValueprivate final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}

配置文件:

mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

用枚举类替换Integer:

package com.example.javawebdemo.domain.entity;import com.baomidou.mybatisplus.annotation.*;
import com.example.javawebdemo.enums.UserStatus;
import lombok.Data;import java.time.LocalDateTime;@TableName("user")
@Data
public class User {private Long id;private String username;private String password;private String phone;private String info;private UserStatus status;private Integer balance;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime insertTime;
}

插件功能

MyBatisPlus提供的内置拦截器有下面这些:
在这里插入图片描述
我们这里主要讲解分页插件的使用:

  1. 注册一个配置类​​​​​​​,并编写paginationInterceptor方法
@Configuration
public class MybatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {return new PaginationInterceptor();}}
  1. 编写分页查询代码
@Test
void select() {// Page构造方法有两个参数:current-当前页;size:-页大小Page<User> page = new Page<>(1, 10);// 返回值也是一个Page类型,里面包含了分页信息以及查询出来的数据信息。Page<User> result = mapper.selectPage(page, null);// 查询的结果在Records中result.getRecords().forEach(System.out::println);
}

参考文献

https://baomidou.com/guides/mybatis-x/
https://blog.csdn.net/qq_41320700/article/details/143837356


http://www.ppmy.cn/embedded/164449.html

相关文章

PHP实现登录和注册(附源码)

前言 本博客主要讲述利用php环境实现一个简单的前后端结合的用户登录和注册功能。phpstudy是PHP调试环境的集成包&#xff0c;该程序包集成了 ApachePHPMySQLphpMyAdmin 等多个工具&#xff0c;是很好用的调试环境的程序集成包。 目录 前言 1. 准备工作 1.1 工具 1.2 php…

跟着 Lua 5.1 官方参考文档学习 Lua (5)

文章目录 2.10 – Garbage Collection2.10.1 – Garbage-Collection Metamethods2.10.2 – Weak Tables 2.10 – Garbage Collection Lua performs automatic memory management. This means that you have to worry neither about allocating memory for new objects nor abo…

Spring Boot(快速上手)

Spring Boot 零、环境配置 1. 创建项目 2. 热部署 添加依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional> </dependency&…

蓝桥杯备考:搜索算法之枚举子集

这是我们的决策树&#xff0c;我们要自己搞定递归函数 #include <iostream> using namespace std;string ret; int n; void dfs(int pos) {if(pos>n){cout << ret << endl;return;}retN;dfs(pos1);ret.pop_back(); retY;dfs(pos1);ret.pop_back(); } in…

文件分片上传 python

服务端功能 上传分片保存 app.route(/upload_filesliceprocess, methods[POST]) def upload_filesliceprocess(): file request.files[file] name_index request.form[name_index] complete request.form[complete] process request.form[process] c…

第4章 信息系统架构(五)

4.7 安全架构 安全保障以风险和策略为基础&#xff0c;在信息系统的整个生命周期中&#xff0c;安全保障应包括技术、管理、人员和工程过程的整体安全&#xff0c;以及相关组织机构的健全等。 4.7.1 安全威胁 常见的威胁有&#xff1a; 4.7.2 定义和范围 安全性体现在信息系…

爬虫第七篇数据爬取及解析

这篇博客旨在分享学习过程中的心得和体会&#xff0c;如果有错误请指出&#xff0c;感谢大家。 经过前面的学习&#xff0c;那么我们也就进入了数据爬取的阶段&#xff0c;大家跟着我的步伐一起来学习一下&#xff0c;爬虫的数据爬取与数据解析&#xff08;本篇主要针对于带有…

深入剖析Spring MVC

一、Spring MVC 概述 1. 什么是 Spring MVC&#xff1f; Spring MVC 是基于 Spring 框架的 Web 框架&#xff0c;它实现了 MVC 设计模式&#xff0c;将应用程序分为三个核心部分&#xff1a; Model&#xff1a;封装应用程序的数据和业务逻辑。 View&#xff1a;负责渲染数据…