SpringBoot2 学习笔记04 MyBatis-Plus

本文含有: SpringBoot2中关于MyBatis-Plus使用教程, CURD查询增加修改删除, 分页, 锁, 注解开发, 条件构造器 QueryWrapper, UpdateWrapper, condition, LambdaQueryWrapper & LambdaUpdateWrapper

更新记录

20231211:第一版

20241122:重构系列

配置环境

导入数据

-- 创建学生表
CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 学生ID,主键,自增
    name VARCHAR(50) NOT NULL,          -- 学生姓名
    age INT NOT NULL,                   -- 学生年龄
    teacher_id INT                      -- 所属老师ID
);

-- 创建老师表
CREATE TABLE teacher (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 老师ID,主键,自增
    name VARCHAR(50) NOT NULL          -- 老师姓名
);

INSERT INTO student (id, name, age, teacher_id) VALUES
(1, '小明', 16, 1),   -- 小明 对应 张老师
(2, '小红', 17, 1),   -- 小红 对应 张老师
(3, '小华', 15, 2),   -- 小华 对应 李老师
(4, '小丽', 16, 2),   -- 小丽 对应 李老师
(5, '小强', 17, 3),   -- 小强 对应 王老师
(6, '小芳', 16, 3),   -- 小芳 对应 王老师
(7, '小亮', 15, 4),   -- 小亮 对应 赵老师
(8, '小霞', 17, 4);   -- 小霞 对应 赵老师

INSERT INTO teacher (id, name) VALUES
(1, '张老师'),
(2, '李老师'),
(3, '王老师'),
(4, '赵老师');

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>

    <!-- 组名 -->
    <groupId>org.example</groupId>
    <!-- 项目ID -->
    <artifactId>demo</artifactId>
    <!-- 项目版本 -->
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.7.18</spring-boot.version>
        <alibaba.druid.version>1.2.21</alibaba.druid.version>
        <mybatis.plus.boot.version>3.5.3.2</mybatis.plus.boot.version>
        <mysql.boot.version>8.0.33</mysql.boot.version>
        <lombok.version>1.18.30</lombok.version>
    </properties>

    <!-- 管理依赖版本 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringBoot的依赖配置-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 阿里数据库连接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${alibaba.druid.version}</version>
            </dependency>

            <!-- MP -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis.plus.boot.version}</version>
            </dependency>

            <!-- MYSQL -->
            <dependency>
                <groupId>com.mysql</groupId>
                <artifactId>mysql-connector-j</artifactId>
                <version>${mysql.boot.version}</version>
            </dependency>

            <!-- lombok -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 应用依赖 -->
    <dependencies>
        <!-- Web模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 阿里数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <!-- MP -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- MYSQL -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>


        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>public</id>
            <name>aliyun nexus</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>public</id>
            <name>aliyun nexus</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>

添加配置文件

# 应用服务 WEB 访问端口
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    Studentname: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    cache-enabled: true

创建 entity 实体类

package org.example.entity;

import lombok.Data;

@Data
public class Student implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
    private Integer teacherId;
}

package org.example.entity;

import lombok.Data;

@Data
public class Teacher implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
}

Mapper Interface

https://baomidou.com/guides/data-interface/#mapper-interface 建议看文档, 我这里只是简单介绍, 本文重点在条件构造器

首先, 不讲 IService,我更偏向于使用 mapper

Mapper Interface 就是上面 mapper 继承的 extends BaseMapper

我们创建一个 StudentMapper.xml 文件, 使他继承 BaseMapper 的 Mapper

@Mapper
public interface StudentMapper extends BaseMapper<Student> {
}

你可以直接通过 studentMapper.insert() 去使用, 接下来看些案例

public interface BaseMapper<T> extends Mapper<T> {
    int insert(T entity);

    int deleteById(Serializable id);

    int deleteByMap(@Param("cm") Map<String, Object> columnMap);

    int delete(@Param("ew") Wrapper<T> queryWrapper);

    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> 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);

    T selectOne(@Param("ew") Wrapper<T> queryWrapper);

    Integer 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);

    <E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);

    <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}

这里要注意,给 entity/User 的 ID 属性添加自增注解,不然他默认会以 UUID 作为 ID。上面有说明

那么简单过以下基础操作

@Data
@RestController
@Slf4j
public class HelloController {

    @Autowired
    private StudentMapper studentMapper;

