【MP】还在用 QueryWrapper 吗?

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。我们都知道在 MPQueryWrapperLambdaQueryWrapper) 和 UpdateWrapperLambdaUpdateWrapper) 的父类 AbstractWrapper 用于生成 sqlwhere 条件,但是 Wrapper 这么多你只会用 QueryWrapper 可远远不够的啊!各种高级骚操作必须学起来,结合案例代码轻松驾驭各种用法,顺便梳理一些常用的条件构造器及使用的注意事项,闲话少叙,直接进入正题。

前期准备

准备两张表以及对应的实体类对象

  • tb_province 省份表
1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE `tb_province` (
`pid` int(11) NOT NULL AUTO_INCREMENT COMMENT '省份编号',
`province` char(4) DEFAULT NULL COMMENT '省份名',
`abbr` varchar(3) DEFAULT NULL COMMENT '省份的简称',
`area` int(11) DEFAULT NULL COMMENT '省份的面积(km²)',
`population` decimal(10,2) DEFAULT NULL COMMENT '省份的人口(万)',
`attraction` varchar(50) DEFAULT NULL COMMENT '省份的著名景点',
`postcode` varchar(10) DEFAULT NULL COMMENT '省份的省会邮政编码',
PRIMARY KEY (`pid`),
KEY `postcode` (`postcode`),
CONSTRAINT `tb_province_ibfk_1` FOREIGN KEY (`postcode`) REFERENCES `tb_capital` (`postcode`)
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8;
  • 省份表对应的实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码@TableName(value = "tb_province")
@Data
public class Province implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 省份编号
*/
@TableId(value = "pid", type = IdType.AUTO)
private Integer pid;

/**
* 省份名
*/
private String province;

/**
* 省份的简称
*/
private String abbr;

/**
* 省份的面积
*/
private Integer area;

/**
* 省份的人口
*/
private BigDecimal population;

/**
* 省份的著名景点
*/
private String attraction;

/**
* 通过省份的省会邮政编码关联省会信息
*/
@TableField(exist = false)
private Capital capital;
}
  • tb_capital 省会表,通过省份表的邮政编码关联
1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE `tb_capital` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`postcode` varchar(10) NOT NULL COMMENT '省会的邮政编码',
`city` varchar(4) DEFAULT NULL COMMENT '省会名',
`nickname` varchar(10) DEFAULT NULL COMMENT '省会的别名',
`climate` varchar(20) DEFAULT NULL COMMENT '省会的气候条件',
`carcode` varchar(5) DEFAULT NULL COMMENT '省会的车牌号',
PRIMARY KEY (`id`,`postcode`) USING BTREE,
KEY `postcode` (`postcode`),
CONSTRAINT `tb_capital_ibfk_1` FOREIGN KEY (`postcode`) REFERENCES `tb_province` (`postcode`)
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8;
  • 省会表对应的实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码@TableName(value = "tb_capital")
@Data
public class Capital implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 主键id
*/
private Integer id;

/**
* 省会的邮政编码
*/
private String postcode;

/**
* 省会名
*/
private String city;

/**
* 省会的别名
*/
private String nickname;

/**
* 省会的气候条件
*/
private String climate;

/**
* 省会的车牌号
*/
private String carcode;
}

代码演练

OK,一切准备就绪后开始撸代码。

普通 QueryWrapper

先来个简单的,查询省份表中的某一条记录,你可能会用 QueryWrapper 这么写:

1
2
3
4
5
java复制代码// 查询江西省基本信息
QueryWrapper<Province> wrapper = new QueryWrapper<>();
wrapper.eq("province", "江西省");
Province province = provinceMapper.selectOne(wrapper);
System.out.println(province);

执行后,控制台中可以看见如下语句:

1
2
3
4
5
sql复制代码==>  Preparing: SELECT pid,province,abbr,area,population,attraction FROM tb_province WHERE (province = ?) 
==> Parameters: 江西省(String)
<== Columns: pid, province, abbr, area, population, attraction
<== Row: 10, 江西省, 赣, 166900, 4666.10, 庐山、鄱阳湖、滕王阁
<== Total: 1

QueryWrapper 查询条件包装类使用 eqequal) 方法将传入的第一个参数(数据库表中的列名)和第二个参数(条件值)划上等号,然后调用 mapper 接口继承 BaseMapper 下来的 selectOne() 方法,传入 Wrapper 将查询条件加上,于是就得到了上面的 SQL 语句。

