MyBatisPlus

🐦MyBatis-Plus

[TOC]

🚪 1. 快速入门

1.1 入门案例

入门案例:基于课前资料提供的项目,实现下列功能:

  • 新增用户功能
  • 根据 id 查询用户
  • 根据 id 批量查询用户
  • 根据 id 更新用户
  • 根据 id 删除用户
1.1.1 引入 MyBatis-Plus 的依赖
1
2
3
4
5
6
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
1.1.2 定义 Mapper

自定义的 Mapper 继承 MybatisPlus提供的 BaseMapper 接口。

BaseMapper<T> 给我们提供了很多基础方法,如常用的增、删、改、查等操作。

BaseMapperInterface

1
public interface UserMapper extends BaseMapper<User> { }
1.1.3 直接使用

做完上面两步操作,此时我们不需要再编写复杂的 SQL 语句即可对数据库进行增、删、改、查操作。

当然对于原有 MyBatis 操作,我们也可以直接使用,因为 MyBatis-PlusMyBatis 是非侵入的,体现了其润物无声的特点。

1.2 常见注解

在初次使用 MyBatis-Plus 时,我们并没有指定要执行操作的表信息和字段信息,那么 Mybatis-Plus 是如何知道我们要查询的是哪张表?表中有哪些字段呢?

MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。

在入门案例中 1.1.2 定义 Mapper 时,我们需要特别指定泛型类即 <User>。泛型中的 User 就是与数据库对应的 PO。

MyBatis-Plus 就是根据 PO 实体的信息来推断出表的信息,从而生成 SQL 的。默认情况下:

  • MyBatis-Plus 会把 PO 实体的类名驼峰转下划线作为表名,如类名为 UserInfo,反射的表名为 user_info
  • MyBatis-Plus 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型;
  • MyBatis-Plus 会把名为 id 的字段作为主键。

但很多情况下,默认的实现与实际场景不符,因此 MyBatis-Plus 提供了一些注解便于我们声明表信息。

1.2.1 @TableName 注解
  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
1
2
3
4
5
6
/* 标注实体类对应的表名 */
@TableName("t_user")
public class User {
private Long id;
private String name;
}

@TableName 注解除了指定表名以外,还可以指定很多其它属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
schema String “” schema
keepGlobalPrefix boolean false 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMap String “” xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMap boolean false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludeProperty String[] {} 需要排除的属性名 @since 3.3.1
1.2.2 @TableId 注解
  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段
1
2
3
4
5
6
@TableName("t_user")
public class User {
@TableId(value="id", type=IdType.AUTO)
private Long id;
private String name;
}

TableId 注解支持两个属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
type Enum IdType.NONE 指定主键类型

IdType支持的类型有:

描述
AUTO 数据库 ID 自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT
INPUT insert 前自行 set 主键值
ASSIGN_ID 分配 ID(主键类型为 NumberLongInteger)或(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 32UUID 字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)

这里比较常见的有三种:

  • AUTO:利用数据库的 id 自增长
  • INPUT:手动生成 id
  • ASSIGN_ID:雪花算法生成 Long 类型的全局唯一 id,这是默认的ID策略
1.2.3 @TableField 注解
  • 描述:普通字段注解

  • 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@TableName("user")
public class User {
@TableId(value="u_id", type=IdType.AUTO)
private Long id;

@TableField("username") // 成员变量名与数据库字段名不一致
private String name;

private Integer age;

@TableField("isMarried") // 成员变量是以 `isXXX` 命名
private Boolean isMarried;

@TableField("`concat`") // 成员变量名与数据库一致, 但是与数据库的关键字冲突
private String concat;

@TableField(exist = false) // 成员变量不是数据库字段
private String address;
}

一般情况下我们并不需要给字段添加 @TableField 注解,一些特殊情况除外:

  • 成员变量名与数据库字段名不一致;
  • 成员变量是以 isXXX 命名,按照JavaBean的规范,MyBatis-Plus识别字段时会把 is 去除,这就导致与数据库不符;
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用 @TableField 注解给字段名添加转义字符:``
  • 成员变量不是数据库字段,添加 exist = false

支持的其它属性如下:

属性 类型 必填 默认值 描述
value String “” 数据库字段名
exist boolean true 是否为数据库表字段
condition String “” 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s}参考(opens new window)
update String “” 字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategy Enum FieldStrategy.DEFAULT 举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
updateStrategy Enum FieldStrategy.DEFAULT 举例:IGNORED update table_a set column=#{columnProperty}
whereStrategy Enum FieldStrategy.DEFAULT 举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
fill Enum FieldFill.DEFAULT 字段自动填充策略
select boolean true 是否进行 SELECT 查询
keepGlobalFormat boolean false 是否保持使用全局的 format 进行处理
jdbcType JdbcType JdbcType.UNDEFINED JDBC 类型 (该默认值不代表会按照该值生效)
typeHandler TypeHander 类型处理器 (该默认值不代表会按照该值生效)
numericScale String “” 指定小数点后保留的位数

