chiliec / yii2-vote

Provides voting for any model :+1: :-1:

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Много запросов + сортировка

loveorigami opened this issue · comments

Установил новый виджет.
Получил 105 запросов к базе, вместо 40, как было...
Видимо, что то связано с кешем, но то такое...

Мне то нужно все равно делать сортировку записей по рейтингу.
Вот что получилось

В модель Post добавил relation и объявил константу MODEL_ID

    public function getRatings()
    {
        return $this->hasOne(AggregateRating::className(), ['target_id' => 'id'])->onCondition(['model_id' => self::MODEL_ID]);
    }

В модели PostSearch делаю поиск с учетом связи

        $query = $this->find();
        $query->modelClass = get_parent_class($this);
        $query->joinWith('ratings')->published();

Для виджета listVeiw добавил параметр сортировки aggregate_rating

        $dataProvider->sort->attributes['aggregate_rating'] = [
            'asc' => [AggregateRating::tableName() . '.rating' => SORT_ASC],
            'desc' => [AggregateRating::tableName() . '.rating' => SORT_DESC],
            'label' => $this->getAttributeLabel('aggregate_rating'),
        ];

И в самом listView могу теперь получить количество likes, dislikes и rating
Отключил виджет Vote

Количество запросов к базе уменьшилось до 40. А все данные для отрисовки виджета у меня есть

$model->ratings->likes
$model->ratings->dislikes
$model->ratings->rating

Так я что-то не понял, это предложение что-то поменять или уведомление об отказе от виджета голосования в пользу кастомного решения? :D

А что за какой-то новый ratings, когда есть уже готовый rating и зачем какая-то константа MODEL_ID?

Сортировку по рейтингу можно добавить примерно так:

/**
 * Creates data provider instance with search query applied
 *
 * @param array $params
 *
 * @return ActiveDataProvider
 */
public function search($params)
{
    $query = MyModel::find();

    $query->joinWith('rating');

    $dataProvider = new ActiveDataProvider([
        'query' => $query,
        'sort' => [
            'defaultOrder' => [
                'Rating' => SORT_DESC,
            ],
            'attributes' => [
                'Rating' => [
                    'asc' => [
                        'rating' => SORT_ASC,
                    ],
                    'desc' => [
                        'rating' => SORT_DESC,
                    ],
                ],
            ],
        ]
    ]);

    $this->load($params);

    if (!$this->validate()) {
        // uncomment the following line if you do not want to return any records when validation fails
        // $query->where('0=1');
        return $dataProvider;
    }

    return $dataProvider;
}

Видимо, что то связано с кешем, но то такое...

Или действительно не настроен кэш, или ещё это может быть связано с расчетом рейтинга при первом запросе модели. Попробуйте перезагрузить страницу ещё раз. У меня со 108 запросов в первый раз падает до 8 в последующие.

Не, я просто никак не мог снизить количество запросов, вот и делал, как по науке через связи ).
Попробую Ваше решение. Спасибо

А что за какой-то новый ratings, когда есть уже готовый rating и зачем какая-то константа MODEL_ID?

ratings - это связь.

 public function getRatings()
    {
        return $this->hasOne(AggregateRating::className(), ['target_id' => 'id'])->onCondition(['model_id' => self::MODEL_ID]);
    }

Здесь self::MODEL_ID - чтобы выбрать записи из AggregateRating для данной модели через условие.

rating - это свойство модели, которое добавлял в первой версии

ALTER TABLE `YOUR_TARGET_TABLE_NAME` ADD (
  `rating` smallint(6) NOT NULL,
  `aggregate_rating` float(3,2) unsigned NOT NULL
)

может из-за этого у меня не падали запросы...

удалил из таблицы rating.
Я когда писал issue - не видел вашего решения.


ага, увидел в поведении

    /**
     * @inheritdoc
     */
    public function getRating()
    {
        return $this->owner
            ->hasOne(AggregateRating::className(), [
                'target_id' => $this->owner->primaryKey()[0],
            ])
            ->select('rating')
            ->where('model_id = :modelId', [
                ':modelId' => Rating::getModelIdByName($this->owner->className())
            ]);
    }

но.... при его использовании у меня пропадают записи. Как будто происходит inner связь.

