Лучший подход к производительности при фильтрации разрешений в Laravel - PullRequest
9 голосов
/ 18 октября 2019

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

Пользователь может иметь доступ к формам по следующим сценариям:

  • Форма собственности
  • Команда владеет формой
  • Имеет разрешения для группы, владеющей формой
  • Имеет разрешения для команды, владеющей формой
  • Имеет разрешение на форму

Как видите, существует 5 возможных способов доступа пользователя к форме. Моя проблема заключается в том, как наиболее эффективно вернуть пользователю массив доступных форм.

Политика форм:

Я попытался получить все формы из модели, а затем отфильтровать формыпо форме политики. Похоже, что это проблема производительности, так как на каждой итерации фильтра форма передается через eloquent метод 5 (5), как показано ниже. Чем больше форм в базе данных, тем это становится медленнее.

FormController@index

public function index(Request $request)
{
   $forms = Form::all()
      ->filter(function($form) use ($request) {
         return $request->user()->can('view',$form);
   });
}

FormPolicy@view

public function view(User $user, Form $form)
{
   return $user->forms->contains($form) ||
      $user->team->forms->contains($form) ||
      $user->permissible->groups->forms($contains);
}

Несмотря на то, что описанный выше метод работает, это бутылочное горлышко производительности.

Из того, что я вижу, мои следующие варианты::

  • Фильтр FormPolicy (текущий подход)
  • Запрос всех разрешений (5) и объединение в одну коллекцию
  • Запрос всех идентификаторов для всех разрешений (5), затемзапросить модель формы, используя идентификаторы в операторе IN ()

Мой вопрос:

Какой метод обеспечит наилучшую производительность, и есть ли другие вариантычто обеспечит лучшую производительность?

Ответы [ 4 ]

2 голосов
/ 29 октября 2019

Я хотел бы сделать SQL-запрос, поскольку он будет работать намного лучше, чем php

Примерно так:

User::where('id', $request->user()->id)
    ->join('group_users', 'user.id', 'group_users.user_id')
    ->join('team_users', 'user.id', 'team_users.user_id',)
    ->join('form_owners as user_form_owners', function ($join) {
        $join->on('users.id', 'form_owners.owner_id')
            ->where('form_owners.owner_type', User::class);
    })
    ->join('form_owners as group_form_owners', function ($join) {
        $join->on('group_users.group_id', 'form_owners.owner_id')
            ->where('form_owners.owner_type', Group::class);
    })
    ->join('form_owners as team_form_owners', function ($join) {
        $join->on('team_users.team_id', 'form_owners.owner_id')
           ->where('form_owners.owner_type', Team::class);
    })
    ->join('forms', function($join) {
        $join->on('forms.id', 'user_form_owners.form_id')
            ->orOn('forms.id', 'group_form_owners.form_id')
            ->orOn('forms.id', 'team_form_owners.form_id');
    })
    ->selectRaw('forms.*')
    ->get();

Из головы и без проверки это должнополучить все формы, которые принадлежат пользователю, его группам и командам.

Однако он не рассматривает разрешения пользовательских форм просмотра в группах и командах.

Я не уверен, как у вас настроена авторизация для этого, поэтому вам нужно изменить запрос на это и любое различие в структуре вашей БД.

1 голос
/ 29 октября 2019

Короткий ответ

Третий вариант: Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement

$teamMorphType  = Relation::getMorphedModel('team');
$groupMorphType = Relation::getMorphedModel('group');
$formMorphType  = Relation::getMorphedModel('form');

$permissible = [
    $teamMorphType  => [$user->team_id],
    $groupMorphType => [],
    $formMorphType  => [],
];

foreach ($user->permissible as $permissible) {
    switch ($permissible->permissible_type) {
        case $teamMorphType:
        case $groupMorphType:
        case $formMorphType:
            $permissible[$permissible->permissible_type][] = $permissible->permissible_id;
            break;
    }
}

$forms = Form::query()
             ->where('user_id', '=', $user->id)
             ->orWhereIn('id', $permissible[$fromMorphType])
             ->orWhereIn('team_id', $permissible[$teamMorphType])
             ->orWhereIn('group_id', $permissible[$groupMorphType])
             ->get();

Длинный ответ

С одной стороны, (почти) все, что вы можете сделать в коде,лучше с точки зрения производительности, чем в запросах.

С другой стороны, получение большего количества данных из базы данных, чем необходимо, уже слишком много данных (использование ОЗУ и т. д.).

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

