Mybatis-Plus 常用操作

MyBatis-Plus系列推荐阅读顺序:


本文目录结构

一、SQL日志开关
二、常用注解
三、代码生成器
四、分页查询
五、Mybatis-Plus Wrapper
六、自动填充数据功能
七、逻辑删除
八、乐观锁


一、SQL日志开关

配置文件application.properties,增加最后一行,执行时会打印出 sql 语句。

spring.application.name=mybatis-plus
# 应用服务 WEB 访问端口
server.port=8080
####数据库连接池###
spring.datasource.url=jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=guo
spring.datasource.password=205010guo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

####输出sql日志###
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

类似JPA的日志输出配置:

jpa:
  show-sql:true#打印SQL。

二、常用注解

注解说明的官方文档:https://mybatis.plus/guide/annotation.html

2.1【@TableName 】

    @TableName               用于定义表名
注:
    常用属性:
        value                用于定义表名

2.2【@TableId】

    @TableId                 用于定义表的主键
注:
    常用属性:
        value           用于定义主键字段名
        type            用于定义主键类型(主键策略 IdType)

   主键策略:
      IdType.AUTO          主键自增,系统分配,不需要手动输入
      IdType.NONE          未设置主键
      IdType.INPUT         需要自己输入 主键值。
      IdType.ASSIGN_ID     系统分配 ID,用于数值型数据(Long,对应 mysql 中 BIGINT 类型)。
      IdType.ASSIGN_UUID   系统分配 UUID,用于字符串型数据(String,对应 mysql 中 varchar(32) 类型)。

2.3【@TableField】

    @TableField            用于定义表的非主键字段。
注:
    常用属性:
        value                用于定义非主键字段名
        exist                用于指明是否为数据表的字段, true 表示是,false 为不是。
        fill                 用于指定字段填充策略(FieldFill)。
        
    字段填充策略:(一般用于填充 创建时间、修改时间等字段)
        FieldFill.DEFAULT         默认不填充
        FieldFill.INSERT          插入时填充
        FieldFill.UPDATE          更新时填充
        FieldFill.INSERT_UPDATE   插入、更新时填充。

2.4【@TableLogic】

    @TableLogic           用于定义表的字段进行逻辑删除(非物理删除)
注:
    常用属性:
        value            用于定义未删除时字段的值
        delval           用于定义删除时字段的值

2.5【@Version】

    @Version             用于字段实现乐观锁

三、代码生成器

3.1 AutoGenerator 简介

AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。  与 mybatis 中的 mybatis-generator-core 类似。

3.2 添加依赖

        <!--  代码生成器 依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mp.version}</version>
        </dependency>
        <!-- 添加 模板引擎 依赖 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
        </dependency>

3.3 生成器代码分析

Step1:
  创建一个 代码生成器。用于生成代码。
  此处不用修改。

// Step1:代码生成器
AutoGenerator mpg = new AutoGenerator();

Step2:
  配置全局信息。指定代码输出路径,以及包名、作者等信息。
  此处按需添加,projectPath 需要修改,setAuthor 需要修改。

// Step2:全局配置
GlobalConfig gc = new GlobalConfig(); 
// 填写代码生成的目录(需要修改)
String projectPath = "E:\\myProject\\test\\test_mybatis_plus"; 
// 拼接出代码最终输出的目录
gc.setOutputDir(projectPath + "/src/main/java"); 
// 配置开发者信息(可选)(需要修改)
gc.setAuthor("郭秀志 jbcode@126.com");
 // 配置是否打开目录,false 为不打开(可选)
gc.setOpen(false); 
// 实体属性 Swagger2 注解,添加 Swagger 依赖,开启 Swagger2 模式(可选)
//gc.setSwagger2(true);
// 重新生成文件时是否覆盖,false 表示不覆盖(可选)
gc.setFileOverride(false);
 // 配置主键生成策略,此处为 ASSIGN_ID(可选)
