MyBatis Plus

Mybatis-Plus

1 简介

1.1 Mybatis-Plus 概述

img

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

我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。

特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

1.2 代码及文档发布地址

官方地址:https://baomidou.com/pages/24112f/

代码地址:

  1. https://github.com/baomidou/mybatis-plus
  2. https://gitee.com/baomidou/mybatis-plus

2 集成MP

2.1 创建测试表

现有一张 User 表,其表结构如下:

id name age email
1 Jone 18 test1@baomidou.com
2 Jack 20 test2@baomidou.com
3 Tom 28 test3@baomidou.com
4 Sandy 21 test4@baomidou.com
5 Billie 24 test5@baomidou.com

其对应的数据库 Schema 脚本如下:

DROP TABLE IF EXISTS user;

CREATE TABLE `user` (
  `id` bigint NOT NULL COMMENT '主键ID',
  `name` varchar(30) COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
  `age` int DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '邮箱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin

其对应的数据库 Data 脚本如下:

DELETE FROM user;

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

2.2 创建 JavaBean

package com.atguigu.mp.mp01.bean;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

2.3 依赖配置

依赖配置

pom.xml中引入 spring-boot-starterspring-boot-starter-testmybatis-plus-boot-starter和数据库连接依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.0</version>
    </dependency>
</dependencies>

使用 Mybatis-Plus 可以节省大量的代码,尽量不要同时导入 Mybatis 和 Mybatis-Plus ,以免版本冲突。因为 MyBatisPlus 的 starter 会自动导入 MyBatis 。 MySQL 依赖的版本根据自己的 MySQL 版本进行修改。

数据源配置

# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mp?useSSL=false&useUnicode=true&characterEncoding=utf-8
    username: root
    password: 12345678

3 入门HelloWorld

3.1 入门CRUD

  1. 提出问题:

假设我们已存在一张 tbl_employee 表,且已有对应的实体类 Employee ,实现 tbl_employee 表的 CRUD 操作我们需要做什么呢?

  1. 实现方式
  • 基于MyBatis
    需要编写 EmployeeMapper 接口,并手动编写 CRUD 方法。提供 EmployeeMapper.xml 映射文件,并手动编写每个方法对应的 SQL语句

  • 基于 MP

    只需要创建 EmployeeMapper 接口,并继承 BaseMapper 接口。这就是使用 MP 需要完成的所有操作,甚至不需要创建 SQL映射文件。

编写 Mapper 类 UserMapper.java

package com.atguigu.mp.mp01.mapper;

import com.atguigu.mp.mp01.beans.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface UserMapper extends BaseMapper<User> {

}

启动类设置

package com.atguigu.mp.mp01;

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

@SpringBootApplication
@MapperScan("com.atguigu.mp.mp01.mapper")
public class Mp01Application {

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

}

配置日志

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

3.2 插入操作

package com.atguigu.mp.mp01;

import com.atguigu.mp.mp01.bean.User;
import com.atguigu.mp.mp01.mapper.UserMapper;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void insert() {
        userMapper.insert(new User(null, "MP", 22, "mp@atguigu.com"));
    }
}

运行结果如下,可以发现,没有设置id的时候,insert方法自动生成了id

2022-01-06 10:44:46.563  INFO 5484 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1684265526 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a4b288a] will not be managed by Spring
==>  Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
==> Parameters: 1478920447641960450(Long), MP(String), 22(Integer), mp@atguigu.com(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1426370c]

3.2.1 主键问题

在主键上使用不同的注解,将会产生不同的ID。如果没有添加注解且主键为Number子类,默认自动生成雪花算法计算得到的ID。因此在主键属性上添加注解至关重要。

属性 类型 必须指定 默认值 描述
value String "" 主键字段名
type Enum IdType.NONE 指定主键类型

相关的枚举:

描述
AUTO 数据库 ID 自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT insert 前自行 set 主键值
ASSIGN_ID 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)
ID_WORKER 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID)
UUID 32 位 UUID 字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)

默认生成的方式为NONE,当插入对象ID为空且类型为Number,使用雪花算法自动补充。

