Mybatis-Plus

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

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑

  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作

  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求

  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错

  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题

  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作

  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )

  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用

  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询

  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库

  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询

  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

Mybatis-Plus配置

初始化工程

创建一个空的 Spring Boot 工程

添加依赖

引入 MyBatis-Plus Starter 依赖

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

配置数据库

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/teach
    username: postgres
    password: zhangyu1234

编码

Mapper 接口类 UserMapper.java

public interface UserMapper extends BaseMapper<User> {
}
package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@MapperScan("com.example.demo.mapper")
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class })
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

测试

package com.cmz.mybatisplus;

import com.cmz.mybatisplus.demos.web.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class test {
    @Autowired
    private StudentMapper studentMapper;
    @Test
    void contextLoads() {
        System.out.println(studentMapper.selectById(1));

    }
}

日志配置

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

主键生成策略(9种 分布式ID生成方案,让你一次学个够 - Java进阶课 - SegmentFault 思否

1、基于UUID

public static void main(String[] args) { 
       String uuid = UUID.randomUUID().toString().replaceAll("-","");
       System.out.println(uuid);
 }

UUID的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718,但UUID却并不适用于实际的业务需求。像用作订单号UUID这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID

优点:

  • 生成足够简单,本地生成无网络消耗,具有唯一性

缺点:

  • 无序的字符串,不具备趋势自增特性

  • 没有具体的业务含义

  • 长度过长16 字节128位,36位长度的字符串,存储以及查询对MySQL的性能消耗较大,MySQL官方明确建议主键要尽量越短越好,作为数据库主键 UUID 的无序性会导致数据位置频繁变动,严重影响性能。

2、基于数据库自增ID

基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:

CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
    id bigint(20) unsigned NOT NULL auto_increment, 
    value char(10) NOT NULL default '',
    PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value)  VALUES ('values');

当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

优点:

  • 实现简单,ID单调自增,数值类型查询速度快

缺点:

  • DB单点存在宕机风险,无法扛住高并发场景

3、基于数据库集群模式

前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。

那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?

解决方案:设置起始值自增步长

MySQL_1 配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

MySQL_2 配置:

set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

这样两个MySQL实例的自增ID分别就是:

1、3、5、7、9
2、4、6、8、10

那如果集群后的性能还是扛不住高并发咋办?就要进行MySQL扩容增加节点,这是一个比较麻烦的事。


从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点压力的问题,同时为了ID生成特性,将自增步长按照机器数量来设置。

增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些,但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改

优点:

  • 解决DB单点问题

缺点:

  • 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。

4、基于数据库的号段模式

号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:

CREATE TABLE id_generator (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '当前最大id',
  step int(20) NOT NULL COMMENT '号段的布长',
  biz_type    int(20) NOT NULL COMMENT '业务类型',
  version int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
) 

biz_type :代表不同业务类型

max_id :当前最大的可用id

step :代表号段的长度

version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

id

biz_type

max_id

step

version

1

101

1000

2000

0

等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]

update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

5、基于Redis模式

Redis也同样可以实现,原理就是利用redisincr命令实现ID的原子性自增。

127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id      // 增加1,并返回递增后的数值
(integer) 2

redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDBAOF

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。

    • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

6、基于雪花算法(Snowflake)模式

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。

Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。

  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。

  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L 60 60 24 365) = 69年

  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。

  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID

根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。

Java版本的Snowflake算法实现:

/**
 * Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数,然后转化为62进制变成一个短地址URL
 *
 * https://github.com/beyondfengyu/SnowFlake
 */
public class SnowFlakeShortUrl {

    /**
     * 起始的时间戳
     */
    private final static long START_TIMESTAMP = 1480166465631L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12;   //序列号占用的位数
    private final static long MACHINE_BIT = 5;     //机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    private long dataCenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastTimeStamp = -1L;  //上一次时间戳

    private long getNextMill() {
        long mill = getNewTimeStamp();
        while (mill <= lastTimeStamp) {
            mill = getNewTimeStamp();
        }
        return mill;
    }

    private long getNewTimeStamp() {
        return System.currentTimeMillis();
    }

    /**
     * 根据指定的数据中心ID和机器标志ID生成指定的序列号
     *
     * @param dataCenterId 数据中心ID
     * @param machineId    机器标志ID
     */
    public SnowFlakeShortUrl(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currTimeStamp = getNewTimeStamp();
        if (currTimeStamp < lastTimeStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currTimeStamp == lastTimeStamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currTimeStamp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastTimeStamp = currTimeStamp;

        return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
                | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }
    
    public static void main(String[] args) {
        SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);

        for (int i = 0; i < (1 << 4); i++) {
            //10进制
            System.out.println(snowFlake.nextId());
        }
    }
}

7、百度(uid-generator)

