在Yii2中的SqlDataProvider中使用SQL_CALC_FOUND_ROWS

时间:2015-04-16 07:32:08

标签: php mysql yii yii2

我在Yii2中使用SqlDataProvider,这是一般示例:

$count = Yii::$app->db->createCommand('
    SELECT COUNT(*) FROM user WHERE status=:status
', [':status' => 1])->queryScalar();

$dataProvider = new SqlDataProvider([
    'sql' => 'SELECT * FROM user WHERE status=:status',
    'params' => [':status' => 1],
    'totalCount' => $count,
    'sort' => [
        'attributes' => [
            'age',
            'name' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'default' => SORT_DESC,
                'label' => 'Name',
            ],
        ],
    ],
    'pagination' => [
        'pageSize' => 20,
    ],
]);

COUNT中获得实际数据结果的实际查询之前,您可以在查询中看到SqlDataProvider。{/ p>

但我更喜欢使用SLC_CALC_FOUND_ROWS,因为这个数字是一个更可靠的方法,可以获得与DataProvider中查询返回的实际行数相匹配的正确数字,因为它可能匹配可以在COUNT查询和SqlDataProvider查询之间添加或删除行,因此我需要更可靠的内容。

我可以锁定桌子,但我不认为这是明智的想法,所以我需要使用SQL_CALC_FOUND_ROWS来获得正确的金额,但我不确定我该怎么做它与dataProvider。

这将是我想要的代码:

$sql = $this->db->createCommand("SELECT FOUND_ROWS()");
$count = $sql->queryScalar();

$dataProvider->totalCount = $count;

...但这不起作用,因为我说我不确定如何实现代码以使用SqlDataProvider

4 个答案:

答案 0 :(得分:1)

根据我的理解,SqlDataProvider的功能如下:

  • 如果未设置分页,则数据提供者将查询数据库,然后count()查询结果生成的模型。 这是您想要的行为
  • 如果设置了分页,则会使用$totalCount提供的值,或$totalCount == NULL是否会返回SqlDataProvider::prepareTotalCount()的值,该值设置为返回0不是您想要的行为

我不认为可以在查询中利用分页并在没有两个查询的情况下获得确切的总计数。毕竟,整个分页的重点是不必处理所有返回的元素。

我看到两种可能性。

您要么删除分页并单独处理它。这只是     如果你知道你的回归设定是相对的,那真的很可行     小。在大多数实际情况中,这不是一种选择。

这导致我们必须运行两个查询。如果你对...感到满意     两个查询的想法,你觉得有必要执行它们     尽可能地靠近,这里有你如何继续获得     最好的结果:

  • SqlDataProvider扩展为新类..让我们称之为CustomSqlDataProvider
  • 设置新的public $totalCountCommand属性。
  • 编写prepareTotalCount()方法以覆盖默认行为

有些事情:

protected function prepareTotalCount()
{
    return $this->totalCountCommand->queryScalar();
}

然后您可以简单地创建数据提供者:

$countCommand = Yii::$app->db->createCommand('
    SELECT COUNT(*) FROM user WHERE status=:status
', [':status' => 1]);

$dataProvider = new CustomSqlDataProvider([
    'sql' => 'SELECT * FROM user WHERE status=:status',
    'params' => [':status' => 1],
    'totalCountCommand' => $countCommand,
    'sort' => [
        'attributes' => [
            'age',
            'name' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'default' => SORT_DESC,
                'label' => 'Name',
            ],
        ],
    ],
    'pagination' => [
        'pageSize' => 20,
    ],
]);

这应该做的是,当数据提供者获得结果集时运行您的计数查询,而不是在数据提供者设置(技术上甚至在设定之前)

PS:我还没有测试过这段代码,只是阅读了yii2代码。但是,它应该可以进行微调,也可以设置正确的路径。如果您需要任何额外信息,请告诉我。

答案 1 :(得分:1)

<强>更新: 我修改了我的答案以允许页面验证。

我迟到了一年,但我想分享实际上可能在Yii2的SqlDataProvider内使用SQL_CALC_FOUND_ROWS。

您需要将SqlDataProvider类扩展为:

  • SELECT FOUND_ROWS()获取总计数。
  • 修改pagination对象的validatePage属性的工作方式。

有什么问题?

如果您查看SqlDataProvider类的prepareModels()方法的last lines(Yii 2.0.10)...

if ($pagination !== false) {
    $pagination->totalCount = $this->getTotalCount();
    $limit = $pagination->getLimit();
    $offset = $pagination->getOffset();
}

$sql = $this->db->getQueryBuilder()->buildOrderByAndLimit($sql, $orders, $limit, $offset);

return $this->db->createCommand($sql, $this->params)->queryAll();

...您会看到$this->getTotalCount()运行查询之前被称为。显然,如果您想使用SELECT FOUND_ROWS()作为总计数,则会出现问题。

但是,为什么需要事先打电话呢?毕竟,在那时它还没有开始构建寻呼机。好吧,pagination对象只需验证当前页面索引的总计数。

getOffset()方法调用getPage()进行计算,调用getQueryParam()获取当前请求的页面。完成后,getPage()调用setPage($page, true)。以下是必要时的总计数:setPage()将调用getPageCount()以确保请求的页面位于边界内。

解决方案是什么?

要扩展SqlDataProvider类,请将validatePage对象的pagination属性设置为false,直到我们执行查询为止。然后,我们可以从SELECT FOUND_ROWS()获取总计数并启用自定义页面验证。

我们新的自定义数据提供商可能是这样的:

use Yii;
use yii\data\SqlDataProvider;

class CustomDataProvider extends SqlDataProvider
{
    protected function prepareModels()
    {
        // we set the validatePage property to false temporarily 
        $pagination = $this->getPagination();
        $validatePage = $pagination->validatePage;
        $pagination->validatePage = false;

        // call parent method 
        $dataModels = parent::prepareModels();

        // get total count
        $count = Yii::$app->db->createCommand( 'SELECT FOUND_ROWS()' )->queryScalar();

        // both the data provider and the pagination object need to know the total count 
        $this->setTotalCount( $count );
        $pagination->totalCount = $count;

        // custom page validation       
        $pagination->validatePage = $validatePage;
        // getPage(true) returns a validated page index if $validatePage is also true
        if ( $pagination->getPage(false) != $pagination->getPage(true) ) { 
            return $this->prepareModels();
            // or if you favor performance over precision (and fear recursion) *maybe* is better:
            //$this->sql = str_replace( 'SQL_CALC_FOUND_ROWS ', '', $this->sql);
            //return parent::prepareModels();
        }

        return $dataModels;
    }
}

我们可以这样使用它:

$dataProvider = new CustomDataProvider([
    'sql' => 'SELECT SQL_CALC_FOUND_ROWS ...';
    //'totalCount' => $count, // this is not necessary any longer!
    // more properties like 'pagination', 'sort', 'params', etc.
]);

有任何缺点吗?

好吧,我们新的自定义页面验证效率较低:如果页面没有通过验证,则需要一个额外的查询。

页面验证如何运作?

您拥有100个项目并使用pageSize => 20的数据提供者来显示ListView中的数据。寻呼机将显示链接以浏览5个页面,但在某些情况下,用户可以尝试访问第6页:因为他手动修改了URL,因为自上次加载页面以来记录数量发生了变化(例如@ Brett&# 39; s example)或因为他遵循旧的链接。

数据提供商如何管理这种情况?如果validatePage设置为...

  • false (无论提供商):它会尝试使用类似SELECT ... LIMIT 20 OFFSET 100的SQL查询第6页。它将获得一个空数据集,小部件将输出&#39; 未找到任何结果。&#39;
  • true SqlDataProvider):它会事先检测 最后一个可用页面是数字5,它将使用{{1}查询它}。
  • true SELECT ... LIMIT 20 OFFSET 80):它会尝试查询第6页,获取空数据集,然后实现 第6页没有&#39;存在,再次查询第5页。