/*
 * Copyright (c) 2011-2022, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baomidou.mybatisplus.annotation;

import lombok.Getter;

/**
 * 生成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;
    }
}

3.2.2 设置主键自增

数据库层面

image

代码层面

在字段上增加注解

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

运行测试方法,可得到恢复正常自增ID

JDBC Connection [HikariProxyConnection@1507181879 wrapping com.mysql.cj.jdbc.ConnectionImpl@5464b97c] will not be managed by Spring
==>  Preparing: INSERT INTO user ( name, age, email ) VALUES ( ?, ?, ? )
==> Parameters: MP(String), 22(Integer), mp@atguigu.com(String)
<==    Updates: 1
id name age email
6 MP 22 mp@atguigu.com

3.2.3 数据库全局配置

在application.yml中配置,具体配置:基本配置

3.2.4 相关注解

@TableName
  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
@TableId
  • 描述:主键注解
  • 使用位置:实体类主键字段
@TableField
  • 描述:字段注解(非主键)
属性 类型 必须指定 默认值 描述
value String "" 数据库字段名
exist boolean true 是否为数据库表字段

具体内容可查看官方文档,这里注意一下exist字段,如果设置成false表示数据库里面没有这个字段。

例如在 User 类中新增一个字段salary,如果不设置该字段会报如下错误:

Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'salary' in 'field list'

可以设置如下:

@TableField(exist = false)
private Double salary;

3.2.5 插入数据后获取主键值

MyBatis-Plus在插入后能够自动获取主键值。示例代码如下:

@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void insert() {
        User user = new User(null, "MP", null, "mp@atguigu.com", 20000d);
        userMapper.insert(user);
        System.out.println(user.getId());
    }
}

控制台打印:

JDBC Connection [HikariProxyConnection@911998047 wrapping com.mysql.cj.jdbc.ConnectionImpl@1a7e799e] will not be managed by Spring
==>  Preparing: INSERT INTO user ( name, email ) VALUES ( ?, ? )
==> Parameters: MP(String), mp@atguigu.com(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@506aa618]
10

3.2.6 插入所有字段

可以在配置文件中修改

insertStrategy

  • 类型:com.baomidou.mybatisplus.annotation.FieldStrategy
  • 默认值:NOT_NULL

字段验证策略之 insert,在 insert 的时候的字段验证策略

/**
 * 字段策略枚举类
 * <p>
 * 如果字段是基本数据类型则最终效果等同于 {@link #IGNORED}
 *
 * @author hubin
 * @since 2016-09-09
 */
public enum FieldStrategy {
    /**
     * 忽略判断
     */
    IGNORED,
    /**
     * 非NULL判断
     */
    NOT_NULL,
    /**
     * 非空判断(只对字符串类型字段,其他类型字段依然为非NULL判断)
     */
    NOT_EMPTY,
    /**
     * 默认的,一般只用于注解里
     * <p>1. 在全局里代表 NOT_NULL</p>
     * <p>2. 在注解里代表 跟随全局</p>
     */
    DEFAULT,
    /**
     * 不加入 SQL
     */
    NEVER
}

如果设置为IGNORE,即使字段为空也进行插入。

3.3 更新操作

@Test
public void update() {
    User user = new User(7L, "MP2", null, "mp2@atguigu.com");
    userMapper.updateById(user);
}
JDBC Connection [HikariProxyConnection@674247007 wrapping com.mysql.cj.jdbc.ConnectionImpl@da22aa] will not be managed by Spring
==>  Preparing: UPDATE user SET name=?, email=? WHERE id=?
==> Parameters: MP2(String), mp2@atguigu.com(String), 7(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4afd65fd]

3.4 查询操作

