Признак для подсчета связанных Laravel Модели обращаются к БД только при необходимости - PullRequest
1 голос
/ 26 мая 2020

Мне интересно, существует ли что-нибудь вроде следующего для Laravel? Я написал эту черту под названием CarefulCount.

Что она делает: возвращает количество связанных моделей (с использованием любого определенного отношения), но обращается к БД только в случае крайней необходимости. Во-первых, он пробует два варианта, чтобы избежать попадания в базу данных, если информация уже доступна:

  1. Был ли получен счетчик с использованием withCount('relation') при извлечении модели - т.е. существует ли $model->relation_count? Если да, просто верните это.
  2. Было ли отношение загружено? Если это так, подсчитайте модели в коллекции Eloquent, не обращаясь к базе данных, с помощью $model->relation->count().
  3. Только затем прибегайте к вызову $model->relation()->count(), чтобы получить счет из базы данных.

Чтобы включить его для любого класса модели, вам просто нужно включить черту с use CarefulCount. Затем вы можете вызвать $model->carefulCount('relation') для любого определенного отношения.

Например, в моем приложении есть таблица suburbs с отношением «много-много» как к таблице users, так и к таблицам churches ( т.е. в одном пригороде может быть много пользователей и много церквей). Просто добавив use CarefulCount к модели Suburb, я могу затем вызвать как $suburb->carefulCount('users'), так и $suburb->carefulCount('churches').

Мой вариант использования: я сталкивался с этим несколько раз - где мне нужно количество связанных моделей, но это в части нижнего уровня моего приложения, которую можно вызывать из нескольких мест. Поэтому я не могу знать, как была получена модель и есть ли уже информация о счетчике.

В таких ситуациях по умолчанию будет вызывать $model->relation()->count(). Но это может привести к проблеме запроса N + 1 .

Фактически, специфический триггер c возник в результате добавления превосходного Laravel N + 1 Query Detector от Марселя Посио. пакет к моему проекту. Он обнаружил ряд проблем с запросами N + 1, которые я не заметил, и в большинстве случаев у меня были уже загруженные связанные модели. Но в моих шаблонах Blade я использую политики, чтобы разрешить или запретить удаление записей; а метод delete($user, $suburb) моего класса SuburbPolicy включал следующее:

return $suburb->users()->count() == 0 && $suburb->churches()->count() == 0;

Это привело к проблеме N + 1 - и, очевидно, я не могу предположить, что в моем классе политики (или самом моем классе модели ), что пользователи и церкви очень загружены. Но с добавлением черты CarefulCount получилось:

return $suburb->carefulCount('users') == 0 && $suburb->carefulCount('churches') == 0;

Voila! Повозившись с этим и проверив журнал запросов, он работает. Например, с количеством пользователей:

  • Если $suburb был получен с использованием Suburb::withCount('users'), дополнительный запрос не выполняется.
  • Аналогично, если он был получен с использованием Suburb::with('users') , дополнительный запрос не выполняется.
  • Если ничего из вышеперечисленного не было выполнено, выполняется запрос select count(*) для получения счетчика.

Как я уже сказал, я ' Я хотел бы знать, существует ли что-то подобное уже, и я не нашел его (ни в ядре, ни в пакете) - или я упустил что-то очевидное.

Вот код моей черты :

use Illuminate\Support\Str;

trait CarefulCount
{
    /**
     * Implements a careful and efficient count algorithm for the given
     * relation, only hitting the DB if necessary.
     *
     * @param string $relation
     *
     * @return integer
     */
    public function carefulCount(string $relation): int
    {
        /*
         * If the count has already been loaded using withCount('relation'),
         * use the 'relation_count' property.
         */
        $prop = Str::snake($relation) . "_count";
        if (isset($this->$prop)) {
            return $this->$prop;
        }

        /*
         * If the related models have already been eager-loaded using
         * with('relation'), count the loaded collection.
         */
        if ($this->relationLoaded($relation)) {
            return $this->$relation->count();
        }

        /*
         * Neither loaded, so hit the database.
         */
        return $this->$relation()->count();
    }
}
...