Java之Mybatis-Plus

概述

MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发提高效率而生。MyBatis-Plus提供了通用的mapper和service,可以在不编写任何SQL语句的情况下,快速的实现对单表的CRUD、批量、逻辑州除、分页等操作。本文从MyBatis- Plus的特性及使用,到MyBatis-Pus所提供的优秀的插件,以及多数据源的配置都有详细的讲解。
本文主要以MysQL数据库为案例,使用ldea作为IDE,使用Maven作为构建工具,使用Spring Boot展示MyBatis-Plus的各个功能,所以需要有MyBatis和Spring Boot的基础

内容

image.png

官网

https://baomidou.com
image.png

特性

官网地址:https://baomidou.com/pages/24112f/#%E7%89%B9%E6%80%A7

支持数据库

任何能使用 MyBatis 进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。

  • MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库

框架结构

image.png
第一步:Scan Entity扫描实体,根据反射技术对实体类中的属性进行抽取;
第二步:分析表与实体类中的关系,以及表字段与实体属性之间的关系;
第三步:根据当前调用的方法来生成对应的SQL语句,再将生成的SQL语句注入到MyBatis的容器中

Quick Start

环境介绍

IDE:idea
JDK:JDK8
构建工具:Maven
数据库:MySQL:8.0.29
SpringBoot:2.7.7
MyBatis-Plus:3.5.1

数据库

-- 创建MyBatis-Plus测试数据库
CREATE DATABASE `mybatis_plus`;

USE `mybatis_plus`;