gc.setIdType(IdType.ASSIGN_ID); 
// 配置日期类型,此处为 ONLY_DATE(可选)
gc.setDateType(DateType.ONLY_DATE); 
// 默认生成的 service 会有 I 前缀
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);

Step3:
配置数据源信息。用于指定 需要生成代码的 数据仓库、数据表。
setUrl、setDriverName、setUsername、setPassword均需修改。

// Step3:数据源配置(需要修改)
DataSourceConfig dsc = new DataSourceConfig(); 
// 配置数据库 url 地址
dsc.setUrl("jdbc:mysql://localhost:3306/testMyBatisPlus?useUnicode=true&characterEncoding=utf8");
// dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定数据库名 
// 配置数据库驱动
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
// 配置数据库连接用户名
dsc.setUsername("root"); 
// 配置数据库连接密码
dsc.setPassword("123456");
mpg.setDataSource(dsc);

Step4:
配置包信息。
setParent、setModuleName均需修改。其余按需求修改.

// Step:4:包配置
PackageConfig pc = new PackageConfig();
 // 配置父包名(需要修改)
pc.setParent("com.erbadagang.mybatis.plus"); 
// 配置模块名(需要修改)
//pc.setModuleName("mybatis-plus-starter");
 // 配置 entity 包名
pc.setEntity("entity"); 
// 配置 mapper 包名
pc.setMapper("mapper"); 
// 配置 service 包名
pc.setService("service"); 
// 配置 controller 包名
pc.setController("controller");
mpg.setPackageInfo(pc);

Step5:
配置数据表映射信息。
setInclude 需要修改,其余按实际开发修改。

// Step5:策略配置(数据库表配置)
StrategyConfig strategy = new StrategyConfig(); 
// 指定表名(可以同时操作多个表,使用 , 隔开)(需要修改)
strategy.setInclude("t_user"); 
// 配置数据表与实体类名之间映射的策略
strategy.setNaming(NamingStrategy.underline_to_camel); 
// 配置数据表的字段与实体类的属性名之间映射的策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel); 
// 配置 lombok 模式
strategy.setEntityLombokModel(true); 
// 配置 rest 风格的控制器(@RestController)
strategy.setRestControllerStyle(true); 
// 配置驼峰转连字符
strategy.setControllerMappingHyphenStyle(true); 
// 配置表前缀,生成实体时去除表前缀 
// 此处的表名为 test_mybatis_plus_user,模块名为 test_mybatis_plus,去除前缀后剩下为 user。
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);

t_user建表SQL:

/*
 Navicat Premium Data Transfer

 Source Server         : 上海
 Source Server Type    : MySQL
 Source Server Version : 50636
 Source Host           : 101.133.227.13:3306
 Source Schema         : orders_1

 Target Server Type    : MySQL
 Target Server Version : 50636
 File Encoding         : 65001

 Date: 10/07/2020 16:28:23
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `password` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `pwd_cipher` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

Step6:
  执行代码生成操作。
  此处不用修改。

// Step6:执行代码生成操作
mpg.execute();

完整代码:

package com.erbadagang.mybatis.plus.mybatisplus;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * AutoGenerationTest作用是:生成Mybatis-plus代码,AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
 *
 * @ClassName: AutoGenerationTest
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/10 15:08
 * @Copyright:
 */