/**
 * 根据 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);

3.5 删除操作

/**
 * 根据 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);

3.6 通用 CRUD 小结

  1. 以上是基本的 CRUD操作,如您所见,我们仅仅需要继承一个 BaseMapper 即可实现大部分单表 CRUD操作。 BaseMapper 提供了很多方法给大家使用,可以极其方便的实现单一、批量、分页等操作。极大的减少开发负担,难道这就是 M P的强大之处了吗?
  1. 提出需求:
    现有一个需求,我们需要分页查询 Employee 表中,年龄在 18 ~ 50之间性别为男且姓名为 xx 的所有用户,这时候我们该如何实现上述需求呢?

    MyBatis:需要在 SQL映射文件中编写带条件查询的 SQL,并基于 PageHelper 插件完成分页。实现以上—个简单的需求,往往需要我们做很多重复单调的工作。普通的 Mapper 能够解决这类痛点吗?

    MP: 依旧不用编写 SQL语句, MP提供了功能强大的条件构造器 EntityWrapper。

4 条件构造器 QueryWrapper

4.1 AbstractWrapper

QueryWrapper(LambdaQueryWrapper) 和

UpdateWrapper(LambdaUpdateWrapper) 的父类

用于生成 sql 的 where 条件,entity 属性也用于生成 sql 的 where 条件

注意:entity 生成的 where 条件与使用各个 api 生成的 where 条件没有任何关联行为

所有的条件参数可以看官方文档,复制一大串在这里也挺没意思的。

4.1.1 继承关系

Wrapper:条件构造抽象类,最顶端父类,抽象类中提供4个方法西面贴源码展示

AbstractWrapper :用于查询条件封装,生成 sql 的 where 条件

AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。

LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper

LambdaUpdateWrapper : Lambda 更新封装Wrapper

QueryWrapper : Entity 对象封装操作类,不是用lambda语法

UpdateWrapper : Update 条件封装,用于Entity对象更新操作

4.2 QueryWrapper

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件及 LambdaQueryWrapper ,可以通过 new QueryWrapper().lambda() 方法获取

https://baomidou.com/pages/10c804/#querywrapper

4.2.1 查询分页示例

在查询分页需要进行如下配置

package com.atguigu.mp.mp01.config;

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;

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

给一个简单示例,一看就能明白了

@Test
public void wrapper() {
    Page<User> userPage = userMapper.selectPage(new Page<>(1, 2),
            new QueryWrapper<User>().like("name", "%MP%")
                    .between("age", 15, 25)
                    .orderByDesc("age")
    );
    System.out.println(JSON.toJSONString(userPage));
}

这个是日志,可以看到打印的SQL

2022-01-07 10:15:40.610  INFO 2573 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-07 10:15:40.804  INFO 2573 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1185122508 wrapping com.mysql.cj.jdbc.ConnectionImpl@3e39f08c] will not be managed by Spring
==>  Preparing: SELECT COUNT(*) AS total FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ?)
==> Parameters: %%MP%%(String), 15(Integer), 25(Integer)
<==    Columns: total
<==        Row: 3
<==      Total: 1
==>  Preparing: SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ?) ORDER BY age DESC LIMIT ?
==> Parameters: %%MP%%(String), 15(Integer), 25(Integer), 2(Long)
<==    Columns: id, name, age, email
<==        Row: 11, MP, 25, mp@atguigu.com
<==        Row: 6, MP, 22, mp@atguigu.com
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1d226f27]

如果不配置 MybatisPlusInterceptor ,MP 提供的分页方法是无效的,例如使用mysql,配置了 MybatisPlusInterceptor ,调用MP提供的分页方法,假设使用selectPage方法,他会执行两条sql语句,一条查询总记录数的sql,一条查询当前页记录的sql是带limit分页条件的。如果不配置,调用selectPage只会执行一条查询记录的sql,并且不带limit,有兴趣你可以试试,很容易就能验证出来的。

返回的结果如下:

{
    "current":1,
    "optimizeCountSql":true,
    "orders":[

    ],
    "pages":2,
    "records":[
        {
            "age":25,
            "email":"mp@atguigu.com",
            "id":11,
            "name":"MP"
        },
        {
            "age":22,
            "email":"mp@atguigu.com",
            "id":6,
            "name":"MP"
        }
    ],
    "searchCount":true,
    "size":2,
    "total":3
}

4.2.2 查询列表示例

测试一个带OR条件的例子

@Test
public void wrapper() {
    Page<User> userPage = userMapper.selectPage(new Page<>(1, 2),
            new QueryWrapper<User>().eq("name", "Jone")
                    .or(wrapper -> wrapper.eq("email", "test3@baomidou.com")
                            .and(subWrppaer -> subWrppaer.eq("name", "Tom"))
                    )
    );
    System.out.println(JSON.toJSONString(userPage));
}

打印的SQL如下

2022-01-07 10:23:16.429  INFO 2648 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-07 10:23:16.612  INFO 2648 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@547647542 wrapping com.mysql.cj.jdbc.ConnectionImpl@70c205bf] will not be managed by Spring
==>  Preparing: SELECT COUNT(*) AS total FROM user WHERE (name = ? OR (email = ? AND (name = ?)))
==> Parameters: Jone(String), test3@baomidou.com(String), Tom(String)
<==    Columns: total
<==        Row: 2
<==      Total: 1
==>  Preparing: SELECT id,name,age,email FROM user WHERE (name = ? OR (email = ? AND (name = ?))) LIMIT ?
==> Parameters: Jone(String), test3@baomidou.com(String), Tom(String), 2(Long)
<==    Columns: id, name, age, email
<==        Row: 1, Jone, 18, test1@baomidou.com
<==        Row: 3, Tom, 28, test3@baomidou.com
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb96449]
{"current":1,"optimizeCountSql":true,"orders":[],"pages":1,"records":[{"age":18,"email":"test1@baomidou.com","id":1,"name":"Jone"},{"age":28,"email":"test3@baomidou.com","id":3,"name":"Tom"}],"searchCount":true,"size":2,"total":2}

4.2.3 删除示例

@Test
public void wrapper() {
    userMapper.delete(new QueryWrapper<User>()
            .eq("id", 9)
            .eq("name", "MP")
    );
}
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e24592e] was not registered for synchronization because synchronization is not active
2022-01-07 10:44:14.443  INFO 2912 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-07 10:44:14.863  INFO 2912 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@545851930 wrapping com.mysql.cj.jdbc.ConnectionImpl@70a24f9] will not be managed by Spring
==>  Preparing: DELETE FROM user WHERE (id = ? AND name = ?)
==> Parameters: 9(Integer), MP(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e24592e]

4.3 UpdateWrapper

说明:

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件及 LambdaUpdateWrapper,可以通过 new UpdateWrapper().lambda() 方法获取!

public void wrapper() {
    /*
        根据 whereEntity 条件,更新记录
            Params:
            entity – 实体对象 (set 条件值,可以为 null)
            updateWrapper – 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
     */
    userMapper.update(new User(null, "Jerry", 22, "test11@baomidou.com"),
            new UpdateWrapper<User>().eq( "id", 11L)
                    .eq("email", "mp@atguigu.com")
    );
}
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f0a4e30] was not registered for synchronization because synchronization is not active
2022-01-07 10:35:04.705  INFO 2774 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-07 10:35:04.891  INFO 2774 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1370822209 wrapping com.mysql.cj.jdbc.ConnectionImpl@362a6aa5] will not be managed by Spring
==>  Preparing: UPDATE user SET name=?, age=?, email=? WHERE (id = ? AND email = ?)
==> Parameters: Jerry(String), 22(Integer), test11@baomidou.com(String), 11(Long), mp@atguigu.com(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f0a4e30]