-- 创建测试user表
DROP TABLE IF EXISTS `user`;
CREATE TABLE IF NOT EXISTS `user`(
`id`	BIGINT(20) NOT NULL COMMENT '主键',
`name` VARCHAR(30) DEFAULT NULL COMMENT'姓名',
`age` INT(11) DEFAULT NULL COMMENT '年龄',
`email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY(`id`)
);

-- 添加测试数据
INSERT INTO `user` (id,name,age,email) VALUES
(1,'Jone',18,'test1@test.com'),
(2,'Jack',20,'test2@test.com'),
(3,'Tom',28,'test3@test.com'),
(4,'Sandy',21,'test4@test.com'),
(5,'Billie',24,'test5@test.com');

MyBatis-Plus在数据插入时,默认会使用雪花算法来生成id,所以采用bigint类型

创建工程

image.png

pom依赖
<?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 https://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>2.7.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.xiu</groupId>
    <artifactId>mybatis-plus-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybatis-plus-demo</name>
    <description>mybatis-plus-demo</description>
    <properties>
        <java.version>1.8</java.version>
    </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>
            <scope>test</scope>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

MyBatis-Plus的依赖

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

image.png
** 注意:**
1、驱动类spring.datasource.driver-class-name:
SpringBoot2.0(内置jdbc5驱动),驱动类使用:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
SpringBoot2.1及以上(内置jdbc8驱动),驱动类使用:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
若配置与版本不对应,会有警告信息!!!
2、连接地址spring.datasource.url
MySQL5.7版本的url:
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=UTF-8&useSSL=false
MySQL8.0版本的url:
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimeZone=GMT%2B&characterEncoding=UTF-8&useSSL=false
否则运行时,会报错:java.sql.SQLException:The server time zone value…
当前MySQL驱动为8版本的且SpringBoot为2.7.7

application.properties 配置文件
#端口
server.port=8080

#数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimeZone=GMT%2B&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=19960327xiu
实体类

MyBatis中有ORM的概念,对象关系映射,表中字段要与实体进行映射。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author zhangzengxiu
 * @date 2022/12/24
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 4916508803893379409L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    private String email;

}
Mapper接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiu.mybatisplusdemo.pojo.User;

/**
 * @author zhangzengxiu
 * @date 2022/12/24
 */
public interface UserMapper extends BaseMapper<User> {
}

说明:
BaseMapper是由MyBatis-Plus提供的通用的Mapper,包含了对单表的各种CRUD操作

启动类
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.xiu.mybatisplusdemo.mapper")
@SpringBootApplication
public class MybatisPlusDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusDemoApplication.class, args);
    }

}

说明:
@MapperScan注解是用来扫描,对应包下的所有Mapper接口, 所动态生成的代理类交给IOC容器进行管理
在这里插入图片描述

测试用例

image.png
报错原因:
@MapperScan注解是用来扫描,对应包下的所有Mapper接口, 所动态生成的代理类交给IOC容器进行管理。并不是将接口交给IOC容器进行管理,IOC容器中只能存在类所对应的Bean,而不能存在接口所对应的Bean,所以是将UserMapper动态生成的代理类放进IOC容器中,IDE在编译时, 认为UserMapper无法自动装配,但是,运行时不会有问题。
若强迫症,不希望看到红色波浪线,可以在UserMapper的接口上添加一个注解:@Repository,标识为持久层组件。
即可解决。

import com.xiu.mybatisplusdemo.mapper.UserMapper;
import com.xiu.mybatisplusdemo.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;

/**
 * @author zhangzengxiu
 * @date 2022/12/24
 */
@SpringBootTest
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    /**
     * 测试查询列表
     * 有条件:构造queryWrapper
     * 无条件传入:null
     */
    @Test
    public void testQueryList() {
        //null表示没有条件,若有条件,需要构造queryWrapper
        List<User> userList = userMapper.selectList(null);
        userList.forEach(System.out::println);
    }


}

之前我们需要在接口中写方法,并在对应的映射文件中编写SQL语句,但是现在我们仅需要使用MyBatis-plus为我们提供的方法即可。
测试结果:
image.png
测试成功!
当前我们只是根据extends BaseMapper设置了User的范型,即找到了对应的user表。
将user表中的字段赋值给了User类中对应的属性。
BaseMapper中存在各种各样的方法:
image.png

加入日志

当前我们只看到了MyBatis-Plus为我们提供的方法,即结果,如果要查看实际操作所生成的SQL语句,需要添加日志。
在application.properties中添加配置即可。

#日志配置
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

再次执行测试用例
image.png
然而,我们当前并没有指明我们要操作的表,以及表中的字段和实体类中属性的对应关系。
因为,当前我们的表名与类名刚好一致,表中的字段与属性刚好一一对应,完全匹配。
image.png
该图中显示:先去扫描实体,然后反射抽取实体类中属性,再分析表及表字段,再生成SQL注入容器。
所以,我们当前要操作的表及表字段由实体类及属性来决定

BaseMapper

源码:

/**
 * Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
 * <p>这个 Mapper 支持 id 泛型</p>
 *
 * @author hubin
 * @since 2016-01-23
 */
public interface BaseMapper<T> extends Mapper<T> {

    /**
     * 插入一条记录
     *
     * @param entity 实体对象
     */
    int insert(T entity);

    /**
     * 根据 ID 删除
     *
     * @param id 主键ID
     */
    int deleteById(Serializable id);

    /**
     * 根据实体(ID)删除
     *
     * @param entity 实体对象
     * @since 3.4.4
     */
    int deleteById(T entity);

    /**
     * 根据 columnMap 条件,删除记录
     *
     * @param columnMap 表字段 map 对象
     */
    int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

    /**
     * 根据 entity 条件,删除记录
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
     */
    int delete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 删除(根据ID或实体 批量删除)
     *
     * @param idList 主键ID列表或实体列表(不能为 null 以及 empty)
     */
    int deleteBatchIds(@Param(Constants.COLLECTION) Collection<?> idList);

    /**
     * 根据 ID 修改
     *
     * @param entity 实体对象
     */
    int updateById(@Param(Constants.ENTITY) T entity);

    /**
     * 根据 whereEntity 条件,更新记录
     *
     * @param entity        实体对象 (set 条件值,可以为 null)
     * @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
     */
    int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);

    /**
     * 根据 ID 查询
     *
     * @param id 主键ID
     */
    T selectById(Serializable id);

    /**
     * 查询(根据ID 批量查询)
     *
     * @param idList 主键ID列表(不能为 null 以及 empty)
     */
    List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

    /**
     * 查询(根据 columnMap 条件)
     *
     * @param columnMap 表字段 map 对象
     */
    List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

    /**
     * 根据 entity 条件,查询一条记录
     * <p>查询一条记录,例如 qw.last("limit 1") 限制取一条记录, 注意:多条数据会报异常</p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    default T selectOne(@Param(Constants.WRAPPER) 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");
            }
            return ts.get(0);
        }
        return null;
    }

    /**
     * 根据 Wrapper 条件,判断是否存在记录
     *
     * @param queryWrapper 实体对象封装操作类
     * @return
     */
    default boolean exists(Wrapper<T> queryWrapper) {
        Long count = this.selectCount(queryWrapper);
        return null != count && count > 0;
    }

    /**
     * 根据 Wrapper 条件,查询总记录数
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    Long selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 根据 entity 条件,查询全部记录
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 根据 Wrapper 条件,查询全部记录
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 根据 Wrapper 条件,查询全部记录
     * <p>注意: 只返回第一个字段的值</p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 根据 entity 条件,查询全部记录(并翻页)
     *
     * @param page         分页查询条件(可以为 RowBounds.DEFAULT)
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    <P extends IPage<T>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 根据 Wrapper 条件,查询全部记录(并翻页)
     *
     * @param page         分页查询条件
     * @param queryWrapper 实体对象封装操作类
     */
    <P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}

新增功能

    /**
     * 测试新增功能
     */
    @Test
    public void testInsert() {
        User user = new User(null, "zhangsan", 20, "zhangsan@test.com");
        int count = userMapper.insert(user);
        //获取自动生成的id
        System.out.println("id=" + user.getId());
        Assert.assertEquals(1, count);
    }

测试结果:
image.png
数据信息:
image.png
使用断言,添加了pom

        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

删除功能

根据id进行删除
    /**
     * 测试根据id进行删除
     */
    @Test
    public void testDeleteById() {
        //DELETE FROM user WHERE id=?
        int count = userMapper.deleteById(1606668345558142977L);
        Assert.assertEquals(1, count);
    }

测试结果:
image.png

根据map进行删除
    /**
     * 根据map集合中设置的条件进行删除
     * map中是条件
     */
    @Test
    public void testDeleteByMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "zhangsan");
        map.put("age", 20);
        //DELETE FROM user WHERE name = ? AND age = ?
        int count = userMapper.deleteByMap(map);
        Assert.assertEquals(1, count);
    }

测试结果:
image.png

根据idList批量删除
    /**
     * 测试通过多个id进行批量删除方法
     */
    @Test
    public void testDeleteBatchIds() {
        List<Long> idList = Arrays.asList(1606685497249914881L, 1606685517403557890L, 1606685581836427266L);
        int count = userMapper.deleteBatchIds(idList);
        Assert.assertEquals(idList.size(), count);
    }

测试结果:
image.png

修改功能

根据id进行修改
    /**
     * 测试根据id进行修改
     */
    @Test
    public void testUpdateById() {
        User user = new User();
        user.setId(1606686812017520641L);
        user.setName("test");
        user.setEmail("test@test.com");
        //UPDATE user SET name=?, email=? WHERE id=?
        int count = userMapper.updateById(user);
        Assert.assertEquals(1, count);
    }

测试结果:
image.png

查询功能

根据id进行查询
    /**
     * 测试根据id进行查询
     */
    @Test
    public void testSelectById() {
        Long id = 1L;
        //SELECT id,name,age,email FROM user WHERE id=?
        User user = userMapper.selectById(id);
        System.out.println(user);
        Assert.assertEquals(user.getId(), id);
    }

测试结果:
image.png

根据idList查询列表
    /**
     * 测试根据id列表进行查询
     */
    @Test
    public void testSelectBatchIds() {
        List<Long> idList = Arrays.asList(1L, 2L, 3L);
        //SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? )
        List<User> userList = userMapper.selectBatchIds(idList);
        userList.forEach(System.out::println);
        Assert.assertEquals(idList.size(), userList.size());
    }

测试结果:
image.png

根据Map进行查询
    /**
     * 测试根据map进行查询
     */
    @Test
    public void testSelectByMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "Jack");
        map.put("age", 20);
        //SELECT id,name,age,email FROM user WHERE name = ? AND age = ?
        List<User> userList = userMapper.selectByMap(map);
        userList.forEach(System.out::println);
    }

测试结果:
image.png

查询列表
    /**
     * 测试查询列表
     */
    @Test
    public void testSelectList() {
        //SELECT id,name,age,email FROM user
        List<User> userList = userMapper.selectList(null);
        userList.forEach(System.out::println);
    }

测试结果:
image.png
注意:若没有条件,则queryWrapper参数传null,删除和修改也可这样操作,但是,一般不会修改或删除全部数据。

自定义功能

MyBatis只是对我们原来的MyBatis功能进行增强,而不会影响原本的功能。
若BaseMapper提供的功能,无法满足我们的需求,我们可通过编写映射文件来实现。
我们可以配置mybatis-plus.config-location属性的值,来指定mapper文件位置。
但是,mybatis-plus.config-location有默认配置:classpath*:/mapper/**/*.xml
也就是资源目录下的mapper文件夹内,可以里面再创建多层文件目录。

根据id查询Map

UserMapper接口:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiu.mybatisplusdemo.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.Map;

/**
 * @author zhangzengxiu
 * @date 2022/12/24
 */
@Repository
public interface UserMapper extends BaseMapper<User> {

    /**
     * 根据id查询map
     *
     * @param id
     * @return
     */
    Map<String, Object> selectMapById(@Param("id") Long id);
}

映射配置文件:
image.png

<?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.xiu.mybatisplusdemo.mapper.UserMapper">

    <!--根据id查询map-->
    <select id="selectMapById" resultType="java.util.Map">
        SELECT
            id,
            name,
            age,
            email
        FROM
            `user`
        WHERE
            id=#{id}
    </select>

</map>

单测:

    /**
     * 测试自定义方法:根据id查询Map
     */
    @Test
    public void testSelectMapById() {
        Long id = 1L;
        //SELECT id, name, age, email FROM `user` WHERE id=?
        Map<String, Object> map = userMapper.selectMapById(id);
        System.out.println(map);
    }

测试结果:
image.png

IService

官网地址:https://baomidou.com/pages/49cc81/#service-crud-%E6%8E%A5%E5%8F%A3

说明:

  • 通用 Service CRUD 封装IService(opens new window)接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper 为 条件构造器

MyBatis-Plus中有一个接口IService和其实现类ServiceImpl,封装了常见的业务逻辑。
IService
saveOrUpdate的操作区别:
如果没有id,则为新增,如果有id,则说明是修改。

源码

IService:

public interface IService<T> {
    //T是实体对象
}

ServiceImpl:

/**
 * IService 实现类( 泛型:M 是 mapper 对象,T 是实体 )
 *
 * @author hubin
 * @since 2018-06-23
 */
@SuppressWarnings("unchecked")
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
    //M:自己写的Mapper
    //T:实体对象
}

注意:
ServiceImpl大概率不能满足我们的需求,不建议使用,我们可以自己编写接口,然后继承IService

接口

image.png
当前UserServiceImpl实现了UserService,但是UserService接口继承了IService,所以需要重写所有的IService中的方法,但实际并不需要,所以UserServiceImpl只需要再即成MyBatis-Plus提供的ServiceImpl即可。

实现类

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
	//UserMapper是我们要操作的Mapper
    //User是要操作的实体类
}

如此,我们便可以使用通用的功能,也可以使用我们自定义的功能。

查询记录总数功能

    /**
     * 测试查询总count数,不带条件
     */
    @Test
    public void testGetCount() {
        //SELECT COUNT( * ) FROM user
        long count = userService.count();
        System.out.println(count);
    }

测试结果:
image.png

批量添加功能

BaseMapper中并未提供批量添加的方法,但是IService中提供了saveBatch方法,但是该方法,也是通过单个添加,循环来实现。

    /**
     * 测试批量添加的功能
     */
    @Test
    public void testInsertMore() {
        List<User> entityList = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            User user = new User();
            user.setName("test" + i);
            user.setAge(10 + i);
            entityList.add(user);
        }
        // INSERT INTO user ( id, name, age ) VALUES ( ?, ?, ? )
        boolean res = userService.saveBatch(entityList);
        Assert.assertEquals(true, res);
    }

测试结果:
image.png
通过单个添加,循环实现的批量添加。

注解

@TableName

之前,我们并未指定表名,便可进行操作,说明操作的表是由BaseMapper设置的范型来决定的。
如果,我们的表名与我们的实体类的类名不一致,在操作时就会报错:
如果我们将数据库中的表名从user改为t_user,再次执行测试用例:
image.png
我们可以通过在实体类上添加@TableName来指定数据库中的表名

@TableName("t_user")    
public class User implements Serializable {
    ...
}

再次执行测试用例查看 :
image.png
但是,如果我们每张表名前全部都有一样的前缀就会很繁琐。
我们可以通过配置MyBatis-Plus的全局配置,指定所有实体类表名默认的前缀。

#全局配置实体类对应表名的统一前缀
mybatis-plus.global-config.db-config.table-prefix=t_

image.png

@TableId

MyBatis-Plus默认会将id作为主键,但是如果我们的实体属性是uid,表字段也是uid,这样去执行方法时,会报错,因为,MyBatis-Plus默认是将id作为主键的。如果uid会指定为主键,那么其他属性是不是也会指定为主键。
我们可以通过@TableId 将这个属性所对应的字段指定为主键
示例:

@TableName("t_user") //指定数据库中的表名
public class User implements Serializable {
    private static final long serialVersionUID = 4916508803893379409L;

    /**
     * 主键
     * TableId是将这个属性所对应的字段指定为主键
     */
    @TableId
    private Long id;

    ...
}

@TableId注解源码:

/**
 * 表主键标识
 *
 * @author hubin
 * @since 2016-01-23
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {

    /**
     * 字段名(该值可无)
     */
    String value() default "";

    /**
     * 主键类型
     * {@link IdType}
     */
    IdType type() default IdType.NONE;
}
value属性

若我们的实体类中的主键属性与表字段不一致,实体类中主键是id,表字段是uid,则无法形成对应。此时执行方法也会报错。
image.png
当我们去操作id这个属性时,对应的字段也是id,表中没有对应的字段就会报错。
我们可通过@TableId注解的value属性来指定表中表示主键的字段名。

@TableName("t_user") //指定数据库中的表名
public class User implements Serializable {
    private static final long serialVersionUID = 4916508803893379409L;

    /**
     * 主键
     * TableId是将这个属性所对应的字段指定为主键
     * value来指定表中对应的主键字段
     */
    @TableId(value = "id")
    private Long id;

    ...
}

image.png

@TableId的value属性时用于指定主键的字段
如果只设置value的属性,则可以省略value=,因为默认设置的就是value属性
@TableId(“id”) 即可。

type属性

表示主键生成的策略,MyBatis-Plus中id默认生成主键的策略是雪花算法。
如果我们希望主键是自增的而不是通过雪花算法生成,首先需要设置数据库中的主键字段是主键自动自增。

-- 清空表数据
TRUNCATE TABLE t_user;

-- 指定表主键自增
ALTER TABLE t_user MODIFY id BIGINT AUTO_INCREMENT;

我们之前的主键是雪花算法生成然后再赋值给id字段,我们使用主键自增并没有生成再赋值,而是直接自增生成。

@TableName("t_user") //指定数据库中的表名
public class User implements Serializable {
    private static final long serialVersionUID = 4916508803893379409L;

    /**
     * 主键
     * TableId是将这个属性所对应的字段指定为主键
     * value来指定表中对应的主键字段
     * type指定主键生成策略
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    ...
}

再次测试:
image.png
此时,id并不是先通过雪花算法生成再插入数据库的,而是通过数据库主键自增生成的。
type的其他选项:

/**
 * 生成ID类型枚举类
 *
 * @author hubin
 * @since 2015-11-10
 */
@Getter
public enum IdType {
    /**
     * 数据库ID自增
     * <p>该类型请确保数据库设置了 ID自增 否则无效</p>
     */
    AUTO(0),
    /**
     * 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
     */
    NONE(1),
    /**
     * 用户输入ID
     * <p>该类型可以通过自己注册自动填充插件进行填充</p>
     */
    INPUT(2),

    /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
    /**
     * 分配ID (主键类型为number或string),
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
     *
     * @since 3.3.0
     */
    ASSIGN_ID(3),
    /**
     * 分配UUID (主键类型为 string)
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
     */
    ASSIGN_UUID(4);

    private final int key;

    IdType(int key) {
        this.key = key;
    }
}

说明:
默认的是ASSIGN_ID:基于雪花算法生成,与数据库id是否设置自增无关
AUTO:使用数据库的自增,该类型必须确保数据库设置了主键自增,否则无效。

因为id作为主键,是非空的,如果不设置主键自增,我们在执行时,id没有值,就会报错。
如果我们只是设置了value=“id”,并没有指定type。
我们在添加时,设置了id=100L,此时是否还会按照type的默认雪花算法生成id。测试结果是并不会根据雪花算法生成id,而是根据设置的值,进行赋值。
如果我们想统一主键的生成策略,实现方式:
配置全局配置主键生成策略:

#全局统一配置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto

image.png
配置的属性值与@TableId注解的值选项IdType类一致。
总结:image.png

雪花算法

上述主键策略默认生成是通过雪花算法生成的。

背景

随着数据规模增长,需要合适的方案去应对逐渐增长的访问压力和数据量。
数据库拓展:业务分库、主从复制(读写分离)、数据库分表。

数据库分表

在不同业务数据分散到不同的数据库服务器中,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如:淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定无法满足性能要求,此时就需要对单表数据进行拆分。
单表数据拆分的两种方式:垂直分表和水平分表。
示意图:
image.png

垂直分表

垂直分表适合将表中某些不常用旦古了大量空间的列拆分出去。
例如,前面示意因中的nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和sex两个宇段进行查询,而 nicknamne 和description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和sex 时,就能带来一定的性能提升。

水平分表

水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。
但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶顽或者隐患
水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理

主键id
  • 主键自增

◎以最常见的用户 ID 为例,可以按照 1000000 的范国大小进行分段,1-999999 放到表1中,1000000-
1999999 放到表2中,以此类推。
②复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。
③优点:可以随着数据的增加平滑地扩 充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。
④缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有1条,而另外
段实际存储的数据量有1000 万条。

  • 取模

◎同样以用户 1D 为例,假如我们一开始就规划了 10 个数据库表,可以简单地用 user _id % 10的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID为 10086 的用户放到编号为 6 的子表中。
②复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。
③优点:表分布比较均匀。
④缺点:扩充新的表很麻烦,所有数据都要重分布,
优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率极高。image.png

@TableField

之前@TableId注解主要是用来处理主键的映射关系,用来设置主键,以及主键的生成策略。
若其他属性与数据库表字段名不一致。
情况一:
实体类属性:userName 小驼峰命名
表字段:user_name 下划线命名
此时并不需要额外配置,MyBatis-Plus默认配置下划线转驼峰。
情况二:
实体类属性:name
表字段:user_name 下划线命名
此时,属性与字段名不匹配,且不满足下划线转驼峰。运行会报错:
image.png
我们可以通过@TableField注解来指定属性所对应的数据库中的表字段。
示例:

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * @author zhangzengxiu
 * @date 2022/12/24
 */
@TableName("t_user") //指定数据库中的表名
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 4916508803893379409L;

    /**
     * 主键
     * TableId是将这个属性所对应的字段指定为主键
     * value来指定表中对应的主键字段
     * type指定主键生成策略
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     * TableField用来指定数据库中对应的字段名
     */
    @TableField("name")
    private String name;
    
    ...
}

注意:
@TableId与@TableField区别:
@TableId设置的是主键相关的配置关系
@TableField设置的是普通非主键的配置关系

@TableLogic

  • 逻辑删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
  • 物理删除:假删除,将对应的数据代表删除字段的状态修改为“被删除的状态”,之后在数据库中仍然可以查看此条数据记录。
  • 使用场景:进行数据恢复

比如:is_deleted字段表示该数据是否删除,0表示未删除,1表示已删除。其实本质上就是修改操作。

逻辑删除实现

在表中新添加一个新的字段:is_deleted

 -- 添加逻辑删除字段is_deleted
ALTER TABLE t_user ADD is_deleted INT(2) DEFAULT 0 COMMENT '逻辑删除,0未被删除,1已删除';

实体类中添加对应属性:

    /**
     * 是否被删除
     */
    @TableField("is_deleted")
    @TableLogic //表示逻辑删除字段
    private Integer isDeleted;

测试:
image.png
其本质就是修改操作。
数据库表数据:
image.png
再次执行查询操作:
image.png
对应的SQL添加了条件,is_deleted=0。
结果并没有被逻辑删除的数据。
如果,表名与属性名不一致时,我们使用@TableField或者@TableId注解,MyBatis-Plus会通过别名的形式,形成映射关系:
image.png
我们当前的删除操作会变成修改操作,查询操作会变成查询未被逻辑删除的数据,而不需要手动去指定

条件构造器

条件构造器就是用来封装我们当前的条件的。增删改查的SQL语句中,删除、修改、查询是可以添加条件的,我们可以通过Wrapper来构造条件。

Wrapper

image.png
image.png

QueryWrapper

组装查询条件

示例:

    /**
     * 查询条件:
     * 用户名包含a,
     * 年龄在20-30之间,
     * email不为null
     */
    @Test
    public void testSelectList() {
        //范型指定为实体类类型
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.like("name", "a").between("age", 20, 30).isNotNull("email");
        //SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }

结果:
image.png

组装排序条件

示例:

    /**
     * 查询条件:
     * 查询用户信息,按照年龄的降序排序,若年龄相同,则按照id升序排序
     */
    @Test
    public void testSelectList02() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 ORDER BY age DESC,id ASC
        wrapper.orderByDesc("age").orderByAsc("id");
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }

结果:
image.png

组装删除条件

删除此次使用的是QueryWrapper条件构造器
示例:

    /**
     * 删除条件:
     * 删除email为null的user
     */
    @Test
    public void testDel() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.isNull("email");
        //UPDATE t_user SET is_deleted=1 WHERE is_deleted=0 AND (email IS NULL)
        int delete = userMapper.delete(wrapper);
        System.out.println("res = " + delete);
    }

结果:
image.png

组装修改条件

示例:

    /**
     * 修改条件:
     * 将年龄大于20并且用户名中包含a或者邮箱为null的用户信息修改
     */
    @Test
    public void testUpdate() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.gt("age", 20).like("name", "a").or().isNull("email");
        User user = new User();
        user.setAge(44);
        user.setEmail("test@tt.com");
        //UPDATE t_user SET age=?, email=? WHERE is_deleted=0 AND (age > ? AND name LIKE ? OR email IS NULL)
        int update = userMapper.update(user, wrapper);
        System.out.println("res = " + update);
    }

结果:
image.png

备注:
update方法,两种用法:
用法一使用QueryWrapper:参数一为要修改的字段,参数二为条件。
用法二使用UpdateWrapper:参数一为null,参数二为条件+要修改的值。

条件优先级

举例:将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
当前需求中,括号中的条件为优先判断条件。
当我们使用了and或者or方法,如果使用了lambda表达式,则lambda优先执行。
示例:

	/**
     * 修改条件:
     * 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
     */
    @Test
    public void testUpdate2() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //wrapper.gt("age", 20).like("name", "a").or().isNull("email");
        wrapper.like("name", "a").and(x -> x.gt("age", 20).or().isNull("email"));
        User user = new User();
        user.setName("aaa");
        user.setEmail("test@tt.com");
        //UPDATE t_user SET name=?, email=? WHERE is_deleted=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
        int update = userMapper.update(user, wrapper);
        System.out.println("res = " + update);
    }

结果:
image.png

组装select语句

如果我们只需要查看部分字段即可,而不需要每次都返回全部字段。
示例:

    /**
     * 只返回部分字段
     */
    @Test
    public void testSelect() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.select("name", "age", "email");
        //SELECT name,age,email FROM t_user WHERE is_deleted=0
        List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
        maps.forEach(System.out::println);
    }

结果:
image.png

组装子查询

示例:
SQL语句:

USE `mybatis_plus`;
SELECT id FROM t_user WHERE id<=100;

SELECT * FROM t_user WHERE id IN (SELECT id FROM t_user WHERE id<=100);

代码:

    /**
     * 子查询
     */
    @Test
    public void testChildSelect() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.inSql("id", "SELECT id FROM t_user WHERE id<=100");
        //SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (id IN (SELECT id FROM t_user WHERE id<=100))
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }

结果:
image.png

UpdateWrapper

修改功能

UpdateWrapper不仅仅可以设置修改的条件,还可以设置修改的字段
示例:

    /**
     * 根据updateWrapper来修改用户信息
     * 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
     */
    @Test
    public void testUpdateByUpdateWrapper() {
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.like("name", "a").and(x -> x.gt("age", 20).or().isNull("email"));
        updateWrapper.set("name", "111").set("email", "111@111.com");
        //UPDATE t_user SET name=?,email=? WHERE is_deleted=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
        int res = userMapper.update(null, updateWrapper);
        System.out.println("res = " + res);
    }

结果:
image.png

模拟实际开发中的条件组装

示例:

    /**
     * 模拟实际开发中的条件
     */
    @Test
    public void testQuery() {
        String name = "";
        Integer ageBegin = 10;
        Integer ageEnd = 30;
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        if (StringUtils.isNotBlank(name)) {
            wrapper.like("name", name);
        }
        if (ageBegin != null) {
            wrapper.ge("age", ageBegin);
        }
        if (ageEnd != null) {
            wrapper.le("age", ageEnd);
        }
        //SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }

结果:
image.png

condition组装条件

当前存在的问题,每个条件都需要手动去判断、组装,很繁琐。
示例:

    /**
     * 根据condition条件查询
     */
    @Test
    public void testQueryByCondition() {
        String name = "";
        Integer ageBegin = 10;
        Integer ageEnd = 30;
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.like(StringUtils.isNotBlank(name), "name", name);
        wrapper.ge(ageBegin != null, "age", ageBegin);
        wrapper.le(ageEnd != null, "age", ageEnd);
        //SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }

结果:
image.png

LambdaQueryWrapper

当前存在的另外一个问题:我们在字符串内手动写字段名,写错了,字符串也不会有任何提示。
我们要访问的字段直接访问实体类对应的属性即可,无需手动去写字段名了,避免出错。
示例:

    /**
     * 根据LambdaQueryWrapper条件查询
     */
    @Test
    public void testQueryByLambda() {
        String name = "";
        Integer ageBegin = 10;
        Integer ageEnd = 30;
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(StringUtils.isNotBlank(name), User::getName, name);
        wrapper.ge(ageBegin != null, User::getAge, ageBegin);
        wrapper.le(ageEnd != null, User::getAge, ageEnd);
        //SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }

结果:

image.png

LambdaUpdateWrapper

示例:

    /**
     * 根据LambdaUpdateWrapper来修改用户信息
     * 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
     */
    @Test
    public void testUpdateByLambdaUpdateWrapper() {
        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.like(User::getName, "a").and(x -> x.gt(User::getAge, 20).or().isNull(User::getEmail));
        updateWrapper.set(User::getName, "111").set(User::getEmail, "111@111.com");
        //UPDATE t_user SET name=?,email=? WHERE is_deleted=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
        int res = userMapper.update(null, updateWrapper);
        System.out.println("res = " + res);
    }

结果:
image.png

插件

分页插件

MyBatis-Plus自带分页插件,只需要简单配置。
因为是在查询功能进行拦截,然后再进行操作。
配置Bean:

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangzengxiu
 * @date 2023/1/2
 */
@Configuration
public class MyBatisPlusConfig {

    /**
     * 分页插件
     *
     * @return
     */
    @Bean
    public MybatisPlusInterceptor getMybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

示例:

    /**
     * 测试分页插件
     */
    @Test
    public void testPagePlugin() {
        Page<User> page = new Page<>(2, 2);
        userMapper.selectPage(page, null);
        System.out.println("记录:" + page.getRecords());
        System.out.println("总页数" + page.getPages());
        System.out.println("总记录数" + page.getTotal());
        System.out.println("是否有上一页" + page.hasPrevious());
        System.out.println("是否有下一页" + page.hasNext());
    }

结果:
image.png
自定义查询:
接口:

	/**
     * 通过年龄查询用户信息并分页
     *
     * @param page MyBatis-Plus提供的分页对象,必须放在第一个参数位置
     * @param age
     * @return
     */
    Page<User> selectUserByPage(@Param("page") Page<User> page, @Param("age") Integer age);

映射配置文件:

<!--通过年龄查询用户信息并分页-->
    <select id="selectUserByPage" resultType="com.xiu.mybatisplusdemo.pojo.User">
        SELECT
            id,
            name,
            age,
            email
        FROM
            `t_user`
        WHERE
            age > #{age}
    </select>

注意:
1、返回值必须是:Page
2、第一个参数必须是:Page

结果:
image.png

乐观锁插件

乐观锁实现流程:
数据库表中添加字段version,
去除记录时,获取当前version

SELECT id,name,price,version FROM product WHERE id = 1;

更新时,version+1,如果version的版本与之前取出来的版本不对应,则更新失败

UPDATE product SET price=price+50 ,version=version+1 WHERE id = 1 AND version=1;
环境准备

表:

USE `mybatis_plus`;
DROP TABLE IF EXISTS t_product;

CREATE TABLE IF NOT EXISTS t_product(
	id BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
	`name` VARCHAR(30) DEFAULT NULL COMMENT '商品名',
	price INT(11) DEFAULT 0 COMMENT '价格',
	version INT(11) DEFAULT 0 COMMENT '乐观锁'
);

-- 添加测试数据
INSERT INTO t_product (name,price) VALUES ("Mac",100);

代码:
接口:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiu.mybatisplusdemo.pojo.Product;
import org.springframework.stereotype.Repository;

/**
 * @author zhangzengxiu
 * @date 2023/1/2
 */
@Repository
public interface ProductMapper extends BaseMapper<Product> {
}

实体:

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * t_product表对应的实体
 *
 * @author zhangzengxiu
 * @date 2023/1/2
 */
@TableName("t_product")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {

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

    /**
     * 商品名
     */
    @TableField("name")
    private String name;

    /**
     * 价格
     */
    @TableField("price")
    private Integer price;

    /**
     * 版本号
     */
    @TableField("version")
    private Integer version;

}

单测:

    @Autowired
    private ProductMapper productMapper;

    @Test
    public void testUpdate() {
        Product productA = productMapper.selectById(1);
        System.out.println("A查询到的价格 = " + productA.getPrice());
        Product productB = productMapper.selectById(1);
        System.out.println("B查询到的价格 = " + productB.getPrice());
        //A将价格在原本基础上+50
        productA.setPrice(productA.getPrice() + 50);
        productMapper.updateById(productA);
        //B将价格在原本基础上-30
        productB.setPrice(productB.getPrice() - 30);
        productMapper.updateById(productB);
        Product productBoss = productMapper.selectById(1);
        System.out.println("Boss查询到的价格 = " + productBoss.getPrice());
    }

结果:
image.png
此时结果是70,与我们的预期不符。
修改实体类:

@TableName("t_product")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    ...
    /**
     * 版本号
     */
    @TableField("version")
    @Version
    private Integer version;

}

@Version 注解标明是一个乐观锁版本号字段。

配置乐观锁插件:

    /**
     * @return
     */
    @Bean
    public MybatisPlusInterceptor getMybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

再次执行:
此时查询到的价格为150
image.png
数据库数据:
image.png

优化修改

当前存在的问题:A操作执行完成,但是B并未执行成功,以至于最终结果不满足预期。
添加重试逻辑代码:

	@Autowired
    private ProductMapper productMapper;

    @Test
    public void testUpdate() {
        Product productA = productMapper.selectById(1);
        System.out.println("A查询到的价格 = " + productA.getPrice());
        Product productB = productMapper.selectById(1);
        System.out.println("B查询到的价格 = " + productB.getPrice());
        //A将价格在原本基础上+50
        productA.setPrice(productA.getPrice() + 50);
        productMapper.updateById(productA);
        //B将价格在原本基础上-30
        productB.setPrice(productB.getPrice() - 30);
        int res = productMapper.updateById(productB);
        if (res == 0) {
            //重试
            Product productNew = productMapper.selectById(1);
            productNew.setPrice(productNew.getPrice() - 30);
            productMapper.updateById(productNew);
        }
        Product productBoss = productMapper.selectById(1);
        System.out.println("Boss查询到的价格 = " + productBoss.getPrice());
    }

执行流程:
image.png
image.png

通用枚举

表添加性别字段:

USE `mybatis_plus`;

-- 添加性别字段
ALTER TABLE t_user ADD gender INT(1) COMMENT '性别:1男2女';

实体:

/**
 * @author zhangzengxiu
 * @date 2022/12/24
 */
@TableName("t_user") //指定数据库中的表名
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    ...
    /**
     * 性别字段
     */
    @TableField("gender")
    private GenderEnum gender;
}

性别枚举:

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author zhangzengxiu
 * @date 2023/1/2
 */
@Getter
@AllArgsConstructor
public enum GenderEnum {
    UNKNOWN(0, "未知"),
    MALE(1, "男"),
    FEMALE(2, "女");

    /**
     * 性别
     */
    private Integer gender;

    /**
     * 性别名称
     */
    private String genderName;

}

单测:

/**
 * @author zhangzengxiu
 * @date 2023/1/2
 */
@SpringBootTest
public class MyBatisPlusEnumTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void test() {
        User user = new User();
        user.setName("admin");
        user.setAge(33);
        user.setGender(GenderEnum.MALE);
        int res = userMapper.insert(user);
        Assert.assertEquals(1, res);
    }

}

执行单测:
报错:
image.png

修改

修改性别枚举:添加注解@EnumValue 标识要存到数据库中的值

@Getter
@AllArgsConstructor
public enum GenderEnum {    
    ...
    /**
     * 性别
     */
    @EnumValue
    private Integer gender;
}

添加配置:

#扫描通用枚举包
mybatis-plus.type-enums-package=com.xiu.mybatisplusdemo.enums

再次执行:
image.png
实际存到数据库的值也是对应的值。
即可。
场景:一些固定的值, Java中可以通过枚举类来实现这个功能,将指定的值添加到数据库中,我们需要通过@EnumValue注解配合包扫描来找到对应的枚举。

代码生成器

略。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值