@SpringBootTest
public class AutoGenerationTest {
    @Test
    public void autoGenerate() {
        // Step1:代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // Step2:全局配置
        GlobalConfig gc = new GlobalConfig();
        // 填写代码生成的目录(需要修改)
        String projectPath = "D:\\dev\\GitRepository\\mybatis-plus-starter";
        // 拼接出代码最终输出的目录
        gc.setOutputDir(projectPath + "/src/main/java");
        // 配置开发者信息(可选)(需要修改)
        gc.setAuthor("郭秀志 jbcode@126.com");
        // 配置是否打开目录,false 为不打开(可选)
        gc.setOpen(false);
        // 实体属性 Swagger2 注解,添加 Swagger 依赖,开启 Swagger2 模式(可选)
        //gc.setSwagger2(true);
        // 重新生成文件时是否覆盖,false 表示不覆盖(可选)
        gc.setFileOverride(false);
        // 配置主键生成策略,此处为 ASSIGN_ID(可选)
        gc.setIdType(IdType.AUTO);
        // 配置日期类型,此处为 ONLY_DATE(可选)
        gc.setDateType(DateType.ONLY_DATE);
        // 默认生成的 service 会有 I 前缀
        gc.setServiceName("I%sService");
        mpg.setGlobalConfig(gc);

        // Step3:数据源配置(需要修改)
        DataSourceConfig dsc = new DataSourceConfig();
        // 配置数据库 url 地址
        dsc.setUrl("jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8");
        // dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定数据库名
        // 配置数据库驱动
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        // 配置数据库连接用户名
        dsc.setUsername("guo");
        // 配置数据库连接密码
        dsc.setPassword("205010guo");
        mpg.setDataSource(dsc);

        // Step:4:包配置
        PackageConfig pc = new PackageConfig();
        // 配置父包名(需要修改)
        pc.setParent("com.erbadagang.mybatis.plus.mybatisplus");
        // 配置模块名(需要修改)
        //pc.setModuleName("mybatis-plus-starter");
        // 配置 entity 包名
        pc.setEntity("entity");
        // 配置 mapper 包名
        pc.setMapper("mapper");
        // 配置 service 包名
        pc.setService("service");
        // 配置 controller 包名
        pc.setController("controller");
        mpg.setPackageInfo(pc);

        // Step5:策略配置(数据库表配置)
        StrategyConfig strategy = new StrategyConfig();
        // 指定表名(可以同时操作多个表,使用 , 隔开)(需要修改)
        strategy.setInclude("t_user");//表名t_user
        // 配置数据表与实体类名之间映射的策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 配置数据表的字段与实体类的属性名之间映射的策略
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // 配置 lombok 模式
        strategy.setEntityLombokModel(true);
        // 配置 rest 风格的控制器(@RestController)
        strategy.setRestControllerStyle(true);
        // 配置驼峰转连字符
        strategy.setControllerMappingHyphenStyle(true);
        // 配置表前缀,生成实体时去除表前缀
        // 此处的表名为 test_mybatis_plus_user,模块名为 test_mybatis_plus,去除前缀后剩下为 user。
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);

        // Step6:执行代码生成操作
        mpg.execute();
    }

}

3.4 测试生成的service

由于生成的Service接口及实现类有些问题,需要稍为改造一下:

  • Service接口:public interface ITUserService extends IService<TUser> 增加泛型:public interface ITUserService<TUser> extends IService<TUser> 。
  • 实现类:public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements IService<TUser> 实现接口由IService变成 ITUserService

Junit 测试代码:

    @Autowired
    private ITUserService<TUser> tUserService;

    @Test
    public void testService() {
        TUser user = new TUser();
        user.setUserName("trek");
        user.setPassword("888999");
        user.setPwdCipher("ewifwiEFafe==");
        if (tUserService.save(user)) {
            tUserService.list().forEach(System.out::println);
        } else {
            System.out.println("添加数据失败");
        }
    }

测试结果:


表插入新数据

控制台输出信息:

==>  Preparing: INSERT INTO t_user ( user_name, password, pwd_cipher ) VALUES ( ?, ?, ? ) 
==> Parameters: trek(String), 888999(String), ewifwiEFafe==(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@47acd13b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e26f1ed] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@633514467 wrapping com.mysql.cj.jdbc.ConnectionImpl@297c9a9b] will not be managed by Spring
==>  Preparing: SELECT id,user_name,password,pwd_cipher FROM t_user 
==> Parameters: 
<==    Columns: id, user_name, password, pwd_cipher
<==        Row: 1, guo, bwMhZeGXyD98aToKQdXLcw==, null
<==        Row: 2, guo, bwMhZeGXyD98aToKQdXLcw==, null
<==        Row: 3, guo, 123456, bwMhZeGXyD98aToKQdXLcw==
<==        Row: 4, trek, 888999, ewifwiEFafe==
<==      Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e26f1ed]
TUser(id=1, userName=guo, password=bwMhZeGXyD98aToKQdXLcw==, pwdCipher=null)
TUser(id=2, userName=guo, password=bwMhZeGXyD98aToKQdXLcw==, pwdCipher=null)
TUser(id=3, userName=guo, password=123456, pwdCipher=bwMhZeGXyD98aToKQdXLcw==)
TUser(id=4, userName=trek, password=888999, pwdCipher=ewifwiEFafe==)