uid-generator是由百度技术部开发,项目GitHub地址 https://github.com/baidu/uid-...

uid-generator是基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳工作机器ID序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。

对于uid-generator ID组成结构:

workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,而且同一应用每次重启就会消费一个workId

参考文献
https://github.com/baidu/uid-...

8、美团(Leaf)

Leaf由美团开发,github地址:https://github.com/Meituan-Di...

Leaf同时支持号段模式和snowflake算法模式,可以切换使用。

号段模式

先导入源码 https://github.com/Meituan-Di... ,在建一张表leaf_alloc

DROP TABLE IF EXISTS `leaf_alloc`;

CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '' COMMENT '业务key',
  `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',
  `step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',
  `description` varchar(256)  DEFAULT NULL COMMENT '业务key的描述',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库维护的更新时间',
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

然后在项目中开启号段模式,配置对应的数据库信息,并关闭snowflake模式

leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=root

leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=

启动leaf-server 模块的 LeafServerApplication项目就跑起来了

号段模式获取分布式自增ID的测试url :http://localhost:8080/api/segment/get/leaf-segment-test

监控号段模式:http://localhost:8080/cache

snowflake模式

Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,LeafworkId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId

leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1
leaf.snowflake.port=2181

snowflake模式获取分布式自增ID的测试url:http://localhost:8080/api/snowflake/get/test

9、滴滴(Tinyid)

Tinyid由滴滴开发,Github地址:https://github.com/didi/tinyid

Tinyid是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]
Tinyid提供httptinyid-client两种方式接入

Http方式接入

(1)导入Tinyid源码:

git clone https://github.com/didi/tinyi...

(2)创建数据表:

CREATE TABLE `tiny_id_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
  `begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
  `max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
  `step` int(11) DEFAULT '0' COMMENT '步长',
  `delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
  `remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';

CREATE TABLE `tiny_id_token` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `token` varchar(255) NOT NULL DEFAULT '' COMMENT 'token',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '此token可访问的业务类型标识',
  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'token信息表';

INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
    (1, 'test', 1, 1, 100000, 1, 0, '2018-07-21 23:52:58', '2018-07-22 23:19:27', 1);

INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
    (2, 'test_odd', 1, 1, 100000, 2, 1, '2018-07-21 23:52:58', '2018-07-23 00:39:24', 3);


INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
    (1, '0f673adf80504e2eaa552f5d791b644c', 'test', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
    (2, '0f673adf80504e2eaa552f5d791b644c', 'test_odd', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

(3)配置数据库:

datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456

(4)启动tinyid-server后测试

获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c'
返回结果: 3

批量获取分布式自增ID:
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c&batchSize=10'
返回结果:  4,5,6,7,8,9,10,11,12,13
Java客户端方式接入

重复Http方式的(2)(3)操作

引入依赖

       <dependency>
            <groupId>com.xiaoju.uemc.tinyid</groupId>
            <artifactId>tinyid-client</artifactId>
            <version>${tinyid.version}</version>
        </dependency>

配置文件

tinyid.server =localhost:9999
tinyid.token =0f673adf80504e2eaa552f5d791b644c

testtinyid.token是在数据库表中预先插入的数据,test 是具体业务类型,tinyid.token表示可访问的业务类型

// 获取单个分布式自增ID
Long id =  TinyId . nextId( " test " );

// 按需批量分布式自增ID
List< Long > ids =  TinyId . nextId( " test " , 10 );

MyBatis-Plus 主键策略

源码

package com.baomidou.mybatisplus.annotation;

public enum IdType {
    AUTO(0),
    NONE(1),
    INPUT(2),
    ASSIGN_ID(3),
    ASSIGN_UUID(4);

    private final int key;

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

    public int getKey() {
        return this.key;
    }
}
AUTO(0),          id自增
NONE(1),          未设置
INPUT(2),         手动输入
ASSIGN_ID(3),     雪花算法
ASSIGN_UUID(4);   UUID

使用

@TableName("classroom")
@Data
@Builder
public class Classroom implements Serializable {
    @TableId(type = IdType.ASSIGN_ID)
    private int id;
    private String roomName;
}

Mybatis-Plus 更新操作

Mybatis-Plus 提供了丰富的更新操作方法,可以方便地对数据库中的数据进行更新。以下是一些常用的更新操作详解:

1. 根据主键更新单个实体

通过 Mybatis-Plus,可以根据主键更新单个实体。示例代码如下:

User user = new User();
user.setId(1L);
user.setName("Alice");
user.setAge(25);
userMapper.updateById(user);

上述代码中,首先创建一个 User 对象,并设置需要更新的字段值,然后调用 updateById 方法,传入该对象即可根据主键更新数据库中对应的记录。

2. 根据条件更新实体

除了根据主键更新外,还可以根据条件更新实体。示例代码如下:

UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("age", 25).set("name", "Bob");
userMapper.update(null, updateWrapper);

在上述代码中,首先创建一个 UpdateWrapper 对象,通过 eq 方法设置更新条件,然后通过 set 方法设置需要更新的字段及对应的值,最后调用 update 方法进行更新操作。

3. Lambda 表达式更新

Mybatis-Plus 还支持使用 Lambda 表达式进行更新操作,使更新操作更加简洁。示例代码如下:

LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.eq(User::getAge, 25).set(User::getName, "Charlie");
userMapper.update(null, lambdaUpdateWrapper);

在上述代码中,通过 LambdaUpdateWrapper 对象,可以使用 Lambda 表达式指定更新条件和更新字段,从而实现更新操作。

4. 批量更新

Mybatis-Plus 也支持批量更新操作,可以一次性更新多条记录。示例代码如下:

List<User> userList = new ArrayList<>();
// 填充 userList
userMapper.updateBatchById(userList);

在上述代码中,首先创建一个包含多个 User 对象的列表 userList,然后调用 updateBatchById 方法,传入列表即可进行批量更新操作。

自动填充字段

MyBatis-Plus 提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。以下是如何使用这一功能的详细说明。

原理概述

自动填充功能通过实现 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 接口来实现。你需要创建一个类来实现这个接口,并在其中定义插入和更新时的填充逻辑。

使用步骤

1. 定义实体类

在实体类中,你需要使用 @TableField 注解来标记哪些字段需要自动填充,并指定填充的策略。

public class User {
  @TableField(fill = FieldFill.INSERT)

  private String createTime;

  @TableField(fill = FieldFill.UPDATE)

  private String updateTime;

// 其他字段...

}

2. 实现 MetaObjectHandler

创建一个类来实现 MetaObjectHandler 接口,并重写 insertFillupdateFill 方法。

package com.cmz.mybatisplus.demos.web;
import java.time.LocalDateTime;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    //插入时的填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        //打印日志信息
        log.info("start insert fill...");
        //是否开启了插入填充
//        this.openInsertFill();
        /* *
         * setFieldValByName方法有三个参数:
         * String fieldName, Object fieldVal, MetaObject metaObject
         * @param fieldName  Java实体类的属性名称
         * @param fieldVal   Java实体类的属性值
         * @param metaObject 数据源对象参数
         */
        //设置创建时间属性和值
        //3.3.0之前使用setFieldValByName方法
//        this.setFieldValByName("createTime",new Date(),metaObject);
        //3.3.0之后推荐使用strictInsertFill方法
        this.strictInsertFill(metaObject,"createTime", LocalDateTime.class,LocalDateTime.now());
        //设置修改时间属性和值
        //3.3.0之前使用setFieldValByName方法
//        this.setFieldValByName("updateTime",new Date(),metaObject);
        //3.3.0之后推荐使用strictInsertFill方法
        this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());
    }

    //更新时的填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        //打印日志信息
        log.info("start update fill...");
        //设置修改时间属性和值
        //3.3.0之前使用setFieldValByName方法
//        this.setFieldValByName("updateTime",new Date(),metaObject);
        //3.3.0之后推荐使用strictUpdateFill方法
        this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());
    }

}

3. 配置自动填充处理器

确保你的 MyMetaObjectHandler 类被 Spring 管理,可以通过 @Component@Bean 注解来实现。

注意事项

  • 自动填充是直接给实体类的属性设置值。

  • 如果属性没有值,入库时会是 null

  • MetaObjectHandler 提供的默认方法策略是:如果属性有值则不覆盖,如果填充值为 null 则不填充。

  • 字段必须声明 @TableField 注解,并设置 fill 属性来选择填充策略。

  • 填充处理器需要在 Spring Boot 中声明为 @Component@Bean

  • 使用 strictInsertFillstrictUpdateFill 方法可以根据注解 FieldFill.xxx、字段名和字段类型来区分填充逻辑。

  • 如果不需区分,可以使用 fillStrategy 方法。

  • update(T entity, Wrapper<T> updateWrapper) 时,entity 不能为空,否则自动填充失效。

  • update(Wrapper<T> updateWrapper) 时不会自动填充,需要手动赋值字段条件。

当自定义mapper方法需要走填充时,建议按下列注解方式添加参数注解(如果使用编译参数保留的情况下,变量名字与注解名字保持一致也行)

数据类型 注解 示例

多参数填充示例:

 insertFillByCustomMethod1(H2User h2User);

 insertFillByCustomMethod8(H2User[] h2Users);

 insertFillByCustomMethod6(@Param("coll") Collection<H2User> h2User);

 insertFillByCustomMethod5(@Param("collection") Collection<H2User> h2User);

 insertFillByCustomMethod7(@Param("list") List<H2User> h2User);

 insertFillByCustomMethod9(@Param("array") H2User[] h2Users);

 //et标记的会填充,coll虽然是特殊的key值,但不属于实体,所以不会处理

 updateFillByCustomMethod2(@Param("coll") Collection<Long> ids, @Param("et") H2User h2User);  

 updateFillByCustomMethod4(@Param("colls") Collection<Long> ids, @Param("et") H2User h2User);

无法填充示例:

//方法虽然与上面方法一致,但key值不满足填充条件,需要将user换成et才能正确填充

updateFillByCustomMethod3(@Param("coll") Collection<Long> ids, @Param("user") H2User h2User);

FieldFill 枚举

public enum FieldFill {
DEFAULT, // 默认不处理

INSERT, // 插入填充字段

UPDATE, // 更新填充字段

INSERT_UPDATE // 插入和更新填充字段

}

Mybatis-Plus 乐观锁

Mybatis-Plus 提供了乐观锁的支持,乐观锁是一种通过版本号或时间戳来实现的并发控制机制,用于解决多个用户同时修改同一数据时可能出现的数据冲突问题。在 Mybatis-Plus 中,乐观锁的实现非常简单,只需要在实体类中添加一个用于存储版本号的字段,并在对应的数据库表中添加一个对应的字段即可。

使用步骤

配置乐观锁插件

Spring Boot 注解方式

@Configuration
@MapperScan("按需修改")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

在实体类中添加版本号字段

  1. 在实体类中添加一个用于存储版本号的字段,通常命名为version,并使用@Version注解标注该字段,示例代码如下:

    public class User {
        private Long id;
        private String name;
        private Integer age;
        @Version
        private Integer version;
        // 省略其他字段和方法
    }
    

在数据库表中添加版本号字段

  1. 在对应的数据库表中添加一个用于存储版本号的字段,通常选择整型或时间戳类型,例如version字段。

  2. 更新操作时自动更新版本号

    在进行更新操作时,Mybatis-Plus 会自动更新版本号字段的数值。当进行更新操作时,Mybatis-Plus 会根据版本号的值来判断是否允许更新,如果版本号与更新前不一致,则更新操作会失败,从而避免数据冲突。

示例

假设有一个名为User的实体类,包含idnameageversion字段,其中version字段用于存储版本号。在进行更新操作时,Mybatis-Plus 会自动更新version字段的数值,示例代码如下:

User user = userMapper.selectById(1L);
user.setName("newName");
user.setAge(30);
userMapper.updateById(user);

在上述示例中,当调用updateById方法进行更新操作时,Mybatis-Plus 会自动更新version字段的数值,并在更新时检查版本号,如果版本号不一致,则更新操作会失败。

注意事项

  • 支持的数据类型包括:int, Integer, long, Long, Date, Timestamp, LocalDateTime

  • 对于整数类型,newVersionoldVersion + 1

  • newVersion 会自动回写到实体对象中。

  • 支持内置的 updateById(entity)update(entity, wrapper), saveOrUpdate(entity), insertOrUpdate(entity) (version >=3.5.7) 方法。

  • 自定义方法更新时如果满足内置参数的参数条件方式也会执行乐观锁逻辑,例如自定义myUpate(entity) 这个和 updateById(entity) 是等价的,会提取参数进行乐观锁填充,但更新实现需要自行处理。

  • update(entity, wrapper) 方法中,wrapper 不能复用。

Mybatis-Plus 查询操作详解

Mybatis-Plus 提供了丰富的查询操作,可以方便地进行单表的 CRUD 操作。以下是一些常用的查询操作:

1. 根据主键查询

使用 BaseMapper 提供的 selectById 方法可以根据主键查询记录,例如:

User user = userMapper.selectById(1);

2. 条件查询

2.1 使用 Lambda 表达式

Mybatis-Plus 支持使用 Lambda 表达式进行条件查询,例如:

List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery()
        .eq(User::getAge, 20)
        .like(User::getName, "Tom"));

2.2 使用 QueryWrapper

也可以使用 QueryWrapper 进行条件查询,例如:

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("age", 20).like("name", "Tom");
List<User> userList = userMapper.selectList(queryWrapper);

3. 分页查询

Mybatis-Plus 提供了分页插件,可以方便地进行分页查询,例如:

Page<User> page = new Page<>(1, 10); // 第一页,每页10条记录
IPage<User> userPage = userMapper.selectPage(page, Wrappers.<User>lambdaQuery().eq(User::getAge, 20));
List<User> userList = userPage.getRecords();

4. 自定义 SQL 查询

除了以上方法,还可以使用 @Select 注解或者 XML 文件中的 SQL 进行自定义查询,例如:

@Select("SELECT * FROM user WHERE age = #{age}")
List<User> selectByAge(@Param("age") Integer age);

这样就可以根据年龄进行自定义查询操作。