最终打印结果为:

1
console复制代码Province [Hash = 629321967, pid=10, province=江西省, abbr=赣, area=166900, population=4666.10, attraction=庐山、鄱阳湖、滕王阁, capital=null]

capital=null !!因为数据库表字段不对应,使用注解排除了非表字段。

那如何在查询省份信息的时候,将其所属的省会信息塞进实体对象中,从而得到一个比较详细的省份详细信息呢?

两种做法:

  1. Province 类中删除被注解标注的 capital 属性,加上外键通过这个字段关联到 Capital 实体,做一个二次查询然后再赋值。
  2. 一个省份对应一个省会,是一对一关系,所以我们可以在对应 xml 文件中写 resultMap 结果集映射,将外键值通过映射查询返回的结果映射到 capital 属性上。

这里使用第二种做法:

ProvinceMapper.xml

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<resultMap id="BaseResultMap" type="com.xx.xxx.entity.Province">
<id column="pid" jdbcType="INTEGER" property="pid" />
<result column="province" jdbcType="CHAR" property="province" />
<result column="abbr" jdbcType="VARCHAR" property="abbr" />
<result column="area" jdbcType="INTEGER" property="area" />
<result column="population" jdbcType="DECIMAL" property="population" />
<result column="attraction" jdbcType="VARCHAR" property="attraction" />
<association property="capital" javaType="com.xx.xxx.entity.Capital" column="postcode"
select="com.xx.xxx.mapper.CapitalMapper.selectAllByPostcode">
</association>
</resultMap>

CapitalMapper.xml 外键值映射查询 SQL

1
2
3
4
5
6
xml复制代码<select id="selectAllByPostcode" parameterType="map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from tb_capital
where postcode = #{postcode,jdbcType=VARCHAR}
</select>

只要 resultMap="BaseResultMap" ,那么你查询的省份信息就包含省会相关信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码Province province = provinceMapper.selectByProvinceName("浙江省");
System.out.println(province); // Province [Hash = 120157876, pid=7, province=浙江省, abbr=浙, area=105500, population=5850.00, attraction=西湖、乌镇、千岛湖, capital=Capital [Hash = 1919497442, id=14, postcode=310000, city=杭州, nickname=临安, climate=亚热带季风气候, carcode=浙A]]

/* 执行后的 SQL 语句
==> Preparing: select pid, province, abbr, area, population, attraction, postcode from tb_province where province=?
==> Parameters: 浙江省(String)
<== Columns: pid, province, abbr, area, population, attraction, postcode
<== Row: 7, 浙江省, 浙, 105500, 5850.00, 西湖、乌镇、千岛湖, 310000
====> Preparing: select id, postcode, city, nickname, climate, carcode from tb_capital where postcode = ?
====> Parameters: 310000(String)
<==== Columns: id, postcode, city, nickname, climate, carcode
<==== Row: 14, 310000, 杭州, 临安, 亚热带季风气候, 浙A
<==== Total: 1
<== Total: 1
*/

Wrapper 支持链式编程,如果查询条件:省份名为浙江省、简称为浙且邮政编码为 310000,你可能会这么写:

1
2
3
4
5
6
java复制代码QueryWrapper<Province> eq = new QueryWrapper<Province>()
.eq("province", "浙江省")
.eq("abbr", "浙")
.eq("postcode", "310000");
Province province = provinceMapper.selectOne(eq);
System.out.println(province);

其实可以不用接连写三个 eq() ,直接写一个 allEq() 即可:

1
2
java复制代码QueryWrapper<Province> eq = new QueryWrapper<Province>()
.allEq({"province": "浙江省", "abbr": "浙", "postcode": "310000"}, true);

第一个参数接收一个 Mapkey 为数据库字段名, value为字段值;

第二个参数接收一个布尔值,可以不传,默认为 true,为 true 则在 mapvaluenull 时调用 isNull 方法,为 false 时则不将 valuenull 的字段作为查询条件。

链式 Lambda 操作

通过上面代码实例可以明显发觉,每次都要自己写 column_name ,一旦写错立马报错,而且写固定列名损害了代码的健壮性,比较死板。

所以使用函数式接口,实现链式查询就非常有必要了!

查询省份名带有 ”“ 、人口超过 2000 万或省份面积在 10w~25w 平方千米的省份信息,按照人口数量降序显示。