四、分页查询

4.1 配置拦截器组件

MybatisPlusApplication启动类添加代码:

     /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

4.2 编写分页代码

直接 new 一个 Page 对象,对象需要传递两个参数(当前页,每页显示的条数)。
调用 mybatis-plus 提供的分页查询方法,其会将 分页查询的数据封装到 Page 对象中。


    @Test
    public void selectPage() {
        // 根据Wrapper 自定义条件查询
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("age", "18");
        queryWrapper.orderByDesc("age");
        Page<User> userPage = new Page<User>(2, 2);
        // userPage.setCurrent(2L);  //当前是第几页 默认为1
        // userPage.setSize(2);  //每页大小
        IPage<User> userIPage = userMapper.selectPage(userPage, queryWrapper);

        System.out.println("当前页" + userIPage.getCurrent());  //当前页
        System.out.println("总页数" + userIPage.getPages()); //总页数
        System.out.println("返回数据" + userIPage.getRecords());  //返回数据
        System.out.println("每页大小" + userIPage.getSize());  //每页大小
        System.out.println("满足符合条件的条数" + userIPage.getTotal());  //满足符合条件的条数
        System.out.println("下一页" + userPage.hasNext());   //下一页
        System.out.println("上一页" + userPage.hasPrevious());  //上一页
    }

运行结果:
结果说明

控制台System.out.println代码部分日志输出:

当前页2
总页数2
返回数据[User(id=4, name=Oliver, age=21, email=xds@erbadagang.com), User(id=2, name=xiu, age=20, email=specialized@erbadagang.com)]
每页大小2
满足符合条件的条数4
下一页false
上一页true

五、Mybatis-Plus Wrapper

参考上篇文章:MyBatis-Plus 条件构造器(Wrapper)

5.1 删除

    /**
     * <p>
     * 根据根据 entity 条件,删除记录,QueryWrapper实体对象封装操作类(可以为 null)
     * 下方获取到queryWrapper后删除的查询条件为name字段为null的and年龄大于等于12的and email字段不为null的
     * 同理写法条件添加的方式就不做过多介绍了。
     * </p>
     */
    @Test
    public void delete() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper
                .isNull("name")
                .ge("age", 12)
                .isNotNull("email");
        int delete = userMapper.delete(queryWrapper);
        System.out.println("delete return count = " + delete);
    }

SQL输出:

==>  Preparing: DELETE FROM user WHERE (name IS NULL AND age >= ? AND email IS NOT NULL) 
==> Parameters: 12(Integer)
<==    Updates: 0

5.2 selectOne

    /**
     * <p>
     * 根据 entity 条件,查询一条记录,
     * 这里和上方删除构造条件一样,只是seletOne返回的是一条实体记录,当出现多条时会报错
     * </p>
     */
    @Test
    public void selectOne() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "guo");

        User user = userMapper.selectOne(queryWrapper);
        System.out.println(user);
    }

SQL输出:

==>  Preparing: SELECT id,name,age,email FROM user WHERE (name = ?) 
==> Parameters: guo(String)
<==    Columns: id, name, age, email
<==        Row: 1, Guo , 18, trek@erbadagang.com
<==      Total: 1

5.3 selectCount

   /**
     * <p>
     * 根据 Wrapper 条件,查询总记录数
     * </p>
     *
     * @param queryWrapper 实体对象
     */
    @Test
    public void selectCount() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "guo");

        Integer count = userMapper.selectCount(queryWrapper);
        System.out.println(count);
    }