5 ActiveRecord 活动记录

Active Record(活动记录 ),是一种领域模型模式,特点是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的 一行记录。

ActiveRecord 一直广受动态语言( PHP 、 Ruby 等)的喜爱,而 Java 作为准静态语言,对于 ActiveRecord 往往只能感叹其优雅,所以 MP也在 AR 道路上进行了一定的探索

5.1 如何使用 AR 模式

仅仅需要让实体类继承Model类且实现主键制定方法,即刻开启AR之旅。

新版本中不这么干也行

package com.atguigu.mp.mp01.bean;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class User extends Model<User> {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;

    @Override
    public Serializable pkVal() {
        return this.id;
    }
}

5.2 AR 的使用

这里就用一个新增举例子了,其他的都是一样的,看父类里面的方法和参数就行了 。

@Test
public void arInsert() {
    User user = new User();
    user.setName("Song");
    user.setEmail("sls@atguigu.com");
    user.setAge(35);

    boolean result = user.insert();
    System.out.println("result = " + result);
}

日志长这样

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@64f3ca6] was not registered for synchronization because synchronization is not active
2022-01-07 12:35:12.278  INFO 5830 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-07 12:35:12.462  INFO 5830 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@313145375 wrapping com.mysql.cj.jdbc.ConnectionImpl@6115846e] will not be managed by Spring
==>  Preparing: INSERT INTO user ( name, age, email ) VALUES ( ?, ?, ? )
==> Parameters: Song(String), 35(Integer), sls@atguigu.com(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@64f3ca6]
result = true

5.3 AR 小结

AR模式提供了一种更加便捷的方式实现 CRUD 操作,其本质还是调用的 MyBatis 对应的方法,类似于语法糖。

到此,我们简单领略了 MyBatis-Plus 的魅力与高效率,值得注意的一点是:我们提供了强大的代码生成器,可以快速生成各类代码,真正的做到了即开即用。

6 代码生成器

MP提供了大量的自定义设置,生成的代码完全能够满足各类型的需求

表及字段命名策略选择

在MP中,我们建议数据库表名和表字段名采用驼峰命名方式,如果采用下划线命名方式谙开启全局下划线开关。(如果表名字段名命名方式不一致请注解指定)我们建议最好保持一致。

这么做的原因是为了避免在对应实体类时产生的性能损耗,这样字段不用做映射就能直接和实体类对应。当然如果项目里不用考虎这点性能损耗,那么你采用下划线也是没问题的。只需要在生成代码时配置 dbColumnUnderline 属性就可以。

6.1 快速入门

Generator源码地址:

Github:https://github.com/baomidou/generator

Gitee:https://gitee.com/baomidou/generator

Maven仓库坐标

https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator

官方文档:https://baomidou.com/pages/779a6e/#安装

6.1.1 构建者模式

在我们展开使用代码生成器之前,有必要简单介绍下构建者模式Builder Pattern