Ну да, она и происходит. Это нормально, ведь соответствующая запись к этому моменту уже должна быть в аггрегирующей таблице. Для этого надо использовать либо бутстрап, который вешает такой эвент, либо повесить его вручную https://github.com/Chiliec/yii2-vote/tree/develop/docs#manually-add-behavior-in-models

Такой записи там еще нет, т.к. я подключаю к уже существующей модели с постами (более 1000 штук). Event тоже не хочется цеплять, т.к. если у нас нет записи - она добавляется сама при первом голосовании.

Я немножко по другому сделал ...

Переписал в поведении

    /**
     * @inheritdoc
     */
    public function getRating()
    {
        return $this->owner
            ->hasOne(AggregateRating::className(), [
                'target_id' => $this->owner->primaryKey()[0],
            ])->onCondition(['model_id' => Rating::getModelIdByName($this->owner->className())]);
    }

теперь получаю все записи
а в виджете теперь можно писать так

public function run()
{
    return $this->render('vote', [
        'modelId' => Rating::getModelIdByName($this->model->className()),
        'targetId' => $this->model->{$this->model->primaryKey()[0]},
        'likes' => $this->model->rating->likes,
        'dislikes' => $this->model->rating->dislikes,
        'favs' => $this->model->rating->favs,
        'rating' => $this->model->rating->rating,
        'showAggregateRating' => $this->showAggregateRating,
    ]);
}

и не нужно делать еще раз запросов для каждой записи для получения likes, dislikes....

Мне нравится, пожалуй так и сделаю 👍

Ну тогда и event AfterFind уже не нужен )

Я тут еще такую штуку сделал
В поведение добавил

    /**
     * @inheritdoc
     */
    public function getVoted()
    {
        return $this->owner
            ->hasOne(Rating::className(), [
                'target_id' => $this->owner->primaryKey()[0],
            ])
            ->onCondition([
                'model_id' => Rating::getModelIdByName($this->owner->className()),
                'user_id' => \Yii::$app->user->id,
            ]);
    }

добавил связь в DataProvider
в виджете прописал

public function run()
{
    return $this->render('vote', [
        'modelId' => Rating::getModelIdByName($this->model->className()),
        'targetId' => $this->model->{$this->model->primaryKey()[0]},
        'likes' => $this->model->aggregate->likes ?: 0,
        'dislikes' => $this->model->aggregate->dislikes ?: 0,
        'rating' => $this->model->aggregate->rating ?: 0,
        'showAggregateRating' => $this->showAggregateRating,

        'voted' => $this->model->voted->id,
    ]);
}

и в виде можно теперь, при наличии voted, выводить просто данные без actiona


я все подготавливаю к favorites )
при такой связи при наличии voted можно вывести кнопку - удалить из избранного...

Ну тогда и event AfterFind уже не нужен )

Почему это? А кто будет заполнять то аггрегирующую таблицу?

Я тут еще такую штуку сделал

Голосовать могут и гости. Именно поэтому концепция избранного плохо сочетается в голосованием. Может лучше будет запилить отдельный модуль для этого?

  1. Тот, кто первый раз проголосует
    https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L180
    И кеш, следовательно, не нужен
    https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L163
  2. Я у себя уже сделал ).
    У меня немного другая структура модулей и моделей, поэтому пишу свой модуль на основе Вашего.

В виджет можно добавить параметр useFavorites и выводить, проверяя, залогинен ли юзер или нет.

Отдельным модулем сложно потом будет вывести красиво три кнопки рядом +
два виджета + два набора конфигов, аггрегирующих таблиц и тп...

У меня все укладывается в рамках одного модуля
https://bitbucket.org/loveorigami/lo-module-vote/

2016-02-08_15-57-29

Тот, кто первый раз проголосует

Проблема в том, что до этого момента будет

[yii\base\ErrorException] Trying to get property of non-object

при попытке получить likes, dislikes или rating.

У меня такого нет...
В каком месте?

[yii\base\ErrorException] Trying to get property of non-object

Тут оно берет из Rating
https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L165
получаем 1, т.к. уже вставился голос.

Но - не существует AggregateModel еще.
Поэтому тут устанавливаются свойства likes, dislikes или rating.
https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L181

а тут присваиваем им значения
https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L185

Сохраняем, т.е. вставляем первую запись
https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L188

получаем 1, т.к. уже вставился голос.

