前言:mybatis-plus使用场景
mybatis大家都有使用过,既面向对又灵活可配。不友好的地方是,会随着使用出现大量xml文件和SQL语句。
此时,mybatis-plus应运而生,对mybatis做了无侵入增强,还可以简化SQL语句,或者不写SQL语句。
MyBatis-Plus 官网:https://mp.baomidou.com/
MyBatis-Plus 官方文档:https://mp.baomidou.com/guide/
码云项目地址:https://gitee.com/baomidou/mybatis-plus
GitHub地址:https://github.com/baomidou/mybatis-plus
MyBatis-Plus开发组织:https://gitee.com/baomidou
#职位表
CREATE TABLE `position` (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键',
position_no VARCHAR(20) DEFAULT NULL COMMENT '职位编码',
position_name VARCHAR(30) DEFAULT NULL COMMENT '职位名称',
create_time DATETIME DEFAULT NULL COMMENT '创建时间'
) ENGINE=INNODB CHARSET=UTF8;
INSERT INTO `position`(id, position_no, position_name, create_time) VALUES
(1, 'P001', '大boss', NOW()),
(2, 'P002', '经理', NOW()),
(3, 'P003', '艺人', NOW());
#用户表
CREATE TABLE `user` (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键',
`name` VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
position_no VARCHAR(20) NOT NULL COMMENT '职位编码',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
CONSTRAINT manager_fk FOREIGN KEY (manager_id) REFERENCES `user` (id)
) ENGINE=INNODB CHARSET=UTF8;
INSERT INTO `user` (id, `name`, age, email, position_no, manager_id, create_time) VALUES
(1087982257332887553, '张靓颖', 40, '[email protected]', 'P001', NULL, NOW()),
(1088248166370832385, '陶子', 25, '[email protected]', 'P002', 1087982257332887553, NOW()),
(1088250446457389058, '李艺伟', 28, NULL, 'P003',1088248166370832385, NOW()),
(1094590409767661570, '张雨琪', 31, '[email protected]', 'P003', 1088248166370832385, NOW()),
(1094592041087729666, '刘红雨', 32, '[email protected]', 'P003', 1088248166370832385, NOW());
/**
* 实体类
*/
import lombok.Data;
import java.util.Date;
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
private Date createTime;
@TableField(exist = false)
private String positionName;
}
/**
* Mapper 接口
*/
public interface UserMapper extends BaseMapper<User> {
}
/**
* Mapper xml
*/
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.test.UserMapper">
</mapper>
/**
* Service 接口
*/
public interface IUserService extends IService<User> {
}
/**
* Service 实现类
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
常规用法这里就不演示了,下面分析一下工作中使用到的技巧。
mybatis-plus是支持单表的分页查询的,如果分页查询多表数据,需要自己重写一下。
演示场景:分页查询用户列表时,想展示用户信息和用户的职位名称,并支持按职位名称模糊匹配过滤用户数据。重写代码片段如下:
Controller类:
@RestController
@RequestMapping("/user")
@Api(value = "测试用户", tags = "测试用户")
public class UserController {
@Autowired
private IUserService userService;
@ApiOperation(value = "分页查询", notes = "分页查询")
@RequestMapping(value = "/listByPage", method = RequestMethod.POST)
public Result<IPage<User>> listByPage(@RequestBody(required = false) QueryParam<JSONObject> queryParam) {
if (queryParam == null) {
queryParam = new QueryParam();
}
QueryWrapper qw = new QueryWrapper<>();
JSONObject jsonParam = queryParam.getParam();
if (jsonParam != null) {
if (StringUtils.isNotBlank(jsonParam.getStr("name"))) {
qw.likeRight("u.name", jsonParam.getStr("name"));
}
if (StringUtils.isNotBlank(jsonParam.getStr("positionName"))) {
qw.like("p.position_name", jsonParam.getStr("positionName"));
}
}
Page<User> page = userService.page(new Page(queryParam.getCurrent(), queryParam.getSize()), qw);
return Result.ok(page);
}
}
扩展:
一、关闭分页时执行count()查询总数
new Page(queryParam.getCurrent(), queryParam.getSize())
构造函数的两个参数:当前页、每页数量
如果业务需求只需要查询分页做上下页切换而不需要记录总数,可以设置第三个参数false就可以不查询count(),以提高性能。new Page(queryParam.getCurrent(), queryParam.getSize(), false) 。
二、物理分页、逻辑分页的概念
物理分页就是每次查库做分页,逻辑分页是一次查出以后在代码里做分页。
Mapper xml:
上面代码userService.page(),使用的是mybatis-plus的page()方法,所以我们重写BaseMapper的selectPage()方法的SQL语句就可以了。
<mapper namespace="com.test.UserMapper">
<select id="selectPage" resultType="com.test.User">
SELECT u.*,p.position_name FROM `user` u LEFT JOIN `position` p ON p.position_no=u.position_no ${ew.customSqlSegment}
select>
mapper>
这样就实现了演示场景的需求。验证一下,输入入参:
{
"current": 1,
"param": {
"name": "张",
"positionName": "boss"
},
"size": 10
}
响应数据:
{
"success": true,
"message": "返回成功",
"code": 200,
"data": {
"records": [
{
"id": 1087982257332887600,
"name": "张靓颖",
"age": 40,
"email": "[email protected]",
"managerId": null,
"createTime": "2021-03-31T18:15:54",
"positionName": "大boss"
}
],
"total": 0,
"size": 10,
"current": 1,
"orders": [],
"optimizeCountSql": true,
"hitCount": false,
"countId": null,
"maxLimit": null,
"searchCount": true,
"pages": 0
},
"timestamp": 1617188664412
}
SQL语句:
SELECT u.*,p.position_name FROM user u LEFT JOIN position p ON p.position_no=u.position_no
WHERE u.name LIKE '张%' AND p.position_name LIKE '%boos%';
上面我们是通过在xml重写了mybatis-plus的page()方法来实现多表分页查询,如果我们不想破坏mybatis-plus的page()方法,就想自己定义一个分页查询可以吗?当然可以。
Mapper 接口:
public interface UserMapper extends BaseMapper<User> {
List<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> queryWrapper);
}
Mapper xml:
<mapper namespace="com.test.UserMapper">
<select id="selectUserPage" resultType="com.test.User">
SELECT u.*,p.position_name FROM `user` u LEFT JOIN `position` p ON p.position_no=u.position_no ${ew.customSqlSegment}
select>
mapper>
IUserService接口:
public interface IUserService extends IService<User> {
Page<User> listPage(IPage<User> page, QueryWrapper<User> qw);
}
UserServiceImpl实现类:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Page<User> listPage(IPage<User> page, QueryWrapper<User> qw) {
return userMapper.selectUserPage(page, qw);
}
}
上面两个示例中都有使用${ew.customSqlSegment} ,可以用来替换xml里拼接where子句,同理,我们写一些特殊的自定义SQL语句时也可以这么用。
假设场景:比如我们想查询职位是艺人、姓名包含雨、年龄小于等于31岁且邮箱不为空的用户。
Mapper 接口:
public interface UserMapper extends BaseMapper<User> {
List<User> selectListByWrapper(@Param(Constants.WRAPPER) Wrapper<User> queryWrapper);
}
Mapper xml:
<mapper namespace="com.test.UserMapper">
<select id="selectListByWrapper" resultType="com.test.User">
SELECT u.*,p.position_name FROM `user` u LEFT JOIN `position` p ON p.position_no=u.position_no ${ew.customSqlSegment}
select>
mapper>
查询:
// 让 JUnit 运行 Spring 的测试环境, 获得 Spring 环境的上下文的支持
@RunWith(SpringRunner.class)
// 获取启动类,加载配置,确定装载 Spring 程序的装载方法,它回去寻找 主配置启动类(被 @SpringBootApplication 注解的)
@SpringBootTest(classes = DemoApplication.class)
@Slf4j(topic = "MyTest")
public class MyTest {
@Resource
private UserMapper userMapper;
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("p.position_name", "艺人");
qw.like("u.name", "雨").le("u.age", 31);
qw.isNotNull("u.email");
List<User> userList = userMapper.selectListByWrapper(qw);
userList.forEach(System.out::println);
}
}
输出结果:
User(id=1094590409767661570, name=张雨琪, age=31, [email protected], managerId=1088248166370832385, createTime=2021-03-31T18:15:54, positionName=艺人)
这个只是演示示例,实际使用可以灵活调整。
@Test
public void method1() {
// 条件构造器
QueryWrapper<User> qw = new QueryWrapper();
qw.eq("name", "张靓颖");
// 修改后的对象
User user = new User();
user.setAge(18);
user.setEmail("[email protected]");
int i = userMapper.updateByWrapper(user, qw);
System.out.println(i);
}
Mapper 接口:
public interface UserMapper extends BaseMapper<User> {
int updateByWrapper(@Param(Constants.ENTITY) User entity, @Param(Constants.WRAPPER) Wrapper<User> updateWrapper);
}
Mapper xml:
<mapper namespace="com.test.UserMapper">
<update id="updateByWrapper">
UPDATE user SET age=#{et.age},email=#{et.email} ${ew.customSqlSegment}
update>
mapper>
对应的SQL语句:
UPDATE user SET age=18,email='[email protected]' WHERE name = '张靓颖'
Mapper 接口:
public interface UserMapper extends BaseMapper<User> {
List<User> selectListByWrapper(@Param(Constants.WRAPPER) Wrapper<User> queryWrapper);
}
Mapper xml:
<mapper namespace="com.test.UserMapper">
<select id="selectListByWrapper" resultType="com.test.User">
SELECT u.*,p.position_name FROM `user` u LEFT JOIN `position` p ON p.position_no=u.position_no
<where>${ew.sqlSegment}where>
select>
mapper>
DB有些数据 我们希望做逻辑删除,通常会加一个列,比如:deleted是否删除:0否1是。这个逻辑删除字段,mybatis-plus可以帮助我们自动管理,比如:调用mybatis-plus的删除方法时就会改变硬删除为软删除;调用mybatis-plus的查询、修改方法时会自动在where语句后面加上“AND deleted=0”。
那么怎么让mybatis-plus知道deleted就是逻辑删除字段,0、1是删除标识?
logging:
level:
com.test*: debug
mybatis-plus:
mapper-locations: classpath*:mybatis/mapper/**/*.xml
global-config:
db-config:
#过滤查询条件空字符串和NULL
select-strategy: not_empty
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 2
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
/**
* 是否删除: 0 未删除, 1已删除
*/
@TableLogic
private Integer deleted;
@TableLogic注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableLogic {
/**
* 默认逻辑未删除值(该值可无、会自动获取全局配置)、逻辑未删除值
*/
String value() default "";
/**
* 默认逻辑删除值(该值可无、会自动获取全局配置)、逻辑删除值
*/
String delval() default "";
}
可以理解为就近原则。源码见:
@Getter
@ToString
@EqualsAndHashCode
@SuppressWarnings("serial")
public class TableFieldInfo implements Constants {
/**
* 逻辑删除初始化
*
* @param dbConfig 数据库全局配置
* @param field 字段属性对象
*/
private void initLogicDelete(GlobalConfig.DbConfig dbConfig, Field field, boolean existTableLogic) {
/* 获取注解属性,逻辑处理字段 */
TableLogic tableLogic = field.getAnnotation(TableLogic.class);
if (null != tableLogic) {
if (StringUtils.isNotBlank(tableLogic.value())) {
this.logicNotDeleteValue = tableLogic.value();
} else {
this.logicNotDeleteValue = dbConfig.getLogicNotDeleteValue();
}
if (StringUtils.isNotBlank(tableLogic.delval())) {
this.logicDeleteValue = tableLogic.delval();
} else {
this.logicDeleteValue = dbConfig.getLogicDeleteValue();
}
this.logicDelete = true;
} else if (!existTableLogic) {
String deleteField = dbConfig.getLogicDeleteField();
if (StringUtils.isNotBlank(deleteField) && this.property.equals(deleteField)) {
this.logicNotDeleteValue = dbConfig.getLogicNotDeleteValue();
this.logicDeleteValue = dbConfig.getLogicDeleteValue();
this.logicDelete = true;
}
}
}
}
@TableField(exist = false)
private Integer deleted;
如果删除字段加上@TableField(exist = false),会造成执行mybatisPlush的removeById(id),无差别的执行了delete的SQL操作,即物理删除。
DELETE FROM t_user WHERE id=?
如果没有加,那么执行mybatisPlush的removeById(id)时,会转换成update的SQL操作,即逻辑删除。
UPDATE t_user SET deleted=1 WHERE id=? AND deleted=0
使用场景一:标识扩展字段
比如上面的示例,我们查询用户列表时想包含职位名称,但user表没有position_name,如果直接在User对象添加positionName字段,那insert、update操作时就会报错,提示没有该列。怎么办?
在positionName字段上添加@TableField(exist = false),标识这是一个扩展字段。
/**
* 职位名称
*/
@TableField(exist = false)
private String positionName;
使用场景二:查询时排除列
比如上面的示例,我们查询用户数据时不需要查询deleted字段,那么通过@TableField(select = false)标识该字段,mybatis-plus在生成select语句时就不会包含deleted。
/**
* 是否删除:0否1是,默认0
*/
@TableField(select = false)
private Integer deleted;
查询指定的列:
qw.select(“name, email”) 指定只查询name, email。
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("name, email");
qw.and(w -> {
w.likeLeft("email", "@baomidou.com").or().likeLeft("email", "@qq.com");
});
qw.orderByDesc("create_time");
qw.last("LIMIT 1");
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT name,email FROM user
排除指定的列:
方式一:上面的示例中,在字段上添加@TableField(select = false) 标识该列不参与查询;
方式二:使用select(Class entityClass, Predicate predicate) 方法,除了create_time、manager_id 其他列都查询。
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select(User.class, p -> !p.getColumn().equals("create_time") && !p.getColumn().equals("manager_id"));
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT id,name,age,email FROM user
假设场景:查询最新录入的一条邮箱是“@baomidou.com”或者“@qq.com”结尾的艺人的姓名和邮件。
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("name, email");
qw.eq("position_no", "P003");
qw.and(w -> {
w.likeLeft("email", "@baomidou.com").or().likeLeft("email", "@qq.com");
});
qw.orderByDesc("create_time");
qw.last("LIMIT 1");
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
分析:
1.通过qw.select()选择只查询用户姓名和邮件;
2.qw.eq(“position_no”, “P003”);过滤艺人;
3.qw.and(w -> {xxx});实现邮箱“或者”关系的嵌套;
4. w.likeLefe()实现邮件后缀的匹配;
5. qw.orderByDesc()根据创建时间倒序结合LIMIT 1,实现或者最新一条;
对应的SQL语句:
SELECT u.name, u.email FROM `user` u
WHERE u.position_no='P003'
AND (u.email LIKE '%@baomidou.com' OR u.email LIKE '%@qq.com')
ORDER BY u.create_time DESC
LIMIT 1;
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("name, email");
qw.nested(w -> {
w.likeLeft("email", "@baomidou.com").or().likeLeft("email", "@qq.com");
});
qw.eq("position_no", "P003");
qw.orderByDesc("create_time");
qw.last("LIMIT 1");
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT u.name, u.email FROM `user` u
WHERE (u.email LIKE '%@baomidou.com' OR u.email LIKE '%@qq.com')
AND u.position_no='P003'
ORDER BY u.create_time DESC
LIMIT 1;
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("id, name, email");
qw.eq("position_no", "P003");
qw.likeLeft("email", "@baomidou.com").or().likeLeft("email", "@qq.com");
qw.orderByDesc("create_time");
qw.last("LIMIT 1");
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
SQL语句就变成:
SELECT u.name, u.email FROM `user` u
WHERE u.position_no='P003'
AND u.email LIKE '%@baomidou.com' OR u.email LIKE '%@qq.com'
ORDER BY u.create_time DESC
LIMIT 1;
@Test
public void method1() {
List<Long> ids = Arrays.asList(new Long[]{1087982257332887553L, 1088248166370832385L, 1088250446457389058L});
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("position_no", "P003");
qw.and(w -> {
for (Long id : ids) {
w.or().eq("id", id);
}
});
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT id,name,age,email,manager_id,create_time FROM USER
WHERE position_no ='P003'
AND (id = '1087982257332887553' OR id = '1088248166370832385' OR id = '1088250446457389058')
假设场景:查询年龄在20至30岁的艺人。
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.between("age", 20, 30);
qw.inSql("position_no", "SELECT p.position_no FROM `position` p WHERE p.position_name='艺人'");
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT id,name,age,email,manager_id,create_time FROM user
WHERE age BETWEEN 20 AND 40 AND position_no IN (SELECT p.position_no FROM position p WHERE p.position_name='艺人')
@Test
public void method1() {
List<String> positionNos = Arrays.asList("P002", "P003");
QueryWrapper<User> qw = new QueryWrapper<>();
qw.between("age", 20, 30);
qw.in("position_no", positionNos);
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
数据create_time是包含年月日时分秒的,现在通过日期匹配。
@Test
public void method1() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.apply("date_format(create_time,'%Y-%m-%d') = {0}", "2021-03-31");
List<User> userList = userMapper.selectList(qw);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT id,`name`,age,email,manager_id,create_time FROM `user`
WHERE DATE_FORMAT(create_time,'%Y-%m-%d') = '2021-03-31'
@Test
public void method1() {
Map<String, Object> params = new HashMap<>();
params.put("name", "张靓颖");
List<User> userList = userMapper.selectByMap(params);
userList.forEach(System.out::println);
}
对应的SQL语句:
SELECT id,name,age,email,manager_id,create_time FROM user
WHERE name = '张靓颖'
getOne()的默认用法在返回多条记录时会报错,mybatis-plus的service提供了解决方法。
getOne()的第二个参数传false就可以解决,如果有多条记录取第一条返回。
@Test
public void method1() {
// 等同于
// QueryWrapper qw = new QueryWrapper();
// qw.eq("position_no", "P003");
User user = userService.getOne(Wrappers.<User>lambdaQuery().eq(User::getPositionNo, "P003"),false);
System.out.println(user);
}
/**
* lambda查询
*/
@Test
public void lambdaQuery(){
List<User> list = userService.lambdaQuery().eq(User::getAge, 18).list();
list.forEach(System.out::println);
}
/**
* lambda修改
*/
@Test
public void lambdaUpdate(){
boolean update = userService.lambdaUpdate().eq(User::getAge, 18).set(User::getAge, 31).update();
System.out.println(update);
}
/**
* lambda删除
*/
@Test
public void lambdaRemove(){
boolean remove = userService.lambdaUpdate().eq(User::getAge, 18).remove();
System.out.println(remove);
}
参考地址:https://jiannan.blog.csdn.net/article/details/101025174
Mybatis-plus默认的字段策略是:
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
global-config:
db-config:
#字段策略,默认非NULL判断
field-strategy: not_null
此时,调用Mybatis-plus的update()或者updateById()时,如果对象的某个字段为NULL,就不会更新这个字段。
解决方式:
1、修改全局配置,如上面的yaml配置;
2、修改该字段的配置策略,如:
@TableField(updateStrategy = FieldStrategy.IGNORED)
private LocalDateTime operateTime;
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String operateUserId;
这样就能忽略了为NULL的判断,此时operateTime、operateUserId为NULL,调用修改操作后对应数据库的列就是null了。
需求:根据userNo查询userStatus
传统写法:
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("user_status");
qw.eq("user_no", userNo);
User entity = baseMapper.selectOne(qw);
传统链式写法:
User entity = baseMapper.selectOne(new QueryWrapper<User>().select("job_status").eq("user_no", userNo));
使用Wrappers.query()简化后的写法:
User entity = baseMapper.selectOne(Wrappers.<User>query().select("user_status").eq("user_no", userNo));
后面还会慢慢补充