    @GetMapping("/")
    public String show() {
        Student student = new Student();
        student.setName("test");
        student.setAge(10);
        student.setTeacherId(1);
        int result = studentMapper.insert(student);
        System.out.println("受影响行数:" + result);
        return "show";
    }
}

增加

Student student = new Student();
student.setName("test");
student.setAge(18);
student.setTeacherId(1);
int result = studentMapper.insert(student);
System.out.println(student);

修改

// 根据 ID 修改
Student student = new Student();
student.setId(1);
student.setName("小明-update");
int result = studentMapper.updateById(student);

删除

// 删除一个
int result = studentMapper.deleteById(1);

// 根据 ID 删除多个
List<Long> idList = Arrays.asList(5L, 7L);
studentMapper.deleteBatchIds(idList);

// 根据 Map 条件删除多个
Map<String, Object> map = new HashMap<>();
map.put("name", "test");	// 删除所有 name == test 的数据
studentMapper.deleteByMap(map);

查询

// 查询一个
System.out.println(studentMapper.selectById(1));

// 根据 ID 查询多个
List<Long> idList = Arrays.asList(2L, 3L);
System.out.println(studentMapper.selectBatchIds(idList));

// 根据 Map 条件查询多个
Map<String, Object> map = new HashMap<>();
map.put("name", "Jack");    // 删除所有 name == Jack 的数据
System.out.println(studentMapper.selectByMap(map));

// 查询所有
System.out.println(studentMapper.selectList(null));

这里补充下, 你可以通过传递 Student 对象去查询, 比如你传递了id和name, 他就会根据这两个值去查询。这个后面也会讲, 现在提前说下

@Mapper
public interface StudentMapper extends BaseMapper<Student> {
    default List<Student> selectStudentList(Student student) {
        return selectList(Wrappers.lambdaQuery(student));
    }
}

条件构造器 Wapper

Wrapper

条件构造器大致可以分为三类

接下来对着三类做个简单介绍

  • 第一个: QueryWrapper, UpdateWrapper
QueryWrapper<Student> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "小明");
Student student = studentMapper.selectOne(queryWrapper);
System.out.println(student);
  • 第二个: LambdaQueryWrapper, LambdaUpdateWrapper
LambdaQueryWrapper<Student> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(Student::getName, "小明");
Student student = studentMapper.selectOne(lambdaQueryWrapper);
System.out.println(student);
  • 第三个: LambdaQueryChainWrapper, LambdaUpdateChainWrapper
    • 如果要查询一个, 结尾 .one()
    • 如果要查询多个, 结尾 .list()
    • 如果要更新, 结尾 .update()
Student student = new LambdaQueryChainWrapper<>(studentMapper)
        .eq(Student::getName, "小明")
        .one();
System.out.println(student);
  • 总结
    • 第一种 QueryWrapper/UpdateWrapper 因为字段名称是直接用字符串的, 非常不好维护,因此不推荐使用。
    • 第二种 LambdaQueryWrapper/LambdaUpdateWrapper 他在第一种的基础上将字段使用 实体类的属性 来表示, 提高了代码的可读性和可维护性。
    • 第三种 LambdaQueryChainWrapper/LambdaUpdateChainWrapper 我非常推荐使用这种方式, 他可以有效减少你需要写的代码。
    • 最后我需要提醒下, Wrapper的所有方法都是基于 AbstractWrapper 这个抽象类的

常见方法

下面是一些比较常见的条件

.select("id", "name", "age")——SELECT id, name, age FROM user
.lt()——小于
.le()——小于等于
.gt()——大于
.ge()——大于等于
.eq()——等于
.ne()——不等于
.in("id", Arrays.asList(1, 2, 3));——字段的值在给定的集合中
.notIn("id", Arrays.asList(1, 2, 3));——字段的值不在给定的集合中
.betweeen("age",10,20)——age在值10到20之间
.notBetweeen("age",10,20)——age不在值10到20之间
.like("属性","值")——模糊查询匹配值"%值%"
.notLike("属性","值")——模糊查询不匹配值"%值%"
.likeLeft("属性","值")——模糊查询匹配最后一位值"%值"
.notLikeLeft("属性","值")——模糊查询不匹配最后一位值"%值"
.likeRight("属性","值")——模糊查询匹配第一位值"值%"
.notLikeRight("属性","值")——模糊查询不匹配第一位值"值%"
.isNull("属性")——属性为空或null
.isNotNull("属性")——属性不为空或null
.groupBy("属性","属性")——根据属性分组
.having("sum(age) > {0}", 10)——与groupBy一起用, 返回age和大于10的
.orderByAsc("属性")——根据属性升序排序
.orderByDesc("属性")——根据属性降序排序
  • setSql
    • 自定义的 SQL 片段
