ORM:被忽略的 group by 后的 count 统计

起因

使用了某个PHP框架的 sql 查询器时,发现每次加上分组条件之后,page 方法返回参数中一个是数据集和一个是总数,而总数总是不准。

问题

于是,我看了一下框架模型的实现方法。

对于,下面这个 sql 查询器,page() 方法会执行两条 sql ,一条是查数据集,一条是查符合条件的数据集总数,对应的返回 [list, total]。

list($list, $total) = $articleModel
->select([
    'XXX',
    'XXX',
])
->groupBy('type')
->page($page, $num);

对于总数的不准,问题出现在 sql 的查询处理结果中,对于上面这个业务的查询,会生成:

# sql1
select count(*) from article group by type;

对比 sql1 和下面的 sql2 ,思考一下有什么不同?

# sql2
select count(*) from article;

sql2 是计算的整张表的记录总数,且返回结果永远只有一条数据;

sql1 比 sql2 多加了一个 type 条件的分组,它计算的是分组后的每个分组下的记录总数,即type1的总数,type2的总数……,返回结果是1-N条。

解决

而框架的bug恰在于此,在处理普通查询分组查询时没有单独处理。

框架 count() 中统一拿取结果集的第一值作为总数,当在普通查询中,这是没问题的;但是在分组查询中,拿到的只是type1分组下的总数。分组查询时,我们要拿总是应该时分组的数量。

// 框架中原代码
public function count($column = '*')
{
    if (!$this->DB) {
        throw  new Exception('您还没有连接数据库', Exception::CODE_DATABASE_ERROR);
    }
    $bak = $this->_sql;
    $sql = $this->buildSql($column);
    $this->sql = $sql;

    if ($this->justSql) {
        return 0;
    }
    $info = $this->DB->query($sql)->fetch(\PDO::FETCH_ASSOC); // 问题在于此,就只拿结果集的第一个记录
    $this->lastQueryAt = time();
    return isset($info['num']) ? $info['num'] : 0;
}

找到了问题,于是我进行了修改,并给框架提了个 pr。

改后代码如下:

// 改造后代码
public function count($column = '*')
{
    if (!$this->DB) {
        throw  new Exception('您还没有连接数据库', Exception::CODE_DATABASE_ERROR);
    }
    $bak = $this->_sql;
    $sql = $this->buildSql($column);
    $this->sql = $sql;

    if ($this->justSql) {
        return 0;
    }

    $total = 0;
    if (count($bak['group']) > 0) {
        $info = $this->DB->query($sql)->fetchAll(\PDO::FETCH_ASSOC); // 有分组时,拿结果集的多个记录
        $total = count($info);
    } else {
        $info = $this->DB->query($sql)->fetch(\PDO::FETCH_ASSOC); // 无分组时,依然如原来一样拿结果集的第一个记录
        $total = isset($info['num']) ? $info['num'] : 0;
    }

    $this->lastQueryAt = time();
    return $total;
}

end!