由于生成器涉及大量的配置,之前旧版本普通为普通javabean配置方式,当配置属性足够多时,弊端就显现了代码不易编写、不易阅读。于是引入构建者设计模式来解决以上问题。

其实本质是一样,就是换了一个写法大家来观摩下:https://baomidou.com/guide/generator-new.html

普通的生成方式

package com.atguigu.mp.mp01.builder;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CodeGenerator {
    /**
     * 数据源配置
     */
    private String dataSource;
    /**
     * 包配置
     */
    private String packageConfig;
    /**
     * 策略配置
     */
    private String strategyConfig;

    /**
     * 执行代码生成
     */
    public void execute() {
        System.out.println(
                "CodeGenerator [dataSource=" + dataSource + ", packageConfig=" + packageConfig + ", strategyConfig="
                        + strategyConfig + "]");
        ;
    }
}

构建者模式

package com.atguigu.mp.mp01.builder;

/**
 * 代码生成器构建模式
 */
public class CodeGeneratorBuilder {
    /**
     * codeGenerator
     */
    private CodeGenerator codeGenerator;

    public CodeGeneratorBuilder(CodeGenerator codeGenerator) {
        this.codeGenerator = codeGenerator;
    }

    public static CodeGeneratorBuilder create(String dataSource) {
        CodeGenerator cg = new CodeGenerator();
        cg.setDataSource(dataSource);
        return new CodeGeneratorBuilder(cg);
    }

    public CodeGeneratorBuilder packageConfig(String packageConfig) {
        codeGenerator.setPackageConfig(packageConfig);
        return this;
    }

    public CodeGeneratorBuilder strategyConfig(String strategyConfig) {
        codeGenerator.setStrategyConfig(strategyConfig);
        return this;
    }

    public void execute() {
        codeGenerator.execute();
    }
}

测试

package com.atguigu.mp.mp01.builder;

public class Test {
    public static void main(String[] args) {
        // 普通模式
        CodeGenerator codeGenerator = new CodeGenerator();
        codeGenerator.setDataSource("mysql");
        codeGenerator.setPackageConfig("com.atguigu.mp.mp01");
        codeGenerator.setStrategyConfig("驼峰命名");
        codeGenerator.execute();

        // 构建者模式
        CodeGeneratorBuilder.create("mysql")
                .packageConfig("com.atguigu.mp.mp01")
                .strategyConfig("驼峰命名")
                .execute();
    }
}

6.1.2 代码生成测试

倒入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>

代码

package com.atguigu.mp.mp01.generator;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;

public class CodeGenerator {
    static final String URL = "jdbc:mysql://localhost:3306/mp?useSSL=false&useUnicode=true&characterEncoding=utf-8";

    public static void main(String[] args) {
        FastAutoGenerator.create(URL, "root", "12345678")
                // 全局配置
                .globalConfig(builder -> builder.outputDir("src/main/java"))
                // 策略配置
                .strategyConfig(builder -> builder.addInclude("user"))
                // 执行
                .execute();
    }
}

6.2 数据库配置

全部配置项地址:数据库配置

  • 添加指定数据库驱动依赖jar
  • 数据库网络可用,云主机注意防火墙、网络安全组对应端口是否开放
  • 严禁使用数据库关键词命名
  • 务必表字段添加注释
  • 多schema非默认必须指定

6.3 包配置

全部配置项地址:包配置

  • 生成当前项目目录System.getProperty("user.dir") + "/src/main/java"
  • 定义父目录parent
  • 自定义输出pathInfo
  • 模块化输出指定parent动态路径

例子:

package com.atguigu.mp.mp01.pkg;

import java.text.MessageFormat;
import java.util.Collections;

import com.atguigu.mp.mp01.generator.CodeGenerator;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;

public class Generater {
    public static void main(String[] args) {
        String pkgPath = MessageFormat.format("{0}/src/main/java", System.getProperty("user.dir"));
        FastAutoGenerator.create(CodeGenerator.URL, "root", "12345678")
                // 全局配置
                .globalConfig(builder -> builder
                        .author("Shibuya Kanon")
                        .commentDate("yyyy-MM-dd hh:mm:ss")
                        .outputDir(pkgPath)
                        .fileOverride()
                        .disableOpenDir())
                // 包配置
                .packageConfig(builder -> builder.parent("com.atguigu.mp.mp01")
                        .pathInfo(Collections.singletonMap(OutputFile.mapperXml,
                                MessageFormat.format("{0}/src/main/resources/mappers",
                                        System.getProperty("user.dir")))))
                // 策略配置
                .strategyConfig(builder -> builder.addInclude("user"))
                // 执行
                .execute();
    }
}
package com.atguigu.mp.mp01.pkg;