1.3 常见配置

MyBatis-Plus 也支持基于 yaml 文件的自定义配置,详见官方文档:

MyBatis-Plus 官方文档

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:

  • 实体类的别名扫描包
  • 全局id类型
1
2
3
4
5
6
7
8
9
10
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。
configuration:
mapper-underscore-to-camel-case: true # 是否开启下划线驼峰映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: auto # 全局 id 类型为自增长, 可以更换为其他方法, 如 assign_id
update-strategy: not_null # 更新策略: 只更新非空字段

需要注意的是,MyBatis-Plus也支持手写 SQL 的,而 mapper 文件的读取地址可以通过 mapper-locations 配置:默认值是classpath*:/mapper/**/*.xml,也就是说我们只要把 mapper.xml 文件放置这个目录下就一定会被加载。

🥑 2. 核心功能

入门案例中都是以 id 为条件的简单CRUD,一些复杂条件的 SQL 语句就要用到一些更高级的功能了。

2.1 条件构造器

除了新增以外,修改、删除、查询的 SQL 语句都需要指定 WHERE 条件。因此 BaseMapper 中提供的相关方法除了以 id 作为 WHERE 条件以外,还支持更加复杂的 WHERE条件。

img

参数中的 Wrapper 就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

img

Wrapper 的子类 AbstractWrapper 提供了 WHERE 中包含的所有条件构造方法:

img

QueryWrapperAbstractWrapper 的基础上拓展了一个 SELETE 方法,允许指定查询字段:

img

UpdateWrapperAbstractWrapper 的基础上拓展了一个 SET 方法,允许指定 SQL 中的 SET 部分:

img

接下来,我们就来看看如何利用 Wrapper 实现复杂查询。

2.1.1 QueryWrapper

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。

当前表的结构如下:

# 名称 数据类型 注释 长度/集合 默认
1 id BIGINT 用户ID 19 AUTO_INCREMENT
2 username VARCHAR 用户名 50 无默认值
3 password VARCHAR 密码 128 无默认值
4 phone VARCHAR 注册手机号 20 NULL
5 info JSON 详细信息 无默认值
6 status INT 使用状态(1: 正常 2: 冻结) 10 “1”
7 balance INT 账户余额 10
8 create_time DATETIME 创建时间 CURRENT_TIMESTAMP
9 update_time DATETIME 更新时间 CURRENT_TIMESTAMP

接下来看一些例子:

查询:查询出名字中带o的,存款大于等于1000元的人。代码如下:

1
2
3
SELECT id,username,info,balance
FROM user
WHERE username LIKE "%o%" AND Balance >= 100
1
2
3
4
5
6
7
8
9
10
11
@Test
void testQueryWrapper() {
// 1. 构建查询条件 WHERE name LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2. 查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

更新:更新用户名为 Jack 的用户的余额为 2000,代码如下:

1
2
3
UPDATE user
SET balance=2000
WHERE (username="Jack")
1
2
3
4
5
6
7
8
9
10
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 WHERE name="Jack"
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");

// 2.更新数据, user 中非 NULL 字段都会作为 SET 语句
User user = new User();
user.setBalance(2000);
userMapper.update(user, wrapper);
}
2.1.2 UpdateWrapper

基于 BaseMapper 中的 UPDATE 方法更新时只能直接赋值,对于一些复杂的需求就难以实现。

例如:更新 id124 的用户的余额,扣 200,对应的SQL应该是:

1
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET 的赋值结果是基于字段现有值的,这个时候就要利用 UpdateWrapper 中的 setSql 功能了:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") // SET balance = balance - 200
.in("id", ids); // WHERE id in (1, 2, 4)

// 2.更新, 注意第一个参数可以给null, 也就是不填更新字段和数据,
// 而是基于 UpdateWrapper 中的 setSQL 来更新
userMapper.update(null, wrapper);
}
2.1.3 LambdaQueryWrapper

无论是 QueryWrapper 还是 UpdateWrapper 在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?

其中一种办法是基于变量的 getter 方法结合反射技术。因此我们只要将条件对应的字段的 getter 方法传递给 MyBatis-Plus,它就能计算出对应的变量名了。而传递方法可以使用 JDK8 中的 方法引用Lambda 表达式。 因此 MyBatis-Plus 又提供了一套基于LambdaWrapper,包含两个:

  • LambdaQueryWrapper
  • LambdaUpdateWrapper

分别对应 QueryWrapperUpdateWrapper

其使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

2.2 自定义 SQL