SQL输出:

==>  Preparing: SELECT COUNT( 1 ) FROM user WHERE (name = ?) 
==> Parameters: guo(String)
<==    Columns: COUNT( 1 )
<==        Row: 1
<==      Total: 1

5.4 selectList


    /**
     * <p>
     * 根据 entity 条件,查询全部记录
     * </p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)为null查询全部
     */
    @Test
    public void selectListByEntity() {
        List<User> list = userMapper.selectList(null);//null为无条件

        System.out.println(list);
    }

    /**
     * <p>
     * 根据 Wrapper 条件,查询全部记录
     * </p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    @Test
    public void selectListByMapper() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "guo");

        List<User> list = userMapper.selectList(queryWrapper);//null为无条件

        System.out.println(list);
    }

5.5 selectMaps

    @Test
    public void selectMaps() {
        Page<User> page = new Page<User>(1, 5);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
        maps.forEach(map -> {
            System.out.println("name-->" + map.get("name"));
            System.out.println("email-->" + map.get("email"));
        });
        System.out.println(maps);
    }

返回类型List<Map<String, Object>>。Map的key为字段名称,value为对应的字段值。
控制台输出:

==>  Preparing: SELECT id,name,age,email FROM user 
==> Parameters: 
<==    Columns: id, name, age, email
<==        Row: 1, Guo , 18, trek@erbadagang.com
<==        Row: 2, xiu, 20, specialized@erbadagang.com
<==        Row: 3, zhi, 28, giant@erbadagang.com
<==        Row: 4, Oliver, 88, winspace@erbadagang.com
<==        Row: 5, Messi, 24, look@erbadagang.com
<==      Total: 5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@294aba23]
name-->Guo 
email-->trek@erbadagang.com
name-->xiu
email-->specialized@erbadagang.com
name-->zhi
email-->giant@erbadagang.com
name-->Oliver
email-->winspace@erbadagang.com
name-->Messi
email-->look@erbadagang.com
[{name=Guo , id=1, age=18, email=trek@erbadagang.com}, {name=xiu, id=2, age=20, email=specialized@erbadagang.com}, {name=zhi, id=3, age=28, email=giant@erbadagang.com}, {name=Oliver, id=4, age=88, email=winspace@erbadagang.com}, {name=Messi, id=5, age=24, email=look@erbadagang.com}]

六、自动填充数据功能

添加、修改数据时,每次都会使用相同的方式进行填充。比如: 数据的创建时间、修改时间、操作者等。

6.1 数据库准备

Mybatis-plus 支持自动填充这些字段的数据。给之前的数据表新增3个字段:创建时间、修改时间、操作人。
SQL语句:

ALTER TABLE `orders_1`.`user` 
ADD COLUMN `create_time` datetime(0) COMMENT '创建时间' AFTER `email`,
ADD COLUMN `update_time` datetime(0) COMMENT '修改时间' AFTER `create_time`,
ADD COLUMN `operator` varchar(20) COMMENT '操作人' AFTER `update_time`;

6.2 重新生成代码

并使用 代码生成器重新生成代码,注意修改生成器配置为可覆盖老代码。

        // 重新生成文件时是否覆盖,false 表示不覆盖(可选)
        gc.setFileOverride(true);

6.3 修改entity

使用@TableField注解,标注需要进行填充的字段。

package com.erbadagang.mybatis.plus.mybatisplus.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * 
 * </p>
 *
 * @author 郭秀志 jbcode@126.com
 * @since 2020-07-11
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {

    private static final long serialVersionUID=1929834928304L;

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

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

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

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

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    /**
     * 操作人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String operator;


}

填充策略FieldFill.INSERT_UPDATE表示插入和更新都进行自动填充。

6.4 自定义MetaObjectHandler

自定义一个类,实现 MetaObjectHandler 接口,并重写方法。添加 @Component 注解,交给 Spring 去管理。

package com.erbadagang.mybatis.plus.mybatisplus.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @description 自定义的数据填充handler,分别写insert和update的写入策略。
 * @ClassName: MyFillDataMetaObjectHandler
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/11 9:39
 * @Copyright:
 */