import java.text.MessageFormat;

import com.atguigu.mp.mp01.generator.CodeGenerator;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;

public class Generater {
    public static void main(String[] args) {
        String pkgPath = MessageFormat.format("{0}/src/main/java", System.getProperty("user.dir"));
        FastAutoGenerator.create(CodeGenerator.URL, "root", "12345678")
                // 全局配置
                .globalConfig(builder -> builder
                        .author("Shibuya Kanon")
                        .commentDate("yyyy-MM-dd hh:mm:ss")
                        .outputDir(pkgPath)
                        .fileOverride()
                        .disableOpenDir())
                // 包配置
                .packageConfig((scanner, builder) -> builder.parent("com.atguigu.mp" + scanner.apply("请输入你的模块名称:")))
                // 策略配置
                .strategyConfig((scanner, builder) -> builder.addInclude(scanner.apply("请输入表名:")))
                // 执行
                .execute();
    }
}

6.4 模版配置

全部配置:模版配置

  • 禁用模块disable
  • 自定义模板,先要了解下什么是模板引擎?
  • 渲染模板的参数怎么查看?
package com.atguigu.mp.mp01.template;

import java.text.MessageFormat;

import com.atguigu.mp.mp01.generator.CodeGenerator;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.TemplateType;

public class TemplateGenerator {
    public static void main(String[] args) {
        String pkgPath = MessageFormat.format("{0}/src/main/java", System.getProperty("user.dir"));
        FastAutoGenerator.create(CodeGenerator.URL, "root", "12345678")
                // 全局配置
                .globalConfig(builder -> builder
                        .author("Shibuya Kanon")
                        .commentDate("yyyy-MM-dd hh:mm:ss")
                        .outputDir(pkgPath)
                        .fileOverride()
                        .disableOpenDir())
                // 模板配置
                .templateConfig(builder -> builder.disable(TemplateType.XML, TemplateType.CONTROLLER))
                // 包配置
                .packageConfig((scanner, builder) -> builder.parent("com.atguigu.mp" + scanner.apply("请输入你的模块名称:")))
                // 策略配置
                .strategyConfig((scanner, builder) -> builder.addInclude(scanner.apply("请输入表名:")))
                // 执行
                .execute();
    }
}

6.5 注入配置

方法 说明 示例
beforeOutputFile(BiConsumer<TableInfo, Map<String, Object>>) 输出文件之前消费者
customMap(Map<String, Object>) 自定义配置 Map 对象 Collections.singletonMap("test", "baomidou")
customFile(Map<String, String>) 自定义配置模板文件 Collections.singletonMap("test.txt", "/templates/test.vm")
new InjectionConfig.Builder()
    .beforeOutputFile((tableInfo, objectMap) -> {
        System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size());
    })
    .customMap(Collections.singletonMap("test", "baomidou"))
    .customFile(Collections.singletonMap("test.txt", "/templates/test.vm"))
    .build();

7 插件扩展

7.1 Mybatis插件机制简介

1)插件机制

Mybatis通过插件(Interceptor)可以做到拦截四太对象相关方法的执行,根据需求,完成相关数据的动态改变。

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

2)插件原理

四大对象的每个对象在创建时,都会执行 interceptorChain.pluginAll() , 会经过每个插件对象的 plugin() 方法,目的是为当前的四大对象创建代理。代理对象就可以拦截到四大对象相关方法的执行,因为要执行四太对象的方法需要经过代理。

7.2 分页插件 PaginationInnerInterceptor

启用分页插件

package com.atguigu.mp.mp01.config;

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;

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

具体的使用方法之前已经有过样例了,这里就直接给一下官网的文档了。

7.2.1 支持的数据库

  • mysql,oracle,db2,h2,hsql,sqlite,postgresql,sqlserver,Phoenix,Gauss ,clickhouse,Sybase,OceanBase,Firebird,cubrid,goldilocks,csiidb
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库

👉 如果没有支持你需要的数据库,点击参考 Pull Request 我们会第一时间审核(opens new window)

7.2.2 属性介绍