lambdaUpdateWrapper.setSql("name = 'New Name'");
// 等价于
UPDATE user SET name = 'New Name'
  • .inSql:使用 SQL 语句来生成 IN 子句中的值集合
lambdaQueryWrapper.inSql(Student::getId, "select id from user where id < 3");
// 等价于
SELECT * FROM user WHERE id IN (select id from other_table where id < 3)
  • 除了 inSql 还有下面这些
    • 你只需将上面SQL中 WHERE id(select...from) 的连接词换成下面我写好的就可以了
    • 比如notInSqlSELECT * FROM user WHERE id NOT IN (select id from other_table where id < 3)
.notInSql——NOT IN
.eqSql——=
.gtSql——>
.geSql——>=
.itSql——<
.leSql——<=

条件连接

  • or() / and()
lambdaQueryWrapper.eq(Student::getId, 1).or().eq(Student::getName, "老王");
// 等价于
SELECT * FROM user WHERE id = 1 OR name = '老王'

嵌套

lambdaQueryWrapper.or(i ->
        i.and(j -> j.eq(Student::getName, "李白").eq(Student::getStatus, "alive"))
                .or(j -> j.eq(Student::getName, "杜甫").eq(Student::getStatus, "alive"))
);
// 等价于
SELECT * FROM user 
WHERE (name = '李白' AND status = 'alive') 
OR (name = '杜甫' AND status = 'alive')

func

lambdaQueryWrapper.func(i -> {
    if (true) {
        i.eq(Student::getId, 1);
    } else {
        i.ne(Student::getId, 1);
    }
});

拼接sql片段

  • apply / last
    • last 在最后拼接
    • 比如: .last("limit 1")
  • 拼接 SQL 片段, 下面是使用占位符的例子
  • 并且他有三个参数
    • condition 可以控制是否启用
    • applySql SQL片段
    • params 参数
apply(boolean condition, String applySql, Object... params)

lambdaQueryWrapper.apply("date_format(dateColumn, '%Y-%m-%d') = {0}", "2008-08-08");
// 等价于
SELECT * FROM user WHERE date_format(dateColumn, '%Y-%m-%d') = '2008-08-08'

exists

  • exists / notExists
lambdaQueryWrapper.exists("select id from table where age = 1");
// 等价于
SELECT * FROM user WHERE EXISTS (select id from table where age = 1)

实例

接下来我会写一些实例方便你理解

查询

  • 1、下面两种方式的结果都是一样的。
// 第一种
List<Student> student = studentMapper.selectList(
        new LambdaQueryWrapper<Student>()
                .like(Student::getName, "小明")
);
// 第二种
LambdaQueryWrapper<Student> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(Student::getName, "小明");
Student student = studentMapper.selectOne(lambdaQueryWrapper);
  • 2、判断变量是否存在 condition
    • 第一个参数传递:true/false,假如为 false 则代表不使用该条件。
String name = "小明";
List<Student> student = studentMapper.selectList(
        new LambdaQueryWrapper<Student>()
                .like(name.isEmpty(), Student::getName, name)
);

关于查询就基本就这点了东西了

增加

这里也不说了,上面写过个例子,这个没什么好说的

修改

  • 1、修改ID=1的名称
Student student = new Student();
student.setName("小明-update2");

LambdaQueryWrapper<Student> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Student::getId, 1);

int result = studentMapper.update(student, lambdaQueryWrapper);
  • 2、又或是这样也可以
// 根据 ID 修改
Student student = new Student();
student.setId(1);
student.setName("小明-update3");
int result = studentMapper.updateById(student);
  • 3、将ID=1的年龄增加1
    • 版本需3.5.6+
    • setIncrBy:数值字段进行增量操作
    • setDecrBy:数值字段进行减量操作
lambdaUpdateWrapper.setIncrBy(Student::getAge, 1);
// 等价于
UPDATE student SET age = age + 1

删除

  • 1、删除ID=1
int result = studentMapper.delete(new LambdaQueryWrapper<Student>()
        .eq(Student::getId, 1)
);
  • 2、删除 age 为空的学生
int result = studentMapper.delete(new LambdaQueryWrapper<Student>()
        .isNull(Student::getAge)
);

常用注解

  • @TableName
    • 解决实体类名称与数据库表名不一致