@Component
public class MyFillDataMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "operator", String.class, "梅西爱骑车");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "operator", String.class, "梅西爱骑车");
    }
}

6.5 测试

6.5.1 插入测试


    /**
     * 测试插入的自动填充数据功能。
     */
    @Test
    public void testAutoFillInsert() {
        User user = new User();
        user.setId(0l);
        user.setName("崔克");
        user.setAge(18);
        user.setEmail("trek@erbadagang.cn");

        int id = userMapper.insert(user);//自动返回插入的id

        System.out.println(id);
    }

运行测试用例,如果报错:Caused by: java.sql.SQLException: Field 'id' doesn't have a default value
需要把id列勾上自增。

自增

输出的SQL信息:

==>  Preparing: INSERT INTO user ( name, age, email, create_time, update_time, operator ) VALUES ( ?, ?, ?, ?, ?, ? ) 
==> Parameters: 崔克(String), 18(Integer), trek@erbadagang.cn(String), 2020-07-11 09:57:43.386(Timestamp), 2020-07-11 09:57:43.388(Timestamp), 梅西爱骑车(String)
<==    Updates: 1
表中数据

6.5.2 更新测试

更新name为英文的trek,age为28。

    /**
     * 测试更新的自动填充数据功能。
     */
    @Test
    public void testAutoFillUpdate() {
        User user = new User();
        user.setId(7l);
        user.setName("trek");
        user.setAge(28);
        user.setEmail("trek@erbadagang.cn");

        int id = userMapper.updateById(user);//自动返回插入的id

        System.out.println(id);
    }

运行测试。
输出的SQL信息,只更新了update_time没更新create_time字段:

==>  Preparing: UPDATE user SET name=?, age=?, email=?, update_time=?, operator=? WHERE id=? 
==> Parameters: trek(String), 28(Integer), trek@erbadagang.cn(String), 2020-07-11 10:05:30.249(Timestamp), 梅西爱骑车(String), 7(Long)
<==    Updates: 1

如果入库的时间跟上面打印的SQL不一致,需要在jdbc连接加入时区设置:
jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

七、逻辑删除

删除数据,可以通过物理删除,也可以通过逻辑删除。

  • 物理删除指的是直接将数据从数据库中删除,不保留。
  • 逻辑删除指的是修改数据的某个字段,使其表示为已删除状态,而非删除数据,保留该数据在数据库中,但是查询时不显示该数据(查询时过滤掉该数据)。

7.1 表结构

给数据表增加一个字段:delete_flag,用于表示该数据是否被逻辑删除。
SQL语句:

ALTER TABLE `orders_1`.`user` 
ADD COLUMN `delete_flag` tinyint(1) COMMENT '逻辑删除(0 未删除、1 删除)' AFTER `operator`;

7.3 使用逻辑删除。

可以定义一个自动填充规则,初始值为 0。0 表示未删除, 1 表示删除。
在Entity类新增:

    /**
     * 逻辑删除(0 未删除、1 删除)
     */
    @TableLogic(value = "0", delval = "1")//定义逻辑删除功能。
    @TableField(fill = FieldFill.INSERT)//定义在insert的时候自动填充功能
    private Integer deleteFlag;

@TableLogic定义逻辑删除功能,若去除 TableLogic 注解,再执行 Delete 时进行物理删除,直接删除这条数据。
@TableField定义在自动填充功能。

在自动填充规则MyFillDataMetaObjectHandler类的insertFill方法添加:

@Override
public void insertFill(MetaObject metaObject) {
......
    this.strictInsertFill(metaObject, "deleteFlag", Integer.class, 0);
}

7.4 测试

