MyBatis Plus Basic Usage
MyBatisPlus
快速入门
入门案例
需求:基于可签资料提供的项目,实现下列功能
- 新增用户功能
- 根据 id 查询用户
- 根据 id 批量查询用户
- 根据 id 更新用户
- 根据 id 删除用户
步骤:
-
引入 MyBatisPlus 的起步依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>MyBatisPlus 官方提供了起步依赖,其中集成了 Mybatis 和 MybatisPlus 的所有功能,并且实现了自动装配效果
因此我们可以用 MybatisPlus 的 起步依赖代替 Mybatis 的起步依赖
-
定义 Mapper
自定义的 Mapper 继承 MybatisPlus 提供的 BaseMapper 接口:
public interface UserMapper extends BaseMapper<User> {
}继承的时的泛型为操作的实体类的类型
继承后,就可以使用父类提供的方法来对数据库单表进行增删改查
常见注解
MybatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
-
如果不进行配置,则实体类属性和数据库表字段的对应关系如下:
- 类名驼峰转下划线作为表名
- 名为 id 的字段作为主键(属性中必须有一个字段作为主键!)
- 变量名驼峰转下划线作为表的字段名
-
如果要配置类名与表名、属性与字段的对应关系,则常用到如下几个注解:
-
@TableName:用来指定表名 -
@TableId:用来指定表中的主键字段信息属性 type 有三个常用值:
值 说明 idType.AUTOid 的值交由数据库自增长 idType.INPUTid 的值需要在插入时由程序员自己赋值 idType.ASSIGN_ID(默认值)id 的值由接口 IdentifierGenerator 的默认实现类 DefaultIdentifiterGenerator 中方法 nextId 通过雪花算法来生成 自增主键必须加上
@TableId(type = idType.AUTO) -
@TableField:用来指定表中的普通字段信息使用场景:
-
成员名与数据库字段名不一致
-
成员变量名以 is 开头,且是布尔类型,必须加上该注解来对应字段
反射时,is 开头,且是布尔类型的成员变量名的 is 会被去掉,导致无法与数据库字段对应
-
成员变量名与数据库关键字冲突,如:
// 加上 '' 作为转义字符
private Integer order; -
成员变量不是数据库字段,如:
private String address;
-
-
常见配置
MyBatisPlus 的配置项继承了 MyBatis 原生配置和一些自己特有的配置,例如:
官网配置说明文档:使用配置 | MyBatis-Plus (baomidou.com)
核心功能
条件构造器
MyBatisPlus 支持各种复杂的 where 条件,可以满足日常开发的所有需求
案例:基于 QueryWrapper 的查询
需求:
-
查询出名字中带 o 的,存款大于等于 1000 的人的 id、username、info、balance 字段
// 1.构建查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper); -
更新用户名为 jack 的用户余额为 2000
// 1.要更新的数据
User user = new User();
user.setBalance(2000);
// 2.更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.eq("username", "jack");
// 3.执行更新
userMapper.update(user, wrapper);
案例:基于 UpdateWrapper 的更新
需求:更新 id 为 1、2、4的用户的余额,扣 200
// 1.设置需要更新的 id |
避免字符串硬编码,所以推荐使用 LambdaWrapper 来动态获取 column,如:
// 1.构建查询条件 |
可以避免因实体类属性名变更而需要大幅改动原代码的情况
自定义 SQL
我们可以利用 MyBatisPlus 的 Wrapper 来构建复杂的 where 条件,然后自己定义 SQL 语句中剩下的部分
步骤:
-
基于 Wrapper 构建 where 条件
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
void updateBalanceByIds( LambdaQueryWrapper<User> wrapper, int amount);
-
自定义 SQL,并使用 wrapper 条件
<update id="updateBalanceByIds">
UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
Service 接口
基本用法
如果想要在 Service 实现类中减少代码,controller 直接调 service 实现类就可以实现基本的增删改查功能,其余的代码都不用写,就可以选择让 service 继承 MyBatisPlus 的一个接口 IService,具体步骤如下:
-
让 Service 接口 继承
IServicepublic UserService extends IService<User>{}
-
Service 的实现类继承类
ServiceImpl(实现了 IService 接口的所有方法)public UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{}
IService泛型填 Service 要操作的实体类ServiceImpl泛型第一个填 Service 实现业务功能要调用的 Mapper,第二个填 Service 要操作的实体类
开发基本业务接口
需求:基于 Restful 风格实现下面的接口:
-
新增用户
public void saveUser( UserFormDTO userDTO) {
// 1.把DTO拷贝到PO
User user = BeanUtil.copyProperties(userDTO, User.class);
// 2.新增
userService.save(user);
} -
删除用户
public void deleteUserById( Long id) {
// 删除
userService.removeById(id);
} -
根据id查询用户
public UserVO deleteUserById( Long id) {
// 1.查询
User user = userService.getById(id);
// 2.把PO拷贝到VO
return BeanUtil.copyProperties(user, UserVO.class)
} -
根据id批量查询
public List<UserVO> deleteUserByIds( List<Long> ids) {
// 1.查询
List<User> users = userService.listByIds(ids);
// 2.把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class)
}
开发复杂业务接口
-
根据id扣减余额
// controller
public void deduceBalanceById(
Long id
Integer money) {
// 删除
userService.deduceBalanceById(id, money);
}// service interface
public UserService extends IService<User>{
public void deduceBalanceById(Long id, Integer money);
}// serviceimpl
public UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
public void deduceBalanceById(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3.校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4.扣减余额
baseMapper.deduceBalanceById(id, money);
}
}// mapper
public interface UserMapper extends BaseMapper<User> {
void deduceBalanceById( Long id, Integer money);
}
IService 中的 Lambda 方法 —— 复杂条件查询或复杂更新
IService 中的 Lambda 查询
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
// serviceimpl |
IService 中的 Lambda 更新
需求:改造根据id修改用户余额的接口,要求如下:
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减口余额为 0,则将用户 status 修改为冻结状态(2)
// serviceimpl |
补充:业务中 vo、dto 等的关系
![]()
IService 批量新增
需求:批量插入 10万 条用户数据,并作出对比:
-
普通 for 循环插入:耗时200秒左右
- 该方式一条一条数据插入,总共提交了 10万 次的请求,十分低效
-
IService 的批量插入:耗时20秒左右
-
该方式每 1000 条数据插入一次,IService 的 saveBatch 方法会对这一千条数据进行 SQL 预编译,编译为 1000 条 SQL 语句,这样,总共提交的请求次数减少为 100000/1000 = 100 次,大大提高了效率
-
现在我们想进一步优化时间,于是我们想要 IService 预编译的 1000 SQL 语句合并为一条,这样,MySQL 总共会处理 100 条 SQL 语句,处理 100 次请求,相较于 100000 条 SQL语句和 100 次请求,耗时又可以减少,两种办法:
-
手写 SQL 语句,使用
foreach标签,将 1000 条数据写在一条 SQL 语句中 -
启用 MySQL 的重写批处理语句属性,在
application.properties的数据库连接区域配置spring.datasource.url=jdbc:mysql://localhost:3306/tlias?rewriteBatchedStatements=true
-
-
启用重写批处理语句属性后,执行时间只需要6秒,大大提高了批量插入效率
-
扩展功能
代码生成
- 使用插件——MyBatisPlus
- 在顶部工具栏的 Other 选项中配置数据库信息
- 在 Other 选项卡中选择
code generator,配置代码生成信息- module 填所属模块,若只有一个模块则空出
- TablePrefix 处填表名前缀,如表名为 tb_user,想要生成的实体类名为User,则填入 tb_
- Entity、Mapper等表单中填写想要对应生成类所属包的包名
Db 静态工具
用法与 IService 接口完全一致,区别在于 Db 是静态类,无法使用泛型,所以在方法中需要给出实体类的 class 对象。
Db静态工具的作用:避免 Service 实现类中的循环依赖问题
用法:
Db.methodName(parameters); |
逻辑删除
介绍:逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
-
在表中添加一个字段,标记数据是否被删除
-
当删除数据时把标记设置为 1,即将删除语句改为更新语句
UPDATE user SET deleted = 1 WHERE id = #{id} AND deleted = 0;
-
查询时只查询标记为 0 的数据
SELECT * FROM user WHERE deleted = 0;
但如果采用逻辑删除,则 MyBatisPlus 提供的方法将无法实现需求。
于是,MyBatisPlus 提供了逻辑删除功能,无需改变方法调用的方式,而是在底层自动修改 CRUD 语句。
我们需要做的,就是在 application.properties 文件中配置逻辑删除的字段名称和值即可:
# 全局逻辑删除的实体字段名,字段类型可以是Boolean和Integer |
注意:逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,影响查询效率
- SQL 中全都需要对逻辑删除字段做判断,影响查询效率
因此,逻辑删除不太推荐,如果实在需要,可以考虑将删除数据迁移到其他表来实现
枚举处理器
在用户实体类中,我们常常会用定义表示 状态 的整数类型的字段,但这样做难以记忆,且不直观,又因为枚举类型可以直接用 == 进行比较,所以我们通过用枚举类型来代替整数类型来表示用户状态。
// 用户实体类 |
// 用户状态枚举类 |
这样做虽然可以直观地表示用户的状态了,但是在将用户实体类插入数据库时又会遇到问题——枚举类型无法转换为数据库中的表示状态的数据类型(一般为 int),所以,为了解决这一问题,MyBatisPlus 为我们提供了枚举处理器 MybatisEnumTypeHandler 来实现枚举类型和数据库类型的转换。
使用步骤:
-
在枚举类定义中指明与数据库字段值对应的属性,如
UserStatus类型中value属性对应数据库中的status值:// 用户状态枚举类
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");
private final int value;
private final String description;
UserStatus(int value, String description) {
this.value = value;
this.description = description;
}
} -
使枚举处理器生效
mybatis-plus.configuration.default-enum-type-handler=com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
以上配置后,查询返回的值是枚举成员名 NORMAL 或 FREEZE,如果想要返回枚举类中的某个属性的值,则可以在想要返回的属性上加上
@JsonValue注解,如:
// 用户状态枚举类
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");
private final int value;
private final String description;
UserStatus(int value, String description) {
this.value = value;
this.description = description;
}
}
JSON 处理器
数据库数据类型中有 JSON 类型,保存为 JSON 字符串,插入数据时我们只需要将插入的数据声明为 String 类型,``Mybatis会自动将String` 类型转换为 JSON 类型。
但如果要从数据库中取出 JSON 类型的数据并做业务处理,由于取出的是字符串而不是一个对象,就要转换为对象才能做业务处理,十分繁琐,为了解决这个问题,``MyBatisPlus为我们提供了 JSON 处理器AbstractJsonTypeHandler` 抽象类,其有三个子类
GsonTypeHandlerJacksonTypeHandler:SpringMVC 底层使用的 JSON 处理器,推荐使用FastjsonTypeHandler
使用步骤:
-
在需要从
JSON转换为对象的属性上加上注解@TableField(typeHandler = JacksonTypeHandler.class) -
在该属性所属实体类的
@TableName注解中补充autoResultMap属性为true
public class User {
private Long id;
private String username;
private UserInfo info;
}
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}{
"age": 20,
"intro": "..",
"gender": "male"
}
如此,即可完成 JSON 到对象的转换
插件功能
分页插件
步骤:
-
首先,要在配置类中注册 MyBatisPlus 的核心插件,同时添加分页插件
public class MyBatisConfig {
public MyBatisPlusInterceptor myBatisPlusInterceptor() {
// 1.初始化核心插件
MyBatisPlusInterceptor interceptor = new MyBatisPlusInterceptor();
// 2.添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
} -
接着,就可以使用分页的 API 了:
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数,true升序,false降序
page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));
// 分页查询
Page<User> p = userService.page(page);
// 总条数
System.out.println("total = " + p.getTotal());
// 总页数
System.out.println("total = " + p.getPages());
// 分页数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
通用分页实体
// 通用分页实体 |
可以进行如下优化,封装重复的逻辑,提高代码复用性