@TableName("t_user")
public class User implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}
  • @TableId
    • 标识主键
    • 下面例子中,我们将 ID 标识为主键,并且他是 自增 的。默认是 IdType.ASSIGN_ID 代表使用 UUID
public class User implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}
  • @TableField
    • 解决实体类属性与数据库字段不同
    • 下面例子中,数据库的字段是 username
public class User implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    @@TableField("username")
    private String name;
    private Integer age;
    private String email;
}
  • @TableLogic
    • 将字段标记为逻辑删除吗,意思是不会在数据库中删除数据,但是查询不到。

先在数据库中添加字段,然后我们再次查询会发现以及查询不到了。

ALTER TABLE user
ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0;

# 测试删除功能, 假装被删除
UPDATE user SET is_deleted=1 WHERE id=1 AND is_deleted=0
public class User implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic
    private Integer isDeleted;
}

分页

设置分页(版本低于 3.5 可能会报错)

配置

@Configuration
@MapperScan("com.exam.demo.mapper")
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
        //interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
        return interceptor;
    }
}

简单分页

属性名类型默认值描述
recordsListemptyList查询数据列表
totalLong0查询列表总记录数
sizeLong10每页显示条数,默认 10
currentLong1当前页
ordersListemptyList排序字段信息
optimizeCountSqlbooleantrue自动优化 COUNT SQL
optimizeJoinOfCountSqlbooleantrue自动优化 COUNT SQL 是否把 join 查询部分移除
searchCountbooleantrue是否进行 count 查询
maxLimitLong单页分页条数限制
countIdStringXML 自定义 count 查询的 statementId
  • selectPage:第二个参数是 Wrapper,你可以传递条件。
  • 这种方式适用于单表查询, 如果你需要用到多表查询请看下一节
// 设置分页参数, 第二页 每页 2 条数据
Page<Student> page = new Page<>(2, 2);
studentMapper.selectPage(page, null);
// 获取分页数据
List<Student> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页显示的条数:" + page.getSize());
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("是否有上一页:" + page.hasPrevious());

分页多表查询

Page 是继承于 IPage 的,实现了简单分页模型。如果你需要实现自己的分页模型,可以继承 Page 类或实现 IPage 类。

  • 自定义 mapper
IPage<SysUserInfoVO> selectSysUserInfo(Page<?> page, @Param("userQueryDTO") SysUserQueryDTO userQueryDTO);
// IPage 和 Page 都可以
Page<SysUserInfoVO> selectSysUserInfo(Page<?> page, @Param("userQueryDTO") SysUserQueryDTO userQueryDTO);

