都是MyBatis分页工具PageHelper惹的祸 —— 记一次加入GROUP BY分组查询成功,不分组则失败的奇怪现象Debug过程

项目技术框架

请您打开世界上最好使的Java编译器IntelliJ IDEA,建一个SpringBoot项目,保证项目中包含以下依赖:
后端框架:SpringBoot + JUnit
数据库持久化:MySQL + MyBatis

测试数据

请您打开可爱的小海豚SQLyog,运行以下SQL建表:

/*
SQLyog Ultimate v12.08 (64 bit)
MySQL - 5.5.62 
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;

create table `order_jake` (
	`oid` int (11),
	`customer` varchar (30),
	`cost` int (11)
); 
insert into `order_jake` (`oid`, `customer`, `cost`) values('1','Jake','1000');
insert into `order_jake` (`oid`, `customer`, `cost`) values('2','Jason','1600');
insert into `order_jake` (`oid`, `customer`, `cost`) values('3','Oliver','800');
insert into `order_jake` (`oid`, `customer`, `cost`) values('4','Jimmy','600');
insert into `order_jake` (`oid`, `customer`, `cost`) values('5','Hui','1200');
insert into `order_jake` (`oid`, `customer`, `cost`) values('6','Jake','600');
insert into `order_jake` (`oid`, `customer`, `cost`) values('7','Jason','200');
insert into `order_jake` (`oid`, `customer`, `cost`) values('8','Jake','100');
insert into `order_jake` (`oid`, `customer`, `cost`) values('9','Hui','500');
insert into `order_jake` (`oid`, `customer`, `cost`) values('10','Oliver','200');
insert into `order_jake` (`oid`, `customer`, `cost`) values('11','Jake','1100');
insert into `order_jake` (`oid`, `customer`, `cost`) values('12','Jake','120');
insert into `order_jake` (`oid`, `customer`, `cost`) values('13','Oliver','750');
insert into `order_jake` (`oid`, `customer`, `cost`) values('14','Jason','900');
insert into `order_jake` (`oid`, `customer`, `cost`) values('15','Jimmy','600');
insert into `order_jake` (`oid`, `customer`, `cost`) values('16','Hui','1200');
insert into `order_jake` (`oid`, `customer`, `cost`) values('17','Jason','500');
insert into `order_jake` (`oid`, `customer`, `cost`) values('18','Jake','1000');
insert into `order_jake` (`oid`, `customer`, `cost`) values('19','Jake','720');
insert into `order_jake` (`oid`, `customer`, `cost`) values('20','Jimmy','1100');
insert into `order_jake` (`oid`, `customer`, `cost`) values('21','Oliver','1800');
insert into `order_jake` (`oid`, `customer`, `cost`) values('22','Hui','1500');
insert into `order_jake` (`oid`, `customer`, `cost`) values('23','Jake','80');
insert into `order_jake` (`oid`, `customer`, `cost`) values('24','Hui','210');
insert into `order_jake` (`oid`, `customer`, `cost`) values('25','Jimmy','60');
insert into `order_jake` (`oid`, `customer`, `cost`) values('26','Jason','1400');
insert into `order_jake` (`oid`, `customer`, `cost`) values('27','Jason','450');
insert into `order_jake` (`oid`, `customer`, `cost`) values('28','Oliver','800');
insert into `order_jake` (`oid`, `customer`, `cost`) values('29','Jimmy','200');
insert into `order_jake` (`oid`, `customer`, `cost`) values('30','Jake','50');

在SQLyog中数据视图如下:
在这里插入图片描述

What is the Problem?

SQL和代码准备(可惜SQL是错的)

假设您是一个特例独行的程序员,偶尔想在无聊的编程工作中找找乐子,您可能会脑洞大开地使用having代替where做条件筛选,您可能会写出以下SQL:

-- 查出金额不少于800元的订单
SELECT 
  * 
FROM
  order_jake 
HAVING cost >= 800 ;

查询结果:
在这里插入图片描述

-- 查出姓名以字母J开头的客户的订单
SELECT 
  * 
FROM
  order_jake
HAVING customer LIKE 'J%' ;

查询结果:
在这里插入图片描述
您十分满意,在没有GROUP BY的情况下,HAVING和WHERE的作用一样一样的呢。
于是您基于SpringBoot + MyBatis一溜烟写好POJO、service、mapper(java & xml)层代码,再把JUnit单元测试一写,感觉人生已经达到了巅峰,分分钟完成需求:
Order

@Data
public class Order {

    private Integer oid;

    private String customer;

    private Integer cost;
}

OrderService

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public List<Order> listOrdersByHavingCustomer(String customer) {
        PageHelper.startPage(1, ParamConsts.PAGE_SIZE);
        return orderMapper.listOrdersByHavingCustomer(customer);
    }

    public List<Order> listOrdersByHavingCost(Integer cost) {
        PageHelper.startPage(1, ParamConsts.PAGE_SIZE);
        return orderMapper.listOrdersByHavingCost(cost);
    }
}

OrderMapper.java

public interface OrderMapper {

    List<Order> listOrdersByHavingCustomer(String customer);

    List<Order> listOrdersByHavingCost(Integer cost);
}

OrderMapper.xml

<select id="listOrdersByHavingCustomer" resultType="com.jake.zyt.entity.Order" parameterType="string">
	SELECT
	  *
	FROM
	  order_jake
	HAVING customer LIKE CONCAT('%', #{customer}, '%')
</select>
<select id="listOrdersByHavingCost" resultType="com.jake.zyt.entity.Order">
	SELECT
	  *
	FROM
	  order_jake
	HAVING cost >= 800
</select>

OrderServiceTest

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Test
    public void listOrdersByHavingCustomer() {
        List<Order> orders = orderService.listOrdersByHavingCustomer("J");
        assertThat(orders.size(), Matchers.lessThanOrEqualTo(ParamConsts.PAGE_SIZE));
    }

    @Test
    public void listOrdersByHavingCost() {
        List<Order> orders = orderService.listOrdersByHavingCost(800);
        assertThat(orders.size(), Matchers.lessThanOrEqualTo(ParamConsts.PAGE_SIZE));
    }
}

满是ERROR的控制台

理想很丰满,现实很骨感,运行结果无情报错,惨红的ERROR信息提示您,您这SQL不好使了:
在这里插入图片描述
在这里插入图片描述
把关键错误信息贴一下:

### SQL: SELECT count(0) FROM order_jake HAVING cost >= 800
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'cost' in 'having clause'
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Unknown column 'cost' in 'having clause'
### SQL: SELECT count(0) FROM order_jake HAVING customer LIKE CONCAT('%', ?, '%')
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'customer' in 'having clause'
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Unknown column 'customer' in 'having clause'

您肯定很纳闷,这在SQLyog里运行得飞起的SQL咋到了MyBatis里就不好使了呢?
您看了看报错:Unknown column xxx in ‘having clause’,这是什么鬼?
不明就里的您只好把控制台输出的SQL复制到SQLyog里面运行一下,此时SQLyog也给您来了和MyBatis个一模一样的无情报错:
在这里插入图片描述
您彻底懵圈了,但冥冥之中感觉到似乎有某种规律存在于SELECT和HAVING之中,您怀着试一试的心态,运行了这段SQL:
在这里插入图片描述
虽然这句SQL毫无意义,但是起码您看出了一点规律,HAVING后面的列必须存在SELECT后面,否则会报错:Unknown column xxx in ‘having clause’。
此时您利用这个规律,尝试着写出有意义的SQL:
在这里插入图片描述
这条SQL查出了订单总数,订单平均金额,订单总金额,但您仍然觉得此SQL毫无意义,因为您可以很清楚地看出,HAVING是在SELECT columns FROM table之后才做了筛选。由此,可以总结出只有HAVING关键字的SQL等效于以下的WHERE条件查询SQL:
在这里插入图片描述
咳咳,您的思路飘得有点远了,不过发散性思维让您发现了很多实用的规律。让我们回归之前IntellJ IDEA的控制台里那些飘红的ERROR。您肯定灰常灰常纳闷,why the hell控制台里会蹦出一个:
SELECT COUNT(0) FROM xxx ?

罪魁祸首:PageHelper

您对Mapper层的方法单独做了单元测试,没错啊,那个没有GROUP BY只有HAVING的SQL非常好使,和SQLyog里运行得结果一毛一样。好了,此时该把目光聚集到最大的犯罪嫌疑代码来了:PageHelper.startPage(int startNum, int pageSize)
把这行代码注释掉,再跑一波Service层单元测试,咦,那个没有GROUP BY只有HAVING的SQL又好使了,OK,PageHelper的罪名实锤了。就是它,让MyBatis里的SQL在执行前多执行了一句计数查询,因为要查出总数,才好分页嘛。所以,您终于清楚控制台为啥蹦出诸如:

SELECT count(0) FROM order_jake HAVING cost >= 800

这样SQLyog里面都跑不通的SQL了吧。
这里咱就不对PageHelper的原理多做介绍了,有兴趣的小可爱可以去人家的官网瞅瞅看这玩意儿的原理。传送门:MyBatis PageHelper的GitHub源码仓库
咱们只需要明确一点,只要PageHelper.startPage搁你的查询逻辑前面一放,您的SQL查询运行之前就都要先把查询总数给查出来,再去执行您具体的SQL查询,然后再根据查询结果分页,放到List啊,Set啊等集合里边去,注意了,此时集合的Size最多就只有分页查询的每页数量PageSize个了,顺便提一嘴,对这个关系做断言测试的时候可以这么写:

assertThat(list.size(), Matchers.lessThanOrEqualTo(ParamConsts.PAGE_SIZE));

写正确且意义的SQL

为了新奇,写出只有HAVING没有GROUP BY这样六亲不认的SQL之后,您会想,emm…咱怎样才能写出又有HAVING又靠谱的SQL呢,很简单,把GROUP BY加上呗。
来,作为SQL老司机,您很快写出了几个GROUP BY + HAVING,先分组再筛选,一气呵成,还顺道把查询他们总数的SQL给写出来了:

-- 2. 包含GROUP BY的查询
-- 查出每位顾客消费的金额,分组显示。
SELECT 
  customer customer_name,
  SUM(cost) customer_cost
FROM
  order_jake 
GROUP BY customer ;
-- 查出以上查询结果的总条数
SELECT 
  COUNT(*) query_count 
FROM
  (SELECT 
    customer customer_name,
    SUM(cost) customer_cost 
  FROM
    order_jake 
  GROUP BY customer) order_count ;

-- 查出每位顾客消费的金额,筛选出总消费额度不少于4000元的顾客,分组显示。
SELECT 
  customer customer_name,
  SUM(cost) customer_cost 
FROM
  order_jake 
GROUP BY customer 
HAVING customer_cost >= 4000 ;
-- 查出以上查询结果的总条数
SELECT 
  COUNT(*) query_count
FROM
  (SELECT 
    customer customer_name,
    SUM(cost) customer_cost 
  FROM
    order_jake 
  GROUP BY customer 
  HAVING customer_cost >= 4000) order_count ;

-- 查出每位顾客不少于800元的订单的消费金额,筛选出这些订单的总消费额不少于3500元的顾客,分组显示。
SELECT 
  customer customer_name,
  SUM(cost) customer_cost 
FROM
  order_jake 
WHERE cost >= 800 
GROUP BY customer 
HAVING customer_cost >= 3500 ;
-- 查出以上查询结果的总条数
SELECT 
  COUNT(*) query_count 
FROM
  (SELECT 
    customer customer_name,
    SUM(cost) customer_cost 
  FROM
    order_jake 
  WHERE cost >= 800 
  GROUP BY customer 
  HAVING customer_cost >= 3500) order_count ;

您的小伙伴就纳了闷了,老铁,你查总数干啥总要先查出一个子表,然后在外面再套一层SELECT COUNT(*)呢?俺直接酱紫单刀直入多简单:

SELECT 
  COUNT(*) 
FROM
  order_jake 
GROUP BY customer ; 

我说老铁,动动手,把SQL复制到SQLyog里面一试便知了,这查出来的是啥啊:
在这里插入图片描述
是啥啊哥,俺还真不知道是啥?
好好好,给你看个清楚点的:
在这里插入图片描述
啊,俺知道了,这意思是每个哥们下的订单数是多少?
诶,对了,小机灵鬼~

给正确的SQL做分页

咱给有GROUP BY又有HAVING的SQL们写写service,mapper,MyBatis语句,单元测试呗:
CustomerService

@Service
public class CustomerService {

    @Autowired
    private CustomerMapper customerMapper;

    public List<CustomerVO> groupAndFilterCustomers(Integer cost) {
        PageHelper.startPage(1, ParamConsts.PAGE_SIZE);
        return customerMapper.groupAndFilterCustomers(cost);
    }
}

CustomerMapper.java

public interface CustomerMapper {

    List<CustomerVO> groupAndFilterCustomers(Integer cost);
}

CustomerMapper.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.jake.zyt.mapper.CustomerMapper">
    <resultMap id="customerMap" type="com.jake.zyt.vo.CustomerVO">
        <result property="name" column="customer_name"/>
        <result property="cost" column="customer_cost"/>
    </resultMap>
    <select id="groupAndFilterCustomers" resultMap="customerMap">
        SELECT
          customer customer_name,
          SUM(cost) customer_cost
        FROM
          order_jake
        GROUP BY customer
        HAVING customer_cost >= #{cost}
    </select>
</mapper>

CustomerServiceTest

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerServiceTest {

    @Autowired
    private CustomerService customerService;

    @Test
    public void groupAndFilterCustomers() {
        PageHelper.startPage(1, ParamConsts.PAGE_SIZE);
        List<CustomerVO> customerVOS = customerService.groupAndFilterCustomers(1000);
        assertThat(customerVOS.size(), Matchers.lessThanOrEqualTo(ParamConsts.PAGE_SIZE));
    }
}

测试之前咱先把配置文件application.yml中service和mapper包下的Log记录级别改成Debug,这样才能在控制台中看到打印的SQL:

logging:
  level:
    com.jake.zyt.mapper: debug
    com.jake.zyt.service: debug

好了,单元测试走您:
在这里插入图片描述
绿了绿了,通过了!喜大普奔!
把控制台的打印信息给您贴出来瞧瞧:

2019-07-01 19:54:32.577  INFO 816 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-07-01 19:54:32.832  INFO 816 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-07-01 19:54:32.923 DEBUG 816 --- [           main] c.j.z.m.C.groupAndFilterCustomers_COUNT  : ==>  Preparing: SELECT count(0) FROM (SELECT customer customer_name, SUM(cost) customer_cost FROM order_jake GROUP BY customer HAVING customer_cost >= ?) table_count 
2019-07-01 19:54:32.972 DEBUG 816 --- [           main] c.j.z.m.C.groupAndFilterCustomers_COUNT  : ==> Parameters: 1000(Integer)
2019-07-01 19:54:33.014 DEBUG 816 --- [           main] c.j.z.m.C.groupAndFilterCustomers_COUNT  : <==      Total: 1
2019-07-01 19:54:33.017 DEBUG 816 --- [           main] c.j.z.m.C.groupAndFilterCustomers        : ==>  Preparing: SELECT customer customer_name, SUM(cost) customer_cost FROM order_jake GROUP BY customer HAVING customer_cost >= ? LIMIT ? 
2019-07-01 19:54:33.018 DEBUG 816 --- [           main] c.j.z.m.C.groupAndFilterCustomers        : ==> Parameters: 1000(Integer), 10(Integer)
2019-07-01 19:54:33.022 DEBUG 816 --- [           main] c.j.z.m.C.groupAndFilterCustomers        : <==      Total: 5

其中两条SQL扔到SQLyog里面跑一跑:
在这里插入图片描述
在这里插入图片描述
好使,非常好使~

文末关于WHERE的思考

最后您肯定会想,HAVING的count计数竟如此麻烦,那么WHERE呢?PageHelper.startPage方法对普通的WHERE条件查询时的总数查询语句是怎样的呢?是不是和HAVING一样呢?再上代码-单元测试一条龙服务:
OrderService

public List<Order> listOrdersByCost(Integer cost) {
	PageHelper.startPage(1, ParamConsts.PAGE_SIZE);
	return orderMapper.listOrdersByCost(cost);
}

interface OrderMapper.java

List<Order> listOrdersByCost(Integer cost);

OrderMapper.xml

<select id="listOrdersByCost" resultType="com.jake.zyt.entity.Order" parameterType="integer">
	SELECT
	  *
	FROM
	  order_jake
	WHERE cost >= #{cost}
</select>

OrderServiceTest

@Test
public void listOrdersByCost() {
	List<Order> orders = orderService.listOrdersByCost(800);
	assertThat(orders.size(), Matchers.lessThanOrEqualTo(ParamConsts.PAGE_SIZE));
}

单元测试结果:
在这里插入图片描述
计数SQL如下:

SELECT count(0) FROM order_jake WHERE cost >= 800

对于只有WHERE没有HAVING的语句来说,计数方式就简单得多了,因为此时:

SELECT COUNT(*) FROM TABLE WHERE CONDITION

就是查询

SELECT COLS FROM TABLE WHERE CONDITION

的结果的总条数。
整个探索过程就这么结束了,虽然您很头铁走了很多弯路,但在XJB乱试的过程中收获了知识,岂不美哉?

发布了79 篇原创文章 · 获赞 322 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_15329947/article/details/94394725