Много запросов + сортировка
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 уже не нужен )
Почему это? А кто будет заполнять то аггрегирующую таблицу?
Я тут еще такую штуку сделал
Голосовать могут и гости. Именно поэтому концепция избранного плохо сочетается в голосованием. Может лучше будет запилить отдельный модуль для этого?
- Тот, кто первый раз проголосует
https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L180
И кеш, следовательно, не нужен
https://github.com/Chiliec/yii2-vote/blob/develop/models/Rating.php#L163 - Я у себя уже сделал ).
У меня немного другая структура модулей и моделей, поэтому пишу свой модуль на основе Вашего.
В виджет можно добавить параметр useFavorites и выводить, проверяя, залогинен ли юзер или нет.
Отдельным модулем сложно потом будет вывести красиво три кнопки рядом +
два виджета + два набора конфигов, аггрегирующих таблиц и тп...
У меня все укладывается в рамках одного модуля
https://bitbucket.org/loveorigami/lo-module-vote/
Тот, кто первый раз проголосует
Проблема в том, что до этого момента будет
[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
У нас же ж немного по другому.
- Вначале вставляется голос в Rating (без Aggregate)
- Затем событие afterSave
- Идем на updateRating
- Достаем только что вставленный голос
- Инициализируем новую AggregateRating (разве свойства likes, dislikes или rating не должны создаться автоматически ?)
- Ниже им присваивается значение.
- Сохраняем 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,
]);
}
Закрываю, вроде всё сделано.