1
2
3
4
5
6
7
8
9
java复制代码// 在 QueryWrapper 中是获取的是 LambdaQueryWrapper
LambdaQueryWrapper<Province> eq = new LambdaQueryWrapper<Province>()
.like(Province::getProvince, "江")
.gt(Province::getPopulation, 2000)
.or()
.between(Province::getArea, 100000, 250000)
.orderByDesc(Province::getPopulation);
List<Province> provinces = provinceMapper.selectList(eq);
Optional.ofNullable(provinces).ifPresent(p -> p.forEach(System.out::println));

注意:不调用 or() 则默认为使用 and 连接。

执行后的 SQL 语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码==>  Preparing: SELECT pid,province,abbr,area,population,attraction FROM tb_province WHERE (province LIKE ? AND population > ? OR area BETWEEN ? AND ?) ORDER BY population DESC 
==> Parameters: %江%(String), 2000(Integer), 100000(Integer), 250000(Integer)
<== Columns: pid, province, abbr, area, population, attraction
<== Row: 12, 广东省, 粤, 179725, 11521.00, 丹霞山、华侨城、白云山
<== Row: 19, 山东省, 鲁, 157900, 10070.21, 泰山、沂蒙山、蓬莱阁
<== Row: 11, 河南省, 豫, 167000, 9640.00, 少林寺、龙门石窟、殷墟
<== Row: 6, 江苏省, 苏, 107200, 8070.00, 中山陵、花果山、三台山
<== Row: 1, 河北省, 冀, 188800, 7591.97, 白洋淀、避暑山庄、北戴河
<== Row: 17, 湖南省, 湘, 211800, 6918.38, 张家界、岳阳楼、衡山
<== Row: 8, 安徽省, 皖, 140100, 6365.90, 黄山、九华山、天堂寨
<== Row: 18, 湖北省, 鄂, 185900, 5927.00, 黄鹤楼、神农架、长江三峡
<== Row: 7, 浙江省, 浙, 105500, 5850.00, 西湖、乌镇、千岛湖
<== Row: 13, 广西, 桂, 237600, 4960.00, 桂林山水、银滩、青秀山
<== Row: 10, 江西省, 赣, 166900, 4666.10, 庐山、鄱阳湖、滕王阁
<== Row: 3, 辽宁省, 辽, 148600, 4351.70, 沈阳东陵、大连星海广场
<== Row: 9, 福建省, 闽, 124000, 3973.00, 鼓浪屿、武夷山、涠洲岛
<== Row: 20, 陕西省, 陕, 205600, 3876.21, 兵马俑、华山、黄帝陵
<== Row: 5, 黑龙江省, 黑, 473000, 3751.30, 北极村、扎龙湿地、五大连池
<== Row: 2, 山西省, 晋, 156700, 3729.22, 五台山、平遥古城、云冈石窟
<== Row: 14, 贵州省, 黔或贵, 176167, 3622.95, 黄果树瀑布、梵净山、万峰林
<== Row: 4, 吉林省, 吉, 187400, 2690.73, 长白山、净月潭、高句丽王陵
<== Total: 18

按照气候进行分组,并筛选出别名在三个汉字及以上的直辖市。

  • 第一个入参 boolean condition 表示该条件 是否 加入生成的 SQL 中,默认为 true 。例如:query.like(StringUtils.isNotBlank(name), Entity::getName, name) .eq(age!=null && age >= 0, Entity::getAge, age)
1
2
3
4
5
6
7
8
9
java复制代码List<Capital> capitals = new LambdaQueryChainWrapper<Capital>(capitalMapper)
.isNull(Capital::getCity)
.groupBy(Capital::getClimate)
.having(true, "length(nickname) >= 9") // 等价于 .having(true, "length(nickname) < {0}", 9) {0} 占位
.list();

Optional.ofNullable(capitals).ifPresent(list -> {
list.forEach(System.out::println);
});

LambdaQueryChainWrapper 链式查询 Lambda 式,使用 CapitalMapper 接口(继承 BaseMapper)初始化,链式的末尾使用 list() 方法调用 this.getBaseMapper().selectList(this.getWrapper()); 返回一个 List 结果集。

同时,最后也可通过

  • one() 返回 T 实体类对象
  • oneOpt() 返回 Optional<T> Optional 包装实体类
  • count() 返回 Integer 统计结果集总条数
  • page() 返回 Page 分页对象。

除了使用 new LambdaQueryWrapper<T>() 的方式得到一个 LambdaQueryWrapper,还可以使用 Wrappers 类调用 query() 得到一个 QueryWrapper 对象,调用 lambdaQuery() 静态方法得到一个 LambdaQueryWrapper等等