新增一条闪电牌自行车数据:

        User user = new User();
        user.setId(0l);
        user.setName("闪电");
        user.setAge(18);
        user.setEmail("specialized@erbadagang.cn");

        int id = userMapper.insert(user);//自动返回插入的id

delete_flag字段为自动填充代码定义的默认值0,当然也可以使用数据库定义默认值。


表层面定义的初始值0

新增数据delete_flag值:
delete_flag=0

删除数据:

    //这次使用IUserService而不是mapper进行测试
    @Autowired
    private IUserService userService;

    /**
     * 逻辑删除测试。
     */
    @Test

    public void testDelete() {
        if (userService.removeById(8)) {
            System.out.println("删除数据成功");
            userService.list().forEach(System.out::println);
        } else {
            System.out.println("删除数据失败");
        }
    }

执行测试,输出的日志:

==>  Preparing: UPDATE user SET delete_flag=1 WHERE id=? AND delete_flag=0 
==> Parameters: 8(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@22bdb1d0]
删除数据成功

==>  Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag FROM user WHERE delete_flag=0 
==> Parameters: 
<==      Total: 0

可以看到更新delete_flag=1的操作,以及查询时自动加上了WHERE delete_flag=0的判断。

表数据变化:


delete_flage变为1

八、乐观锁

8.1 基础知识

(1)首先认识一下: 读问题、写问题
  操作数据库数据时,遇到的最基本问题就是 读问题与写问题。
  读问题 指的是从数据库中读取数据时遇到的问题,比如:脏读、幻读、不可重复读。
脏读、幻读、不可重复读 参考地址
  写问题 指的是数据写入数据库时遇到的问题,比如:丢失更新(多个线程同时对某条数据更新,无论执行顺序如何,都会丢失其他线程更新的数据)

(2)如何解决写问题?
  乐观锁、悲观锁就是为了解决 写问题而存在的。
    乐观锁:总是假设最好的情况,每次读取数据时认为数据不会被修改(即不加锁),当进行更新操作时,会判断这条数据是否被修改,未被修改,则进行更新操作。若被修改,则数据更新失败,可以对数据进行重试(重新尝试修改数据)。
    悲观锁:总是假设最坏的情况,每次读取数据时认为数据会被修改(即加锁),当进行更新操作时,直接更新数据,结束操作后释放锁(此处才可以被其他线程读取)。

(3)乐观锁、悲观锁使用场景?
  乐观锁一般用于读比较多的场合,尽量减少加锁的开销。
  悲观锁一般用于写比较多的场合,尽量减少 类似 乐观锁重试更新引起的性能开销。

(4)乐观锁两种实现方式
方式一:通过版本号机制实现。
  在数据表中增加一个 version 字段。
  取数据时,获取该字段,更新时以该字段为条件进行处理(即set version = newVersion where version = oldVersion),若 version 相同,则更新成功(给新 version 赋一个值,一般加 1)。若 version 不同,则更新失败,可以重新尝试更新操作。

方式二:通过 CAS 算法实现。
  CAS 为 Compare And Swap 的缩写,即比较交换,是一种无锁算法(即在不加锁的情况实现多线程之间的变量同步)。
  CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。如果内存地址里面的值 V 和 A 的值是一样的,那么就将内存里面的值更新成B。若 V 与 A 不一致,则不执行任何操作(可以通过自旋操作,不断尝试修改数据直至成功修改)。即 V == A ? V = B : V = V。
  CAS 可能导致 ABA 问题(两次读取数据时值相同,但不确定值是否被修改过),比如两个线程操作同一个变量,线程 A、线程B 初始读取数据均为 A,后来 线程B 将数据修改为 B,然后又修改为 A,此时线程 A 再次读取到的数据依旧是 A,虽然值相同但是中间被修改过,这就是 ABA 问题。可以加一个额外的标志位 C,用于表示数据是否被修改。当标志位 C 与预期标志位相同、且 V == A 时,则更新值 B。

(5)mybatis-plus 实现乐观锁(通过 version 机制)
实现思路:
  Step1:取出记录时,获取当前version
  Step2:更新时,带上这个version
  Step3:执行更新时, set version = newVersion where version = oldVersion
  Step4:如果version不对,就更新失败

