介绍
MyBatis-Plus 是一个 MyBatis 的增强工具,它在 MyBatis 的基础上只做增强不做改变,旨在简化开发流程和提高开发效率。它具备以下特性:
- 无侵入性:MyBatis-Plus 仅作为增强,不会对现有工程产生影响。
- 性能损耗小:自动注入基本的 CURD 操作,对性能的影响微乎其微。
- 强大的 CRUD 支持:内置了通用的 Mapper 和 Service,通过少量配置即可实现单表的大部分 CRUD 操作。
- Lambda 表达式支持:方便地编写查询条件,减少字段错误的可能性。
- 主键自动生成:支持多种主键策略,包括分布式唯一 ID 生成器。
- ActiveRecord 模式支持:通过继承 Model 类,实体类可以直接进行 CRUD 操作。
- 自定义全局操作支持:允许全局通用方法的注入。
- 内置代码生成器:可以快速生成 Mapper、Model、Service、Controller 层代码。
- 内置分页插件:基于 MyBatis 物理分页,简化分页查询的实现。
- 多数据库支持:支持包括 MySQL、Oracle、PostgreSQL 等在内的多种数据库。
- 性能分析插件:能够输出 SQL 语句及其执行时间,帮助优化慢查询。
- 全局拦截插件:提供操作智能分析阻断,预防误操作。
MyBatis-Plus 适用于需要简化数据库操作并提高开发效率的场景,尤其适合企业级应用开发。
使用
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
继承接口
实体类
实体类(POJO,Plain Old Java Object)通常不需要继承特定的接口,它们应该只是简单的 JavaBeans,具有属性和对应的 getter 和 setter 方法。MyBatis-Plus 通过注解来识别和处理实体类与数据库表之间的映射关系。不过,如果你想利用 MyBatis-Plus 提供的 ActiveRecord 特性,实体类可以继承 com.baomidou.mybatisplus.extension.activerecord.Model
接口。这样,实体类就可以直接使用 MyBatis-Plus 提供的一系列数据库操作方法,例如插入、更新、删除和查询等。
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import java.io.Serializable;
@Data
@TableName("user") // 指定表名
public class User extends Model<User> implements Serializable {
private Long id;
private String name;
private Integer age;
}
MybatisPlus根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:
- MybatisPlus会把PO实体的类名驼峰转下划线作为表名
- MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
- MybatisPlus会把名为id的字段作为主键
但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。
@TableName
@TableNmae:表名注解,标识实体类对应的表。
@TableName("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 |
@TableId
@TableId:主键注解,标识实体类中的主键字段
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
}
TableId
:
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 表名 |
type | Enum | 否 | IdType.NONE | 指定主键类型 |
IdType
:
值 | 描述 |
---|---|
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 方法) |
这里比较常见的有三种:
- AUTO:利用数据库的id自增长
- INPUT:手动生成id
- ASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略
@TableField
@TableField:普通字段注解
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
@TableField("isMarried")
private Boolean isMarried;
@TableField("concat")
private String concat;
}
一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。
- 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加转义字符:` `
TableField
:
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 数据库字段名 |
exist | boolean | 否 | true | 是否为数据库表字段 |
condition | String | 否 | “” | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s} |
update | String | 否 | “” | 字段 update set 部分注入,例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 |
select | boolean | 否 | true | 是否进行 select 查询 |
keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 |
jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) |
typeHandler | TypeHander | 否 | 类型处理器 (该默认值不代表会按照该值生效) | |
numericScale | String | 否 | “” | 指定小数点后保留的位数 |
Mapper
在使用 MyBatis-Plus 时,Mapper 接口通常需要继承 com.baomidou.mybatisplus.core.mapper.BaseMapper
接口。BaseMapper 提供了一系列基础的 CRUD 操作方法,这样在实现 Mapper 接口时,你无需自己编写这些基本操作方法的实现。
例如,如果你有一个 User 实体类,你可以创建一个对应的 Mapper 接口如下:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.model.User;
public interface UserMapper extends BaseMapper<User> {
// 这里可以添加一些自定义的方法
}
在这个例子中,UserMapper 接口继承了 BaseMapper 接口,并且指定了 User 作为泛型参数。这样,UserMapper 接口就自动拥有了 MyBatis-Plus 提供的所有基本 CRUD 操作,包括但不限于:
insert(T entity)
:插入一条记录。deleteById(Serializable id)
:根据 ID 删除记录。updateById(T entity)
:根据 ID 更新记录。selectById(Serializable id)
:根据 ID 查询记录。selectList(Wrapper<T> queryWrapper)
:根据条件构造器查询列表。
通过继承 BaseMapper,你可以极大地简化数据访问层的代码,同时利用 MyBatis-Plus 提供的扩展功能,比如条件构造器、分页插件等。如果需要额外的自定义数据库操作方法,也可以在 Mapper 接口中添加自定义方法,并在对应的 Mapper XML 文件或使用 MyBatis-Plus 的注解方式提供实现。
Service层
在 MyBatis-Plus 中,Service 层通常不需要继承特定的接口,因为 MyBatis-Plus 已经通过 Mapper 接口提供了基本的 CRUD 功能。Service 层的主要作用是业务逻辑的处理,你可以自由地添加自己的方法来实现业务需求。
然而,MyBatis-Plus 提供了一个 IService
接口,它可以作为一个服务层的基础接口,提供一些通用的业务操作方法。如果你想要利用 MyBatis-Plus 的一些内置服务功能,你可以让你的服务类实现这个接口。IService 接口提供了一些方法,比如批量保存、根据条件查询等,但这些方法通常在实际开发中使用较少,因为业务逻辑往往需要更定制化的方法。
例如,你可以这样定义一个服务接口:
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.model.User;
public interface UserService extends IService<User> {
// 在这里添加你的业务方法
}
在对应的ServiceImpl实现这个服务接口:
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 实现业务方法
@Override
public void someBusinessMethod() {
// 业务逻辑代码
}
}
在这个例子中,UserServiceImpl 继承了 MyBatis-Plus 提供的 ServiceImpl 类,并实现了 UserService 接口。ServiceImpl 类已经实现了 IService 接口,因此你可以直接使用它提供的所有方法,同时也可以在 UserServiceImpl 中添加自定义的业务方法。
总的来说,Service 层的设计取决于你的业务需求,MyBatis-Plus 提供的 IService 和 ServiceImpl 主要是为了提供便利和扩展性,但不是强制性要求。
插件
在使用MybatisPlus以后,基础的Mapper、Service、PO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用,也很麻烦。MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。
首先需要配置数据库地址,在Idea顶部菜单中,找到Tools,选择Config Database:
在弹出的窗口中填写数据库连接的基本信息:
点击OK保存。
然后再次点击Idea顶部菜单中的other,然后选择Code Generator,在弹出的表单中填写信息:
JSON类型处理器
对于数据库表中为JSON类型的字段,要读取该字段的属性时就非常不方便。如果要方便获取,在定义实体类POJO的该字段时最好是一个Map或者实体类。而一旦我们把这个字段改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。
具体做法是在实体类上加上一个
@TableName(autoResultMap = true)
注解,在对应的字段上加上@TableField(typeHandler = ListLongTypeHandler.class)
package com.itheima.mp.domain.po;
import lombok.Data;
@Data
@TableName(value = "UserInfo ", autoResultMap = true)
public class UserInfo {
private Integer age;
private String intro;
private String gender;
@TableField(typeHandler = ListLongTypeHandler.class)
private OtherInfo otherInfo;
}
分页
配置分页插件
在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IService和BaseMapper中的分页方法都无法正常起效。
所以,我们必须配置分页插件。
package com.itheima.mp.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 MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
使用
分页查询,将mybatis-plus的IPage对象作为Controller的入参,导致前端swagger接口文档显示很多不需要的入参的问题。
可以先写一个通用分页入参的基类
@Data
@ApiModel(description = "基础分页入参")
public class BasePageReq {
@ApiModelProperty(value = "当前页")
@NotNull
private Integer current;
@ApiModelProperty(value = "每页行数")
@NotNull
@Range(min = 1, max = 999)
private Integer size;
}
然后自己的分页业务业务入惨继承这个基类
@Data
public class MenuPermissionPageRequest extends BasePageReq {
@ApiModelProperty(value = "租户id", hidden = true)
private Long tenantId;
@ApiModelProperty(value = "菜单名称")
private String searchMenuName;
@ApiModelProperty(value = "权限名称")
private String searchPermissionName;
@ApiModelProperty(value = "权限编码")
private String searchPermissionCode;
}
将这个类定义作为Controller入参
@GetMapping("page/list")
@ApiOperation("分页查询菜单操作权限列表(关键字搜索)")
public Result<IPage<MenuPermissionInfoResponse>> getPermissionPageList(@Valid MenuPermissionPageRequest req) {
IPage<MenuPermissionInfoResponse> pageList = menuPermissionFacade.getMenuPermissionPageList(req);
return Result.ok(pageList);
}
查询的时候重新构造mybatis-plus的IPage对象作为Dao层的分页查询的入参。
public IPage<MenuPermissionInfoResponse> getMenuPermissionPageList(MenuPermissionPageRequest req) {
Long tenantId = 2L;
req.setTenantId(tenantId);
IPage<MenuPermissionInfoResponse> page = new Page<>(req.getCurrent(), req.getSize());
List<MenuPermissionInfoResponse> list = menuPermissionService.getMenuPermissionPageList(page, req);
page.setRecords(list);
return page;
}
分页查询中,对于返回值实体类如何方便的转换到VO给前端
可以通过ipage的convert的方法,方便的转化记录的类型。
@Override
public IPage<GroupApplyPermissionVO> listGroupApplyPermission(GroupApplyPermissionPageReq req) {
Long schoolId = BizContext.getUser().getSchoolId();
IPage<ApplyAuth> page = new Page<>(req.getCurrent(), req.getSize());
LambdaQueryChainWrapper<ApplyAuth> wrapper = this.lambdaQuery().eq(ApplyAuth::getSchoolId, schoolId);
wrapper.orderByDesc(ApplyAuth::getCreatedOn);
wrapper.page(page);
return page.convert(e -> {
GroupApplyPermissionVO groupApplyPermissionVO = new GroupApplyPermissionVO();
groupApplyPermissionVO.setAuthId(e.getId())
.setStuId(e.getKidId())
.setStuName(e.getKidName())
.setUserId(e.getUserId())
.setUserName(e.getUserName())
.setClazzId(e.getClazzId())
.setClazzName(e.getClazzName());
return groupApplyPermissionVO;
});
}
一个问题:修改人和修改时间的自动填充问题
update不带实体类, 是无法触发字段填充,以下代码在更新时,框架不会自动填充修改人和修改时间
return activityService.lambdaUpdate()
.eq(Activity::getId, id)
.set(Activity::getIsCancelled, Boolean.TRUE)
.update();
以上代码输出以下sql
UPDATE
sc_menu_permission
SET
is_deleted=true
WHERE
is_deleted=0
AND (
tenant_id = 2
AND id IN (
178
)
);
解决方法:构建一个空的实体类作为update方法的参数
@Override
public void deleteMenuPermissionByTenantIdAndIds(Long tenantId, List<Long> permissionIds) {
super.lambdaUpdate()
.eq(MenuPermission::getTenantId, tenantId)
.in(MenuPermission::getId, permissionIds)
.set(MenuPermission::getIsDeleted, Boolean.TRUE)
.update(new MenuPermission());
}
以上代码输出以下sql
UPDATE
sc_menu_permission
SET
modified_by=0,
modified_on=2022-06-16T21:07:54.988,
is_deleted=true
WHERE
is_deleted=0
AND (
tenant_id = 2
AND id IN (
178
)
);