属性名 类型 默认值 描述
overflow boolean false 溢出总页数后是否进行处理(默认不处理,参见 插件#continuePage 方法)
maxLimit Long 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
dbType DbType 数据库类型(根据类型获取应使用的分页方言,参见 插件#findIDialect 方法)
dialect IDialect 方言实现类(参见 插件#findIDialect 方法)

建议单一数据库类型的均设置 dbType

7.2.3 自定义的 mapper#method 使用分页

IPage<UserVo> selectPageVo(IPage<?> page, Integer state);
// or (class MyPage extends Ipage<UserVo>{ private Integer state; })
MyPage selectPageVo(MyPage page);
// or
List<UserVo> selectPageVo(IPage<UserVo> page, Integer state);
<select id="selectPageVo" resultType="xxx.xxx.xxx.UserVo">
    SELECT id,name FROM user WHERE state=#{state}
</select>

如果返回类型是 IPage 则入参的 IPage 不能为null,因为 返回的IPage == 入参的IPage

如果返回类型是 List 则入参的 IPage 可以为 null(为 null 则不分页),但需要你手动 入参的IPage.setRecords(返回的 List);

如果 xml 需要从 page 里取值,需要 page.属性 获取

7.2.4 其他

生成 countSql 会在 left join 的表不参与 where 条件的情况下,把 left join 优化掉。所以建议任何带有 left join 的sql,都写标准sql,即给于表一个别名,字段也要 别名.字段

7.3 不规范SQL拦截器

IllegalSQLInnerInterceptor:不规范SQL拦截器。

由于开发人员水平参差不齐,即使订了开发规范很多人也不遵守,SQL是影响系统性能最重要的因素,所以拦截掉不规范SQL语句。

拦截SQL类型的场景:

  1. 必须使用到索引,包含left join连接字段,符合索引最左原则

    • 如果因为动态SQL,bug导致update的where条件没有带上,全表更新上万条数据
    • 如果检查到使用了索引,SQL性能基本不会太差
  2. SQL尽量单表执行,有查询left join的语句,必须在注释里面允许该SQL运行,否则会被拦截,有left join的语句,如果不能拆成单表执行的SQL,请leader商量在做,SQL尽量单表执行的好处:

    • 查询条件简单、易于开理解和维护
    • 扩展性极强;(可为分库分表做准备)
    • 缓存利用率高
  3. where条件为空

  4. where条件使用了 !=

  5. where条件使用了 not 关键字

  6. where条件使用了 or 关键字

  7. where条件使用子查询

7.3.1 测试案例

配置类添加插件

package com.atguigu.mp.mp01.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.IllegalSQLInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
        return interceptor;
    }
}

测试执行,发现此SQL没有添加条件,被拦截

@Test
void interceptors() {
   List<User> users = userMapper.selectList(new QueryWrapper<>());
   System.out.println("users = " + users);
}

测试执行,发现此SQL没有添加条件,被拦截

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@320ff86f] was not registered for synchronization because synchronization is not active
2022-01-10 16:11:20.169  INFO 10213 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-10 16:11:20.365  INFO 10213 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@47865820 wrapping com.mysql.cj.jdbc.ConnectionImpl@23321be7] will not be managed by Spring
检查SQL是否合规,SQL:SELECT  id,name,age,email  FROM user
original SQL: SELECT  id,name,age,email  FROM user
SQL to parse, SQL: SELECT  id,name,age,email  FROM user
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@320ff86f]

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: 非法SQL,必须要有where条件
### The error may exist in com/atguigu/mp/mp01/mapper/UserMapper.java (best guess)
### The error may involve com.atguigu.mp.mp01.mapper.UserMapper.selectList
### The error occurred while executing a query
### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: 非法SQL,必须要有where条件
	... 

7.4 乐观锁插件

com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor

如果想实现如下需求:当要更新一条记录的时候,希望这条记录没有被别人更新

乐观锁的实现原理:

  1. 取出记录时,获取当前 version
  2. 更新时,带上这个 version
  3. 执行更新时,set version = yourVersion + 1 where version = yourVersion
  4. 如果 version 不对,就更新失败。

@Version用于注解实体字段,必须要有。

7.4.1 测试使用

增加数据库字段version

ALTER TABLE user
    ADD COLUMN version int DEFAULT NULL COMMENT '乐观锁'

代码中对version字段增加@Version注解

/**
 * 乐观锁
 */
@Version
private Integer version;

测试

@Test
void opLock() {
    User user = new User();
    user.setId(4L);
    user.setAge(50);
    user.setName("Sammy");
    user.setVersion(1);

    userMapper.updateById(user);
}
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fde6f05] was not registered for synchronization because synchronization is not active
2022-01-10 16:25:54.141  INFO 10436 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-10 16:25:54.330  INFO 10436 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@967022668 wrapping com.mysql.cj.jdbc.ConnectionImpl@4404a6b] will not be managed by Spring
检查SQL是否合规,SQL:UPDATE user  SET name=?,
age=?,

version=?  WHERE id=?   AND version=?
original SQL: UPDATE user  SET name=?,
age=?,