XML

    <resultMap id="SysUserInfoResultMap" type="com.eezd.main.web.system.vo.SysUserInfoVO">
        <id column="user_id" property="userId"/>
        <result column="role_id" property="roleId"/>
        <result column="role_name" property="roleName"/>
        <result column="role_key" property="roleKey"/>
        <result column="role_status" property="roleStatus"/>
        <result column="username" property="username"/>
        <result column="nickname" property="nickname"/>
        <result column="email" property="email"/>
        <result column="phonenumber" property="phonenumber"/>
        <result column="sex" property="sex"/>
        <result column="avatar" property="avatar"/>
        <result column="status" property="status"/>
        <result column="login_ip" property="loginIp"/>
        <result column="login_date" property="loginDate"/>
        <result column="create_by" property="createBy"/>
        <result column="create_time" property="createTime"/>
        <result column="update_by" property="updateBy"/>
        <result column="update_time" property="updateTime"/>
        <result column="remark" property="remark"/>
    </resultMap>

    <select id="selectSysUserInfo" resultMap="SysUserInfoResultMap">
        SELECT u.*, r.role_key, r.role_name, r.status as role_status
        FROM sys_user u
        LEFT JOIN sys_role r ON u.role_id = r.role_id
        WHERE u.del_flag = 0
        <if test="userQueryDTO.username != null">
            AND u.username LIKE CONCAT('%', #{userQueryDTO.username}, '%')
        </if>
        <if test="userQueryDTO.nickname != null">
            AND u.nickname LIKE CONCAT('%', #{userQueryDTO.nickname}, '%')
        </if>
        <if test="userQueryDTO.email != null">
            AND u.email LIKE CONCAT('%', #{userQueryDTO.email}, '%')
        </if>
        <if test="userQueryDTO.phonenumber != null">
            AND u.phonenumber LIKE CONCAT('%', #{userQueryDTO.phonenumber}, '%')
        </if>
        <if test="userQueryDTO.sex != null">
            AND u.sex = #{userQueryDTO.sex}
        </if>
        <if test="userQueryDTO.status != null">
            AND u.status = #{userQueryDTO.status}
        </if>
    </select>

SysUserInfoVO

@Data
public class SysUserInfoVO implements Serializable {
    private Long userId;

    private Long roleId;

    private String roleName;

    private String roleKey;

    private Long roleStatus;

    private String username;

    private String nickname;

    private String email;

    private String phonenumber;

    private String sex;

    private String avatar;

    private String status;

    private String loginIp;

    private Date loginDate;

    private String createBy;

    private Date createTime;

    private String updateBy;

    private Date updateTime;

    private String remark;
}

DTO

@ApiModel(value = "用户查询DTO")
@Data
public class SysUserQueryDTO {
    @ApiModelProperty(value = "用户ID", position = 1)
    private Long userId;

    @ApiModelProperty(value = "角色ID", position = 2, example = "2")
    private Long roleId;

    @ApiModelProperty(value = "用户账号", position = 3, example = "test")
    private String username;

    @ApiModelProperty(value = "用户昵称", position = 4, example = "测试员")
    private String nickname;

    @ApiModelProperty(value = "用户邮箱", position = 5, example = "test@qq.com")
    private String email;

    @ApiModelProperty(value = "手机号码", position = 6, example = "15888888889")
    private String phonenumber;

    @ApiModelProperty(value = "用户性别(0男 1女 2未知)", position = 7, example = "2")
    private String sex;

    @ApiModelProperty(value = "帐号状态(0正常 1停用)", position = 10, example = "0")
    private String status;
}

调用

Integer pageNum = Convert.toInt(ServletUtils.getParameter("pageNum"), 1);
Integer pageSize = Convert.toInt(ServletUtils.getParameter("pageSize"), 10);
Page<?> page = new Page<>(pageNum, pageSize);

IPage<SysUserInfoVO> pageResult = studentMapper.selectSysUserInfo(page, userQueryDTO);
List<SysUserInfoVO> list = pageResult.getRecords();
long total = page.getTotal();

  • 例如:原本价格是 100 元,A 加 50 元,B 取 30 元。

  • 正常情况:A 和 B 同时操作,如果 A 先提交,结果 150 元,然后 B 再次提交就是 100-30,而不是 150-30。

  • 乐观锁:A 和 B 同时操作,如果 A 先提交,那么 B 则会取 A 提交的结果,150-30。

  • 悲观锁:一条数据只能被一个人操作,当 A 在操作的时候 B 不能进行操作。

乐观锁的插件配置

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 如果配置多个插件,切记分页最后添加
    // interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
    // 添加乐观锁插件
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}
  • 我们创建一个数据表试试
    • 必须存在 VERSION 字段, 用来保存乐观锁的版本号
CREATE TABLE product
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称',
price INT(11) DEFAULT 0 COMMENT '价格',
VERSION INT(11) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id)
);

INSERT INTO product (id, NAME, price) VALUES (1, '外星人笔记本', 100);
  • 实体,添加 @Version 注解。
    • 表示乐观锁
@Data
public class Product {
    private Long id;
    private String name;
    private Integer price;
    @Version
    private Integer version;
}

mapper

public interface ProductMapper extends BaseMapper<Product> {
}

测试

// A 取数据
Product p1 = productMapper.selectById(1L);
// B 取数据
Product p2 = productMapper.selectById(1L);
// A 修改 + 50
p1.setPrice(p1.getPrice() + 50);
int result1 = productMapper.updateById(p1);
System.out.println("A 修改的结果:" + result1);   // 1 修改成功
// B 修改 - 30
p2.setPrice(p2.getPrice() - 30);
int result2 = productMapper.updateById(p2);
System.out.println("B 修改的结果:" + result2);   // 0 修改错误
if (result2 == 0) {
// 失败重试,重新获取version并更新
    p2 = productMapper.selectById(1L);
    p2.setPrice(p2.getPrice() - 30);
    result2 = productMapper.updateById(p2);
}
System.out.println("A 修改重试的结果:" + result2); // 1 重试
// 老板看价格
Product p3 = productMapper.selectById(1L);
System.out.println("B 看价格:" + p3.getPrice());   // 120
Licensed under CC BY-NC-SA 4.0
最后更新于 Nov 22, 2024 00:00 UTC
本博客已稳定运行
发表了53篇文章 · 总计28.17k字
使用 Hugo 构建
主题 StackJimmy 设计