一般企业的开发规范中,是不允许开发人员将 SQL 脱离出 Mapper 层或者 mapper.xml 文件的,但是我们想要使用 MyBatis-Plus 又想要遵守企业的开发规范,应该如何做呢?

我们可以利用 MyBatis-PlusWrapper 来构建复杂的 WHERE 条件,然后自己定义 SQL 语句中剩下的部分。

① 基于 Wrapper 构造 WHERE 条件

1
2
3
4
5
6
7
8
List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;

// 1. 构造条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);

// 2. 自定义SQL方法调用
userMapper.updateBalanceByIds(wrapper, amount);

② 在 mapper 方法参数中用 @Param 注解声明 Wrapper 变量名称,这个名称必须是 ew

1
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

③ 自定义 SQL,并使用 Wrapper 条件,这里 ew.customSqlSegmentWrapper 的一个方法,用于获取用户自定义SQL片段。如果使用出现问题,参考博客:MyBatis-Plus ${ew.customSqlSegment} 使用的史诗级大坑-CSDN博客

1
2
3
<update id="updateBalanceByIds">
UPDATE t_user SET balance = balance - #{amount} ${ew.customSqlSegment};
</update>

2.3 Service 接口

MyBatis-Plus 不仅提供了 BaseMapper,还提供了通用的 Service 接口及默认实现,封装了一些常用的 service 模板方法。 通用接口为 IService,默认实现为 ServiceImpl,其中封装的方法可以分为以下几类:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

IServiceInterface

2.3.1 CRUD

首先了解基本的CRUD接口。

新增

  • save 是新增单个元素
  • saveBatch 是批量新增
  • saveOrUpdate 是根据id判断,如果数据存在就更新,不存在则新增
  • saveOrUpdateBatch 是批量的新增或修改

删除:

IServiceRemove
  • removeById:根据id删除
  • removeByIds:根据id批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper<T>):根据Wrapper条件删除
  • ~~removeBatchByIds~~:暂不支持

修改:

IServiceUpdate

  • updateById:根据id修改
  • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含setwhere部分
  • update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

Get:

IServiceGet

  • getById:根据 id 查询 1 条数据
  • getOne(Wrapper<T>):根据Wrapper 查询 1 条数据
  • getBaseMapper:获取Service 内的 BaseMapper 实现,某些时候需要直接调用 Mapper 内的自定义 SQL 时可以用这个方法获取到 Mapper

List:

IServiceList

  • listByIds:根据id批量查询
  • list(Wrapper<T>):根据Wrapper条件查询多条数据
  • list():查询所有

Count

IServiceCount

  • count():统计所有数量
  • count(Wrapper<T>):统计符合Wrapper条件的数据数量

getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:

img

2.3.2 基本用法

由于 Service 中经常需要定义与业务有关的自定义方法,因此我们不能直接使用 IService,而是自定义 Service 接口,然后继承 IService 以拓展方法。同时,让自定义的 Service实现类 继承 ServiceImpl,这样就不用自己实现 IService 中的接口了。

具体方法如下:

① 首先,定义 IUserService 类,继承 IService

1
2
3
4
5
6
7
8
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {
// 拓展自定义方法
}

② 然后,编写 UserServiceImpl 类,继承 ServiceImpl,实现 UserService

1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.service.IUserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

}

项目结构如下:

img

接下来,我们基于 RESTful 风格快速实现下面几个接口(什么是RESTful风格):

简单理解 REST 就是:URL 中只使用名词来定位资源,用 HTTP 协议里的动词(GETPOSTPUTDELETE)来实现资源的增删改查操作。

什么是 RESTful

  • 使用客户/服务器(B/S、 C/S)模型:客户和服务器之间通过一个统一的接口来互相通讯。
  • 层次化的系统:在一个 REST 系统中,客户端并不会固定地与一个服务器打交道。
  • 无状态:在一个 REST 系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都需要提供足够的信息。
  • 可缓存:REST 系统需要能够恰当地缓存请求,以尽量减少服务端和客户端之间的信息传输,以提高性能。
  • 统一的接口。一个 REST 系统需要使用一个统一的接口来完成子系统之间以及服务与用户之间的交互。这使得 REST 系统中的各个子系统可以独自完成演化。

如果一个系统满足了上面所列出的五条约束,那么该系统就被称为是 RESTful 的。

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户 id
3 根据 id 查询用户 GET /users/{id} 用户 id 用户VO
4 根据 id 批量查询 GET /users 用户 id 集合 用户VO集合
5 根据 id 扣除余额 PUT /users/{id}/deduction/{money} 1. 用户 id
2. 扣除金额

🕸 3. 拓展功能

🧩 4. 插件功能

作者

NilEra

发布于

2024-07-30

更新于

2024-07-31

许可协议

评论