version=?  WHERE id=?   AND version=?
SQL to parse, SQL: UPDATE user  SET name=?,
age=?,

version=?  WHERE id=?   AND version=?
parse the finished SQL: UPDATE user SET name = ?, age = ?, version = ? WHERE id = ? AND version = ?
==>  Preparing: UPDATE user SET name=?, age=?, version=? WHERE id=? AND version=?
==> Parameters: Sammy(String), 50(Integer), 2(Integer), 4(Long), 1(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fde6f05]

7.5 其他

其余的插件可以在这里看到

8 自定义全局操作

根据 MyBatis Plus 的 AbstractSqlInjector 可以自定义各种你想要的SQL,注入到全局中,相当于自定义 MyBatis Plus 自动注入的方法。

8.1 AbstractSqlInjector

从源代码中可以看到一个抽象方法

/**
 * <p>
 * 获取 注入的方法
 * </p>
 *
 * @param mapperClass 当前mapper
 * @return 注入的方法集合
 * @since 3.1.2 add  mapperClass
 */
public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass,TableInfo tableInfo);

8.2 自定义注入器的应用1——逻辑删除

增加字段is_deleted

ALTER TABLE `user`
    ADD COLUMN `is_deleted` boolean

application.yml增加配置

# 配置日志
mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 1
      logic-not-delete-value: 0

实体类增加注解

/**
 * 是否删除
 */
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Boolean isDeleted;

测试

@Test
void logicDelete() {
    int delete = userMapper.deleteById(1L);
    System.out.println(delete);
}
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@8383a14] was not registered for synchronization because synchronization is not active
2022-01-10 17:56:09.772  INFO 11606 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-10 17:56:09.963  INFO 11606 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1640529691 wrapping com.mysql.cj.jdbc.ConnectionImpl@1857fe6c] will not be managed by Spring
检查SQL是否合规,SQL:UPDATE user SET is_deleted=1 WHERE id=?  AND is_deleted=0
original SQL: UPDATE user SET is_deleted=1 WHERE id=?  AND is_deleted=0
SQL to parse, SQL: UPDATE user SET is_deleted=1 WHERE id=?  AND is_deleted=0
parse the finished SQL: UPDATE user SET is_deleted = 1 WHERE id = ? AND is_deleted = 0
==>  Preparing: UPDATE user SET is_deleted=1 WHERE id=? AND is_deleted=0
==> Parameters: 1(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@8383a14]

9 公共字段自动填充

9.1 元数据处理器接口

com.baomidou.mybatisplus.core.handlers.MetaObjectHandler

元对象字段填充控制器抽象类,实现公共字段自动写入

所有入参的 MetaObject 必定是 entity 或其子类的 MetaObject


MetaObject:元对象,是 MyBatis 提供的一个用于更加方便、更加优雅的访问对象的属性,给对象的属性设置值的一个对象.还会用于包装对象。支持对 Object、Map、 Collection 等对象进行包装。

9.2 开发步骤

  1. 注解填充字段 @TableField(fill = FieldFill.INSERT) 查看 TableFill
  2. 自定义公共字段填充处理器。
  3. MP全局注入自定义公共字段填充处理器,

9.3 测试

在相关字段上增加注解

/**
 * 是否删除
 */
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Boolean isDeleted;
/**
 * 姓名
 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String name;

实现元数据处理器接口

package com.atguigu.mp.mp01.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;

@Slf4j
public class CustomMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("===================insertFill");
        // 添加逻辑删除的默认值
        this.setFieldValByName("name", "Jasmine", metaObject);
        this.setFieldValByName("isDeleted", false, metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("===================updateFill");
        this.setFieldValByName("name", "Jasmine", metaObject);
    }
}

测试

@Test
void insert() {
    User user = new User();
    user.setAge(10);
    user.setEmail("eddie@qq.com");
    user.setVersion(1);

    userMapper.insert(user);
}
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3321291a] was not registered for synchronization because synchronization is not active
2022-01-10 18:20:03.184  INFO 11885 --- [           main] c.a.m.m.handler.CustomMetaObjectHandler  : ===================insertFill
2022-01-10 18:20:03.188  INFO 11885 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-01-10 18:20:03.376  INFO 11885 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1853751897 wrapping com.mysql.cj.jdbc.ConnectionImpl@646e6d07] will not be managed by Spring
==>  Preparing: INSERT INTO user ( name, age, email, version, is_deleted ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: Jasmine(String), 10(Integer), eddie@qq.com(String), 1(Integer), false(Boolean)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3321291a]
posted @ 2022-01-10 18:23  我係死肥宅  阅读(588)  评论(0编辑  收藏  举报