Mybatis-Plus
快速入门
首先,引入 maven 依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
接着,让需要使用的 Mapper 接口继承 mybatis-plus 提供的 BaseMapper<T>
接口:
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
// 注意BaseMapper中需要指定泛型
}
继承之后,就可以使用 BaseMapper<T>
提供的基础单表增删改查方法,提高开发效率。
常见注解
MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。默认的约定如下:
- 类名驼峰转下划线为默认映射表名。
- 名为 id 的字段默认映射主键。
- 变量名驼峰转下划线映射为表的字段名。
如果需要自主配置,则需要利用注解,常见的注解如下(下述的三个注解是指定在实体类上的):
- @TableName:用来指定表名。
- @TableId:用来指定表中的主键字段信息。
- @TableField:用来指定表中的普通字段信息。
@TableField 注解的使用场景需要特别注意:
- 成员变量名与数据库字段名不一致。
- 成员变量名以
is
开头,且是布尔值。 - 成员变量名与数据库关键字冲突,此时需要使用转义字符。如:
@TableField("`order`")
。 - 成员变量不是数据库字段,此时需要主动标识其不存在。如:
@TableField(exist = false)
。
示例如下:
@TableName("tb_account")
public class Account {
@TableId(value = "id", type = IdType.AUTO)
/**
* IdType:
* AUTO-数据库自增长
* INPUT-通过set方法自行输入
* ASSIGN_ID-雪花算法分配id
* NONE-无特定生成策略(默认)
* ASSIGN_UUID-UUID分配id
*/
private Integer id;
@TableField("username")
private String name;
@TableField("money")
private Integer money;
}
常见配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml # Mapper.xml地址
type-aliases-package: cn.hnu.mybatisplus.entity # 别名包
configuration:
map-underscore-to-camel-case: true # 自动驼峰映射
cache-enabled: true # 开启二级缓存
global-config:
db-config:
id-type: auto # 全局id生成模式
update-strategy: not_null # 更新策略:只更新非空字段
核心功能
条件构造器
MybatisPlus 支持各种复杂的 where 条件,可以满足日常开发的所有需求。
基于 QueryWrapper 的查询
void testQueryWrapper() {
/**
* 示例sql:
* select id, name, money
* from account
* where name like ? and money >= ?
*/
// 构建查询条件
QueryWrapper<Account> wrapper = new QueryWrapper<Account>()
.select("id", "name", "money")
.like("name", "o")
.ge("money", 1000);
// 查询
List<Account> accounts = accountMapper.selectList(wrapper);
accounts.forEach(System.out::println);
}
void testUpdateByQueryWrapper() {
/**
* 示例sql:
* update account
* set money = 3000
* where name = '张三'
*/
// 要更新的数据,直接赋值
Account account = Account.builder().money(3000).build();
// 更新条件
QueryWrapper<Account> wrapper = new QueryWrapper<Account>()
.eq("name", "张三");
// 执行更新
accountMapper.update(account, wrapper);
}
基于 UpdateWrapper 的更新
void testMybatisPlus() {
/**
* 示例sql:
* update account
* set money = money - 200
* where id in (1, 2)
*/
UpdateWrapper<Account> wrapper = new UpdateWrapper<Account>()
.setSql("money = money - 200")
.in("id", List.of(1, 2));
accountMapper.update(null, wrapper);
}
基于 LambdaWrapper 的查询
Lambda 类型的 Wrapper 目的是为了让我们尽量少写硬编码的代码,采用函数式编程的思想,使用方法代替字段的硬编码,底层可以通过反射获取方法对应字段。为了方便,这里只演示 LambdaQueryWrapper
的使用:
void testMybatisPlus() {
/**
* 示例sql:
* select id, name, money
* from account
* where name like ? and money >= ?
*/
// 构建查询条件
LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<Account>()
.select(Account::getId, Account::getName, Account::getMoney)
.like(Account::getName, "o")
.ge(Account::getMoney, 1000);
// 查询
List<Account> accounts = accountMapper.selectList(wrapper);
accounts.forEach(System.out::println);
}
自定义 SQL
我们可以利用 MyBatisPlus 的 Wrapper 来构建复杂的 where 条件,然后自己定义 SQL 语句中剩下的部分。步骤如下:
基于 Wrapper 构建 where 条件:
void testMybatisPlus() { /** * 示例sql: * update account * set money = money - #{value} * where id in * <foreach collection="ids" separator="," item="id" open="(" close=")"> * #{id} * </foreach> */ // 更新条件 List<Integer> ids = List.of(1, 2); int value = 200; // 构造where条件 QueryWrapper<Account> wrapper = new QueryWrapper<Account>() .in("id", ids); // 调用自定义方法 accountMapper.updateMoneyByIds(wrapper, value); }
在 mapper 方法参数中用 Param 注解声明 wrapper 变量名称(必须是
ew
):@Mapper public interface AccountMapper extends BaseMapper<Account> { void updateMoneyByIds(@Param("ew") QueryWrapper<Account> wrapper, int value); }
在 xml 文件中手写 sql 剩下部分的语句,使用
${ew.customSqlSegment}
进行 sql 拼接:<update id="updateMoneyByIds"> update account set money = money - #{value} ${ew.customSqlSegment} </update>
Service 接口
IService 接口提供了一系列在 Service 层执行的单表 CRUD 方法,方便我们更高效地进行单表增删改查。使用的时候,基本的 Service 接口需要继承 ISerivce,然后实现类需要集成 ServiceImpl,并指定相关的泛型:
// service
public interface IAccountService extends IService<Account> {}
// impl
@Service
public class IAccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {}
// 这里的ServiceImpl泛型指定了AccountMapper,所以直接使用ServiceImpl自带的baseMapper也可以
接着使用的时候只需要调用 Service 层的对应方法即可:
void testService() {
// 新增
Account account = Account.builder().name("赵六").money(100).build();
accountService.save(account);
// 查询
List<Account> accounts = accountService.listByIds(List.of(1, 2));
accounts.forEach(System.out::println);
}
在 Service 层中,也可以使用 lambda 类型方法进行查询:
/**
* 多条件复杂查询
* @param account
* @return
*/
@Override
public List<Account> queryAccounts(Account account) {
// 构建条件
return lambdaQuery() // 三个参数,第一个参数等价于where当中的动态if条件,第二个参数是操作的数据库字段,第三个参数是实际的数据
.like(account.getName() != null, Account::getName, account.getName())
.eq(account.getMoney() != null, Account::getMoney, account.getMoney())
.list();
}
扩展功能
静态工具
在处理多表分步查询的时候,有时候为了防止依赖的循环注入问题,可以使用静态工具 Db
,结合对应表实体类的字节码文件可以在一个 Service 中操作另一张表:
/**
* 根据员工id查询部门名称
* @param id
* @return
*/
@Override
public String queryEmpAndDept(Integer id) {
// 查询员工
Emp emp = getById(id);
if (emp == null) {
throw new RuntimeException("员工不存在");
}
// 多表查询
Dept dept = Db.lambdaQuery(Dept.class).eq(Dept::getId, emp.getDeptId()).one();
return dept.getName();
}
逻辑删除
MybatisPlus 提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改 CRUD 的语句。我们要做的就是在 application.yml
文件中配置逻辑删除的字段名称和值即可(删除时还是调用 remove
方法,不过此时需要保证对应的实体类有逻辑删除的字段,此时就会更改这个字段而不是实际删除):
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名,类型可以是boolean或integer
logic-delete-value: 1 # 逻辑已删除值(默认为1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为0)
不过,逻辑删除本身也有自己的问题,例如:
- 回导致数据库垃圾数据越来越多,影响查询效率。
- SQL 中全部需要对逻辑删除字段做判断,影响查询效率。
逻辑删除的另一种平替方案是把垃圾数据定时迁移到其他表中。
枚举处理器
当一个类中包含了枚举类型的时候,可以借助枚举处理器进行 Java 枚举类型到 mysql INT 类型自动转换。
在枚举类型中对应值添加 @EnumValue:
@Getter public enum UserStatus { NORMAL(1, "正常"), FREEZE(2, "冻结"); @EnumValue // 枚举处理,实现类型的自动转换 @JsonValue // 指示到时候返回哪个值 private final int value; private final String desc; UserStatus(int value, String desc) { this.value = value; this.desc = desc; }
在配置文件中配置全局枚举处理器:
mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
JSON 处理器
在 mysql 中,存在 JSON 数据类型,而在 Java 实体类中,我们一般使用内部类来处理类似的 JSON 字段。但是,这就意味着我们需要进行 JSON 字符串到内部类的自动映射。我们可以使用 JSON 处理器来进行对应的操作:
@Data
@TableName(value = "user", autoResultMap = true) // 需要开启自动属性映射
public class User {
private Long id;
private String username;
@TableField(typeHandler = JacksonTypeHandler.class) // 指定JSON处理器
private UserInfo info; // 这一个字段是JSON对应的内部类
}
插件功能
分页插件
MybatisPlus 提供了很多不同的插件供我们使用,其中,最常用的便是分页插件。MyBatisPlus 的分页插件 PaginationInnerInterceptor
提供了强大的分页功能,支持多种数据库,使得分页查询变得简单高效。
要使用这个插件需要先配置一个拦截器(这意味着 MybatisPlus 的分页插件功能是基于拦截器来制作的):
@Configuration
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor); // 如果配置多个插件, 切记分页最后添加
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
return interceptor;
}
}
接下来就可以使用分页插件了:
void testPageQuery() {
// 准备数据
int pageNo = 1, pageSize = 2;
Page<Emp> page = Page.of(pageNo, pageSize);
// 添加排序条件
page.addOrder(OrderItem.desc("emp_date"));
// 分页查询
page = empService.page(page);
// 可以使用lambdaQuery构造过滤条件
// page = empService.lambdaQuery(...).page(page);
// 解析数据
long total = page.getTotal();
List<Emp> emps = page.getRecords();
System.out.println(total);
emps.forEach(System.out::println);
}
实际开发中可以在 PageQueryDTO 中定义方法,将自身对象转换为 MyBatisPlus 中的 Page 对象;在 PageResult 中定义方法,将 MyBaitsPlus 中的 Page 对象转换为 VO 对象:
@Data
public class PageQueryDTO implements Serializable {
// 属性
private Integer pageNo = 1; // 给默认值,防止前端没传数据,导致空指针
private Integer pageSize = 5;
private String sortBy;
private Boolean isAsc = true;
public <T> Page<T> toMpPage(OrderItem ... items) {
// 分页条件
Page<T> page = Page.of(pageNo, pageSize);
// 排序条件
if (sortBy != null && !sortBy.isEmpty()) {
page.addOrder(isAsc == Boolean.TRUE ? OrderItem.asc(sortBy) : OrderItem.desc(sortBy));
} else if (items != null) {
// 默认排序
page.addOrder(items);
}
return page;
}
public <T> Page<T> toMpPageDefaultSortByTime(String time) {
// 根据时间字段排序
return toMpPage(OrderItem.desc(time));
}
}
@Data
public class PageResult<T> implements Serializable {
private Long total;
private List<T> records;
public static <T> PageResult<T> of(Page<T> page) {
PageResult<T> pageResult = new PageResult<>();
pageResult.setTotal(page.getTotal());
List<T> list = page.getRecords();
if (list != null && !list.isEmpty()) {
pageResult.setRecords(list);
} else {
pageResult.setRecords(Collections.emptyList());
}
return pageResult;
}
}
这样一来,Service 层的分页代码只需要两行即可搞定:
/**
* 员工分页查询
* @param empPageQueryDTO
* @return
*/
@Override
public PageResult<Emp> empPageQuery(EmpPageQueryDTO empPageQueryDTO) {
// 得到分页查询Page对象
Page<Emp> dtoPage = empPageQueryDTO.toMpPageDefaultSortByTime("emp_date");
return PageResult.of(page(dtoPage)); // 注意这里需要调用page方法
}