(6)mybatis-plus 代码实现乐观锁

8.2 MP实现乐观锁

配置乐观锁插件。
启动类MybatisPlusApplication新增如下代码(类似分页插件),将 OptimisticLockerInterceptor通过@Bean交给 Spring 管理。

/**
 * 乐观锁插件
 * @return 乐观锁插件的实例
 */
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
}

8.3 定义一个数据库字段 version

ALTER TABLE `orders_1`.`user` 
ADD COLUMN `version` int COMMENT '版本号(用于乐观锁, 默认为 1)' AFTER `delete_flag`;

8.4 实体类

使用@Version注解标注对应的实体类。可以通过@TableField进行数据自动填充。

/**
 * 版本号(用于乐观锁, 默认为 1)
 */
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;

8.5 自动填充规则

在自动填充规则MyFillDataMetaObjectHandler类的insertFill方法添加:

@Override
public void insertFill(MetaObject metaObject) {
......
    //乐观锁version初始化值为1
    this.strictInsertFill(metaObject, "version", Integer.class, 1);
}

8.6 测试

    /**
     * 乐观锁测试
     */
    @Test
    public void testVersion() {
        User user = new User();
        user.setName("Look");
        user.setAge(8);
        user.setEmail("look@erbadagang.cn");
        userService.save(user);//新增数据
        userService.list().forEach(System.out::println);//查询数据

        user.setName("梅花");
        userService.update(user, null);//修改数据
        userService.list().forEach(System.out::println);//查询数据
    }

运行结果(语句增加了我的注释):

##插入数据,version=1
==>  Preparing: INSERT INTO user ( name, age, email, create_time, update_time, operator, delete_flag, version ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? ) 
==> Parameters: Look(String), 8(Integer), look@erbadagang.cn(String), 2020-07-11 12:03:50.712(Timestamp), 2020-07-11 12:03:50.715(Timestamp), 梅西爱骑车(String), 0(Integer), 1(Integer)
<==    Updates: 1
##查询数据,读取的version为1
==>  Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag,version FROM user WHERE delete_flag=0 
==> Parameters: 
<==    Columns: id, name, age, email, create_time, update_time, operator, delete_flag, version
<==        Row: 9, Look, 8, look@erbadagang.cn, 2020-07-11 12:03:51, 2020-07-11 12:03:51, 梅西爱骑车, 0, 1
<==      Total: 1
##更新数据,条件是version=1如果此时被其他程序更新了,这里条件不满足不会更新数据。
##version的值自动+1,现在是2。
==>  Preparing: UPDATE user SET name=?, age=?, email=?, create_time=?, update_time=?, operator=?, version=? WHERE delete_flag=0 AND (version = ?) 
==> Parameters: 梅花(String), 8(Integer), look@erbadagang.cn(String), 2020-07-11 12:03:50.712(Timestamp), 2020-07-11 12:03:50.715(Timestamp), 梅西爱骑车(String), 2(Integer), 1(Integer)
<==    Updates: 1
##再次查询version为2。
==>  Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag,version FROM user WHERE delete_flag=0 
==> Parameters: 
<==    Columns: id, name, age, email, create_time, update_time, operator, delete_flag, version
<==        Row: 9, 梅花, 8, look@erbadagang.cn, 2020-07-11 12:03:51, 2020-07-11 12:03:51, 梅西爱骑车, 0, 2
<==      Total: 1
##查询出来的最新数据,delete_flag=0
User(id=9, name=梅花, age=8, email=look@erbadagang.cn, createTime=Sat Jul 11 12:03:51 CST 2020, updateTime=Sat Jul 11 12:03:51 CST 2020, operator=梅西爱骑车, deleteFlag=0, version=2)

底线


本文源代码使用 Apache License 2.0开源许可协议,可从Gitee代码地址通过git clone命令下载到本地或者通过浏览器方式查看源代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341