查询简称有两个及以上和不包括含有“海”的著名景点的省份下的省会信息,如果 randomBool 随机布尔值为 true 那么就再去筛选省会别名不为“林城”,否则判断一条查询 SQL 语句是否存在结果,最终的查询的记录按照主键 id 升序且只选取前两个记录。

这个案例需要使用到了两张表,可用子查询 inSql() ,第一个参数为表列名,另一个参数为 sqlString 即需传入 SQL 语句,拼接后的效果为:列 in (查询结果集)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 生成一个随机布尔值
boolean randomBool = Math.random() > 0.5d;

LambdaQueryWrapper<Capital> lambdaQueryWrapper = Wrappers.<Capital>lambdaQuery().
inSql(Capital::getPostcode, "select postcode \n" +
"from tb_province \n" +
"where length(abbr) != 3 and attraction not like '%海%'")
.func(true, wrapper -> { // condition:true,可忽略
if (randomBool) {
wrapper.ne(Capital::getNickname, "林城");
} else {
wrapper.exists("select * from tb_capital where climate = '亚热带季风性气候'");
}
})
.orderByAsc(Capital::getId)
.last("limit 2");

List<Capital> capitals = capitalMapper.selectList(lambdaQueryWrapper);

Optional.ofNullable(capitals).ifPresent(list -> {
list.forEach(System.out::println);
});

func() 方法主要作用就是要方便在出现 if...else 下调用不同方法能不断链,两种情况的不同会得到不同的 SQL 语句。

运行后,控制台打印的 SQL 语句:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码randomBool: true
==> Preparing: SELECT id,postcode,city,nickname,climate,carcode FROM tb_capital WHERE (postcode IN (select postcode from tb_province where length(abbr) != 3 and attraction not like '%海%') AND nickname <> ?) ORDER BY id ASC limit 2
==> Parameters: 林城(String)
<== Columns: id, postcode, city, nickname, climate, carcode
<== Row: 25, 610000, 成都, 天府之国, 亚热带季风性湿润气候, 川A
<== Row: 28, 730000, 兰州, 金城, 温带大陆性气候, 甘A
<== Total: 2

randomBool: false
==> Preparing: SELECT id,postcode,city,nickname,climate,carcode FROM tb_capital WHERE (postcode IN (select postcode from tb_province where length(abbr) != 3 and attraction not like '%海%') AND EXISTS (select * from tb_capital where climate = '亚热带季风性气候')) ORDER BY id ASC limit 2
==> Parameters:
<== Total: 0
  • last() 无视优化规则直接拼接到 SQL 的最后,但是要注意只能调用一次,如果多次调用以最后一次为准,且有 sql 注入的风险,需谨慎使用!!

CustomSqlSegment 入参

经常会遇到这样一个需求:写一个搜索接口,传入的参数是一个 VO 对象,里面包含的都是一些搜索字段,返回的搜索结果放到 List<VO> / page<VO> 中,这时如果使用 mapper.selectPage() 返回并不是一个包装 VOPage 对象,所以这时你就需要自定义 mapper 层方法,编写 xml 文件了。

ProvinceVO 对象:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Data
public class ProvinceVO {

private Integer pid;

private String province;a

private String abbr;

......
}

Mapper 层接口方法:

1
2
3
java复制代码List<ProvinceVO> queryPageList(Page page, @Param("provinceVO") ProvinceVO provinceVO);

Page<ProvinceVO> pageList(Page<ProvinceVO> page, @Param("provinceVO") ProvinceVO provinceVO);

mapper 接口 queryFruitList() 方法返回的是分页后的 List 集合,但是也可以是 Page 的包装对象,拿到里面的列表数据只需要调用 getRecords() 静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码Page<ProvinceVO> page = new Page<ProvinceVO>(1, 10);
ProvinceVO provinceVO = new ProvinceVO();
provinceVO.setProvince("江");
Page<ProvinceVO> provinceVOPage = provinceMapper.pageList(page, provinceVO);

System.out.println(provinceVOPage.getTotal()); // 查询到的总记录条数
System.out.println(provinceVOPage.getCurrent()); // 当前页
System.out.println(provinceVOPage.getRecords()); // 分页列表数据

// 与 provinceVOPage.getRecords() 等价
List<ProvinceVO> provinceVOPageList = fruitMapper.queryPageList(page, provinceVO);
Optional.ofNullable(provinceVOPageList).ifPresent(list -> {
list.forEach(System.out::println);
});

