Вопрос про ActiveQuery::viaTable()

Предварительное обсуждение найденных ошибок перед отправкой их авторам фреймворка, а также внесение новых предложений.
Ответить
Аватара пользователя
Stepan Selyuk
Сообщения: 198
Зарегистрирован: 2010.02.03, 05:51
Откуда: Cyprus, Limassol
Контактная информация:

Вопрос про ActiveQuery::viaTable()

Сообщение Stepan Selyuk »

Приветствую!

Возник вопрос. Есть таблицы profiles (1M записей), mailings_links_profiles (1M записей), mailings (несколько записей). В таблице с линками (стержневой) используются просто поля связей между profiles и mailings, как profile_id, mailing_id.

В модели Mailing я использую (как в документации) получение моделей Profile через стержневую таблицу с viaTable(). Фактически любые операции с данным ActiveQuery (count, one, etc) убивают приложение, и даже 512М памяти не хватает. Все потому что viaTable() фактически выгружает стержневую таблицу (зависит от количества подходящих строк) в память, чтобы сделать из нее массив и уже нужные подходящие ключи. В моем случае это допустим 1М записей. Почему не используются стандартные JOIN ... ?

Код: Выделить всё

/**
     * @return \app\components\yiiExt\ActiveQuery
     */
    public function getProfiles()
    {
        return $this->hasMany(Profile::className(), ['id' => 'profile_id'])->viaTable('{{%mailings_news_links_profiles}}', ['mailing_id' => 'id']);
    }
 
Как решение (для MySQL), я сделал прослойку, может кому-то поможет:

Код: Выделить всё

namespace app\components\yiiExt;

use yii\db\Expression;

/**
 * Class ActiveQuery
 * @package app\components\yiiExt
 */
class ActiveQuery extends \yii\db\ActiveQuery
{

    public $found_rows = null;

    public function populate( $rows )
    {

        if ($this->selectOption == 'SQL_CALC_FOUND_ROWS') {

            /* @var $modelClass ActiveRecord */
            $modelClass = $this->modelClass;

            $this->found_rows = $modelClass::getDb()->createCommand( "SELECT FOUND_ROWS()" )->queryScalar();
        }

        return parent::populate( $rows );
    }

    public function one( $db = null )
    {

        // Yii не выставляет лимит при запросе `one`,
        // это выливается в огромное расходование памяти при запросах без условий (или общих условиях)
        // на больших таблицах, так как вся таблица загружается в память, чтобы взять оттуда только первую строку.
        $this->limit( 1 );

        return parent::one( $db );
    }

    public function viaTableUseJoin( $tableName, $link, callable $callable = null )
    {

        /** @var ActiveRecord $targetClass */
        $targetClass = $this->modelClass;

        $pivotTbl = $tableName;
        $targetTbl = $targetClass::tableName();
        $primaryTbl = $this->primaryModel->tableName();

        /*
            SELECT `ss_profiles`.* FROM `ss_profiles`
                LEFT OUTER JOIN `ss_mailings_news_links_profiles` ON `ss_mailings_news_links_profiles`.`profile_id` = `ss_profiles`.`id`
                LEFT OUTER JOIN `ss_mailings_news` ON `ss_mailings_news`.`id` = `ss_mailings_news_links_profiles`.`mailing_id`
                WHERE `ss_mailings_news`.`id` = 3;
         */

        $query = new ActiveQuery( $this->modelClass );

        $query->select( $targetTbl . '.*' );

        // Соединение pivot таблицы с таблицей целевой модели
        $query->leftJoin(
            $pivotTbl,
            [
                $pivotTbl . '.[[' . array_values( $this->link )[ 0 ] . ']]' => new Expression(
                    $targetTbl . '.[[' . array_keys( $this->link )[ 0 ] . ']]'
                )
            ]
        );

        // Соединение pivot таблицы с таблицей модели, откуда идет запрос
        $query->leftJoin(
            $primaryTbl,
            [
                $primaryTbl . '.[[' . array_values( $link )[ 0 ] . ']]' => new Expression(
                    $pivotTbl . '.[[' . array_keys( $link )[ 0 ] . ']]'
                )
            ]
        );

        // Ставим условия по PK модели вызова
        $query->where( [ $primaryTbl . '.[[' . array_values( $link )[ 0 ] . ']]' => $this->primaryModel->{array_values( $link )[ 0 ]} ] );

        // Ставим условия на целевую модель, чтобы строки были
        $query->andWhere( $targetTbl . '.[[' . array_keys( $this->link )[ 0 ] . ']] IS NOT NULL' );

        $query->multiple = true;
        $query->from( $targetTbl );

        if ($callable !== null) {
            call_user_func( $callable, $query );
        }

        return $query;
    }

}
Сначала невидимое, затем видимое. И так у всех программистов :)
Аватара пользователя
Stepan Selyuk
Сообщения: 198
Зарегистрирован: 2010.02.03, 05:51
Откуда: Cyprus, Limassol
Контактная информация:

Re: Вопрос про ActiveQuery::viaTable()

Сообщение Stepan Selyuk »

Что-то подобное и при ActiveRecord::find()->batch() конструкциях. Судя по запросам, которые производятся, тоже всю таблицу загружает в память. А если там 10М записей? Удивлен что $query->limit и $query->offset не используются.
Сначала невидимое, затем видимое. И так у всех программистов :)
Аватара пользователя
Stepan Selyuk
Сообщения: 198
Зарегистрирован: 2010.02.03, 05:51
Откуда: Cyprus, Limassol
Контактная информация:

Re: Вопрос про ActiveQuery::viaTable()

Сообщение Stepan Selyuk »

Есть способ заставить Yii выполнять запросы через LIMIT, OFFSET при использовании Batch:

Код: Выделить всё

        $query = ActiveRecord::find();

        foreach (( $batch = $query->limit( $size=100 )->batch($size) ) as $links) {

            //...

            $query->offset += $query->limit;
            $batch->reset();
        }
Сначала невидимое, затем видимое. И так у всех программистов :)
Ответить