IMO,这不是什么大问题,因为进入一个不存在的页面很少会发生。

这实际上是必要的吗?

OP希望这种方法确保尽可能接近地执行计数和实际查询。也许你想要它的性能。

无论如何,您应该阅读@AlexanderRavikovich和@ineersa对问题的评论。这是违反直觉的,但在很多情况下,第二次CustomDataProvider查询可能比使用count(*)更快。

有很多关于它的文章,不要太烦心:它很大程度上取决于你的查询和数据库版本。您可以做的最好的事情是在实现自定义数据提供程序之前测试两种方式。

最后的注释:

如果您真的关心精确度,请考虑this scenario

  • 如果SQL_CALC_FOUND_ROWS失败,通常会失败一些记录。
  • 如果count(*)失败......好吧,这可能是史诗般的失败!

如果你真的关心表现,那么在this other question的答案中有一些不错的建议(带点盐,很老),我特别喜欢this one。 / p>

答案 2 :(得分:0)

像这样创建你的查询:

$queryResults  = Yii::app()->db->createCommand()
    ->select('SQL_CALC_FOUND_ROWS (0), ' .
             'table1.column_1, table1.column_n')
    ->from('table1')
    ->where('status=1')
    ->queryAll();

$totalRecords =  Yii::app()->db
    ->createCommand('SELECT FOUND_ROWS()')
    ->queryScalar();
$totalFetched =  count($queryResults);

echo 'Fetched '.$totalFetched.' of '.$totalRecords.' records.';

答案 3 :(得分:0)

如果您想使用SQL_CALC_FOUND_ROWS而不是COUNT,则需要2个查询才能获得总行数。这里 SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE status = 1 LIMIT 10;将返回由10行限制的查询结果,但由于您使用SQL_CALC_FOUND_ROWS,它将计算符合查询条件的项目总数并记住它。之后,使用SELECT FOUND_ROWS();从dbms获取此编号。

<强> ADDED

我写的代码是为了在我的一个项目上测试它:

Yii::$app->db->createCommand('SELECT SQL_CALC_FOUND_ROWS * FROM {{%articles}} LIMIT 1')->queryScalar();
$count = Yii::$app->db->createCommand('SELECT FOUND_ROWS()')->queryScalar();
$dataProvider = new \yii\data\SqlDataProvider([
    'sql' => 'SELECT * FROM {{%articles}}',
    'totalCount' => $count,
    'pagination' => [
        'pageSize' => 2,
    ],
]);
echo $count . ' ' . count($dataProvider->getModels());

它输出5 2,其中5是项目总数,2是为页面提取的项目数