Я бы предложил выполнить несколько запросов, последний предложенный вами вариант (Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. Запрос всех идентификаторов для всех разрешений (5 запросов)
  2. Объединение всех результатов форм в памяти и получение уникальных значений array_unique($ids)
  3. ЗапросМодель формы с использованием идентификаторов в операторе IN ().

Вы можете попробовать три предложенных вами варианта и отслеживать производительность, используя какой-либо инструмент для многократного выполнения запроса, но я на 99%уверен, что последний даст вам тЛучшая производительность.

Это также может сильно измениться, в зависимости от того, какую базу данных вы используете, но если мы говорим, например, о MySQL;В очень большом запросе будет использоваться больше ресурсов базы данных, который не только будет тратить больше времени, чем простые запросы, но также будет блокировать таблицу от записи, и это может привести к ошибкам взаимоблокировки (если вы не используете подчиненный сервер).

С другой стороны, если число идентификаторов форм очень велико, вы можете иметь ошибки для слишком большого количества заполнителей, поэтому вы можете разделить запросы на группы, скажем, 500 идентификаторов (это зависит отограничение по размеру, а не по количеству привязок), и объединить результаты в памяти. Даже если вы не получите ошибку базы данных, вы также можете увидеть большую разницу в производительности (я все еще говорю о MySQL).


Реализация

Я предполагаю, чтоэто схема базы данных:

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

Таким образом, допустимым будет уже настроенное полиморфное отношение .

Следовательно, отношения будут:

  • Форма собственности: users.id <-> form.user_id
  • Команда владеет формой: users.team_id <-> form.team_id
  • Имеет разрешения для группы, владеющей формой: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • Имеет разрешения дляКоманда, владеющая формой: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • Имеет разрешение на форму: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

Упрощенная версия:

$teamMorphType  = Relation::getMorphedModel('team');
$groupMorphType = Relation::getMorphedModel('group');
$formMorphType  = Relation::getMorphedModel('form');

$permissible = [
    $teamMorphType  => [$user->team_id],
    $groupMorphType => [],
    $formMorphType  => [],
];

foreach ($user->permissible as $permissible) {
    switch ($permissible->permissible_type) {
        case $teamMorphType:
        case $groupMorphType:
        case $formMorphType:
            $permissible[$permissible->permissible_type][] = $permissible->permissible_id;
            break;
    }
}

$forms = Form::query()
             ->where('user_id', '=', $user->id)
             ->orWhereIn('id', $permissible[$fromMorphType])
             ->orWhereIn('team_id', $permissible[$teamMorphType])
             ->orWhereIn('group_id', $permissible[$groupMorphType])
             ->get();

Подробная версия:

// Owns Form
// users.id <-> forms.user_id
$userId = $user->id;

// Team owns Form
// users.team_id <-> forms.team_id
// Initialise the array with a first value.
// The permissions polymorphic relationship will have other teams ids to look at
$teamIds = [$user->team_id];

// Groups owns Form was not mention, so I assume there is not such a relation in user.
// Just initialise the array without a first value.
$groupIds = [];

// Also initialise forms for permissions:
$formIds = [];

// Has permissions to a group that owns a Form
// permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
$teamMorphType = Relation::getMorphedModel('team');
// Has permissions to a team that owns a Form
// permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
$groupMorphType = Relation::getMorphedModel('group');
// Has permission to a Form
// permissible.user_id <-> users.id && permissible.permissible_type = 'App\Form'
$formMorphType = Relation::getMorphedModel('form');

// Get permissions
$permissibles = $user->permissible()->whereIn(
    'permissible_type',
    [$teamMorphType, $groupMorphType, $formMorphType]
)->get();

// If you don't have more permissible types other than those, then you can just:
// $permissibles = $user->permissible;

// Group the ids per type
foreach ($permissibles as $permissible) {
    switch ($permissible->permissible_type) {
        case $teamMorphType:
            $teamIds[] = $permissible->permissible_id;
            break;
        case $groupMorphType:
            $groupIds[] = $permissible->permissible_id;
            break;
        case $formMorphType:
            $formIds[] = $permissible->permissible_id;
            break;
    }
}

// In case the user and the team ids are repeated:
$teamIds = array_values(array_unique($teamIds));
// We assume that the rest of the values will not be repeated.

$forms = Form::query()
             ->where('user_id', '=', $userId)
             ->orWhereIn('id', $formIds)
             ->orWhereIn('team_id', $teamIds)
             ->orWhereIn('group_id', $groupIds)
             ->get();

Используемые ресурсы:

Производительность базы данных:

  • Запросы к базе данных (исключая пользователя): 2 ;один для получения разрешенного, а другой для получения форм.
  • Нет присоединений !!
  • Минимальное возможное ИЛИ (user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...).

PHP, в памяти, производительность:

  • foreach зацикливание допустимого с переключателем внутри.
  • array_values(array_unique()) доизбегайте повторения идентификаторов.
  • В памяти 3 массива идентификаторов ($teamIds, $groupIds, $formIds)
  • В памяти соответствующие разрешения красноречивого сбора (это можно оптимизировать,при необходимости).

Плюсы и минусы

ПРОФИ:

  • Время : сумма раз одиночногозапросов меньше, чем время большого запроса с объединениями и OR.
  • Ресурсы БД : ресурсы MySQL, используемые запросом с операторами объединения или или, больше, чем используемыесумма отдельных запросов.
  • Деньги : меньше ресурсов базы данных (процессор, ОЗУ, чтение с диска и т. д.), которые стоят дороже ресурсов PHP.
  • Locks : Если вы не запрашиваете подчиненный сервер, доступный только для чтения, ваши запросы сделают меньше строк блокировками на чтение (блокировка на чтение является общей в MySQL, поэтому она не блокирует другое чтение, но блокируетлюбая запись).
  • Масштабируемый : этот подход позволяет оптимизировать производительность, например разбивать запросы на части.

CONS:

  • Ресурсы кода : Выполнение вычислений в коде, а не в базе данных, очевидно, потребляет больше ресурсов в экземпляре кода, но особенно в ОЗУ, хранящем промежуточную информацию. В нашем случае это будет просто массив идентификаторов, что не должно быть проблемой на самом деле.
  • Обслуживание : Если вы используете свойства и методы Laravel, и вы вносите какие-либо изменения вбазу данных будет проще обновлять в коде, чем при более явных запросах и обработке.
  • Избыточность? : в некоторых случаях, если данные не так велики, оптимизацияпроизводительность может быть чрезмерной.

Как измерить производительность

Некоторые подсказки о том, как измерить производительность?

  1. Медленный запросжурналы
  2. ТАБЛИЦА АНАЛИЗА
  3. ПОКАЗАТЬ СОСТОЯНИЕ ТАБЛИЦЫ, КАК
  4. ОБЪЯСНИТЬ ; Расширенный формат EXPLAIN Output ; с использованием объяснения ; объяснение вывода
  5. ПОКАЗАТЬ ПРЕДУПРЕЖДЕНИЯ

Некоторые интересные инструменты профилирования:

0 голосов
/ 30 октября 2019

Я полагаю, что вы можете использовать Lazy Collections для этого (Laravel 6.x) и стремиться загрузить отношения до того, как они будут доступны.

public function index(Request $request)
{
   // Eager Load relationships
   $request->user()->load(['forms', 'team.forms', 'permissible.group']);
   // Use cursor instead of all to return a LazyCollection instance
   $forms = Form::cursor()->filter(function($form) use ($request) {
         return $request->user()->can('view', $form);
   });
}
public function view(User $user, Form $form)
{
   return $user->forms->contains($form) ||
      $user->team->forms->contains($form) ||
      // $user->permissible->groups->forms($contains); // Assuming this line is a typo
      $user->permissible->groups->contains($form);
}
0 голосов
/ 30 октября 2019

Почему вы не можете просто запросить нужные вам формы, вместо того, чтобы делать Form::all(), а затем связывать функцию filter() после нее?

Примерно так:

public function index() {
    $forms = $user->forms->merge($user->team->forms)->merge($user->permissible->groups->forms);
}

Так что даэто выполняет несколько запросов:

  • Запрос для $user
  • Один для $user->team
  • Один для $user->team->forms
  • Один для $user->permissible
  • Один для $user->permissible->groups
  • Один для $user->permissible->groups->forms

Однако, профессиональная сторона в том, что вам больше не нужноиспользовать политику , поскольку вы знаете, что все формы в параметре $forms разрешены для пользователя.

Так что это решение будет работать для любого количества форм, имеющихся в базе данных.

Примечание по использованию merge()

merge() объединяет коллекции и удаляет дубликаты идентификаторов форм, которые он уже нашел. Поэтому, если по какой-то причине форма из отношения team также является прямым отношением к user, она будет отображаться в объединенной коллекции только один раз.

Это потому, что на самом деле это Illuminate\Database\Eloquent\Collectionкоторая имеет собственную функцию merge(), которая проверяет идентификаторы модели Eloquent. Таким образом, вы не можете использовать этот трюк при объединении двух разных коллекционных материалов, таких как Posts и Users, потому что пользователь с идентификатором 3 и пост с идентификатором 3 будут конфликтовать в этом случае, и только последний (Сообщение) будет найдено в объединенной коллекции.


Если вы хотите, чтобы это было еще быстрее, вы должны создать пользовательский запрос, используя фасад БД, что-то вроде:

// Select forms based on a subquery that returns a list of id's.
$forms = Form::whereIn(
    'id',
    DB::select('id')->from('users')->where('users.id', $user->id)
        ->join('teams', 'users.id', '=', 'teams.user_id')
        ...
)->get();

Ваш фактический запрос намного больше, поскольку у вас очень много отношений.

Основное улучшение производительности здесь связано с тем, что тяжелая работа (подзапрос) полностью обходит логику модели Eloquent. Затем все, что осталось сделать, - это передать список идентификаторов в функцию whereIn, чтобы получить список Form объектов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...