Откуда он уже вставился? При первом выводе виджета ещё никто не проголосовал. При создании новой модели его ещё не будет. Я проиллюстрировал это тестом 86f1391#diff-1990d9763f430e1d6f304730af0e1a3fR24

Кстати, провел тесты onCondition vs where на большом объеме данных (5к записей в таблице):

// onCodition
SELECT COUNT(*) FROM `kp_story` LEFT JOIN `kp_aggregate_rating` ON (`kp_story`.`id` = `kp_aggregate_rating`.`target_id`) AND (`model_id`=0)
// 13215.4 ms
// where
SELECT COUNT(*) FROM `kp_story` LEFT JOIN `kp_aggregate_rating` ON `kp_story`.`id` = `kp_aggregate_rating`.`target_id` WHERE model_id = 0
// 13.3 ms

Не знаю, может это особенности MySQL моей конкретной версии, но 13 секунд... это ужас!

"Вставился" через связь aggregate.
У нас там leftJoin же... По крайней мере у меня...

А если нет значения, ставлю 0 в виджете

    public function run()
    {
        return $this->render('vote', [
            'modelId' => Rating::getModelIdByName($this->model->className()),
            'targetId' => $this->model->{$this->model->primaryKey()[0]},
            'likes' => $this->model->aggregate->likes ?: 0,
            'dislikes' => $this->model->aggregate->dislikes ?: 0,
            'favs' => $this->model->aggregate->favs ?: 0,
            'rating' => $this->model->aggregate->rating ?: 0,

        ]);
    }

У вас нет индекса model_id, target_id

У вас нет индекса model_id, target_id

точно, спасибо!

"Вставился" через связь aggregate.

Нет, на самом деле там null, если в таблице нет этой записи. От этой ошибки Вас спасает тернарный оператор, который видимо и берет на себя проверку объекта и не вываливает экспешн. В принципе, можно и так, если работает :)

При создании новой модели его ещё не будет. Я проиллюстрировал это тестом 86f1391#diff-1990d9763f430e1d6f304730af0e1a3fR24

У нас же ж немного по другому.

  1. Вначале вставляется голос в Rating (без Aggregate)
  2. Затем событие afterSave
  3. Идем на updateRating
  4. Достаем только что вставленный голос
  5. Инициализируем новую AggregateRating (разве свойства likes, dislikes или rating не должны создаться автоматически ?)
  6. Ниже им присваивается значение.
  7. Сохраняем Aggregate

Во всяком случае, у меня работает...

AfterFind же тоже так работает
https://github.com/Chiliec/yii2-vote/blob/develop/components/VoteBootstrap.php#L38

Согласен, если нам не обязательно наличие соответствующей записи в аггрегирующей таблице, то можно отказаться от afterFind и от кэширования в методе обновления рейтинга т.к. он будет вызываться только в afterSave модели Rating.

Кеш можно оставить опционально...
Если будут делать выборку без joinWith('aggregate'), то получат много запросов.
Или в инструкции написать, чтоб доставали данные с joinwith.

Кстати, я тут был словил ошибку, когда присоединял избранное. Появился конфликт полей.
Пришлось связи подкорректировать

    /**
     * @inheritdoc
     */
    public function getAggregate()
    {
        return $this->owner
            ->hasOne(AggregateRating::className(), [
                'target_id' => $this->owner->primaryKey()[0],
            ])
            ->onCondition([
                'model_id' => Rating::getModelIdByName($this->owner->className())
            ]);
    }

    /**
     * @inheritdoc
     */
    public function getVoted()
    {
        return $this->owner
            ->hasOne(Rating::className(), [
                'target_id' => $this->owner->primaryKey()[0],
            ])
            ->from(Rating::tableName() . ' r')
            ->onCondition([
                'r.model_id' => Rating::getModelIdByName($this->owner->className()),
                'r.user_id' => \Yii::$app->user->id,
            ]);
    }

    /**
     * @inheritdoc
     */
    public function getFaved()
    {
        return $this->owner
            ->hasOne(Favorites::className(), [
                'target_id' => $this->owner->primaryKey()[0],
            ])
            ->from(Favorites::tableName() . ' f')
            ->onCondition([
                'f.model_id' => Rating::getModelIdByName($this->owner->className()),
                'f.user_id' => \Yii::$app->user->id,
            ]);
    }

Закрываю, вроде всё сделано.