如果你想使用 Wrapper 自定义 SQL 生成 where 条件,你可以使用注解的方式来书写:

1
2
java复制代码@Select("select * from tb_province ${ew.customSqlSegment}")
List<ProvinceVO> getVOListByCustomSqlSegment(@Param("ew") Wrapper ew);

测试代码:

1
2
3
4
5
6
java复制代码List<ProvinceVO> provinceVOList = provinceMapper.getVOListByCustomSqlSegment(new QueryWrapper<ProvinceVO>()
.like("province", "江"));

Optional.ofNullable(provinceVOList).ifPresent(list -> {
list.forEach(System.out::println);
});

注意:不支持 Wrapper 内的 entity 生成 where 语句!!也就是说不能使用函数式接口代替列名。

1
2
3
4
5
6
java复制代码List<ProvinceVO> provinceVOList = provinceMapper.getVOListByCustomSqlSegment(new LambdaQueryWrapper<ProvinceVO>()
.like(ProvinceVO::getProvince, "江"));*/

// 或者
List<ProvinceVO> provinceVOList = provinceMapper.getVOListByCustomSqlSegment(Wrappers.<ProvinceVO>lambdaQuery()
.like(ProvinceVO::getProvince, "江"));

否则,控制台会报如下错误信息:

1
2
3
console复制代码org.apache.ibatis.builder.BuilderException:
Error evaluating expression 'ew.customSqlSegment'.
... MybatisPlusException: can not find lambda cache for this entity.

除了,注解的方式,还可以在 XML 文件中书写:

  • Constants.WRAPPER 使用了 MP 中字符串常量池,其值为 ew ,等价于 @Param("ew")
1
java复制代码Page<ProvinceVO> getPageListByCustomSqlSegment(Page<ProvinceVO> page, @Param(Constants.WRAPPER) Wrapper wrapper);

XML 中书写 SQL 语句:

1
2
3
4
5
sql复制代码<select id="getPageListByCustomSqlSegment" resultType="com.xx.xxx.vo.ProvinceVO">
select *
from tb_province
${ew.customSqlSegment}
</select>

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码Page<ProvinceVO> page = new Page<>(1, 10);

Page<ProvinceVO> provinceVOPage = provinceMapper.getPageListByCustomSqlSegment(page, new QueryWrapper<Province>()
.like("province", "江"));

Optional.ofNullable(provinceVOPage).ifPresent(p -> {
List<ProvinceVO> provinceVOList = p.getRecords();
long total = p.getTotal();
long currentPage = p.getCurrent();
long pageSize = p.getSize();

provinceVOList.forEach(System.out::println);

System.out.println("总条数:" + total);
System.out.println("当前页:" + currentPage);
System.out.println("每页条数" + pageSize);
});

执行后,控制台可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console复制代码JsqlParserCountOptimize sql=select *
from tb_province
WHERE (province LIKE ?)
==> Preparing: SELECT COUNT(1) FROM tb_province WHERE (province LIKE ?)
==> Parameters: %江%(String)
<== Columns: COUNT(1)
<== Row: 4
==> Preparing: select * from tb_province WHERE (province LIKE ?) LIMIT ?,?
==> Parameters: %江%(String), 0(Long), 10(Long)
<== Columns: pid, province, abbr, area, population, attraction, postcode
<== Row: 5, 黑龙江省, 黑, 473000, 3751.30, 北极村、扎龙湿地、五大连池, 150000
<== Row: 6, 江苏省, 苏, 107200, 8070.00, 中山陵、花果山、三台山, 210000
<== Row: 7, 浙江省, 浙, 105500, 5850.00, 西湖、乌镇、千岛湖, 310000
<== Row: 10, 江西省, 赣, 166900, 4666.10, 庐山、鄱阳湖、滕王阁, 330000
<== Total: 4

ProvinceVO{pid=5, province='黑龙江省', abbr='黑'}
ProvinceVO{pid=6, province='江苏省', abbr='苏'}
ProvinceVO{pid=7, province='浙江省', abbr='浙'}
ProvinceVO{pid=10, province='江西省', abbr='赣'}
总条数:4
当前页:1
每页条数10

注意:${ew.customSqlSegment} 前面千万不要加 where 关键字,QueryWrapper 会附带。

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

参考

条件构造器 | MyBatis-Plus (baomidou.com)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%