Найкращий підхід до продуктивності під час фільтрації дозволів у Laravel


9

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

Користувач може мати доступ до форм за наступними сценаріями:

  • Володіє формою
  • Команда володіє Формою
  • Має дозволи для групи, яка володіє Формою
  • Має дозволи для команди, яка володіє Формою
  • Має дозвіл на Форму

Як бачимо, існує 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 () заяву

Моє запитання:

Який метод забезпечив би найкращі показники та чи є інший варіант, який би забезпечив кращі показники?


ви також можете зробити підхід « Багато до багатьох» для посилання, якщо користувач може отримати доступ до форми
код грошей

Як щодо створення таблиці спеціально для запитів дозволів користувальницької форми? user_form_permissionТаблиця , що містить тільки user_idі form_id. Це зробить дозвіл на читання легким вітром, однак оновити дозволи буде складніше.
PtrTon

Проблема з таблицею user_form_permissions полягає в тому, що ми хочемо розширити дозволи для інших об'єктів, які потім потребують окремої таблиці для кожної сутності.
Тім

1
@Tim, але це ще 5 запитів. Якщо це просто в зоні захищеного члена, це може не бути проблемою. Але якщо це за загальнодоступною URL-адресою, яка може отримати багато запитів за секунду, я повторюю, що ви хочете трохи оптимізувати це. З міркувань продуктивності я б підтримував окрему таблицю (яку я можу кешувати) кожного разу, коли форму чи члена команди додають або видаляють за допомогою модельних спостерігачів. Тоді, на кожен запит, я отримав би це з кешу. Я вважаю це питання і проблемою дуже цікавим, і я хотів би знати, що думають і інші. Це питання заслуговує більшої кількості голосів та відповідей, розпочав
Раул

1
Ви могли б розглянути питання про матеріалізоване уявлення , яке ви могли б оновити в якості запланованого завдання. Таким чином, ви завжди зможете швидко отримати відносно сучасні результати.
апокрифос

Відповіді:


2

Я хотів би зробити 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();

Зверху в голові і неперевірено це повинно отримати вам усі форми, які належать користувачеві, його групам і цим командам.

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

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


дякую за відповідь. Однак проблема була не в запиті, як отримати дані з бази даних. Проблема полягає в тому, як отримати це ефективно кожен раз, на кожне запит, коли у додатку є сотні тисяч форм та безліч команд та членів. У ваших приєднаннях є ORпункти, в яких я підозрюю, що це буде повільно. Тому враження цього на кожен запит буде божевільним, я вважаю.
Раул

Можливо, ви зможете досягти кращої швидкості за допомогою необробленого запиту MySQL або використовуючи щось таке, як перегляди чи процедури, але вам потрібно буде робити дзвінки на зразок цього щоразу, коли вам потрібні дані. Тут також може допомогти кешування результатів.
Джош

Хоча я думаю, що єдиним способом зробити цього виконавця є кешування, це коштує завжди підтримувати цю карту щоразу, коли вносяться зміни. Уявіть, що я створюю нову форму, яка, якщо команда призначена до мого облікового запису, означає, що тисячі користувачів можуть отримати доступ до неї. Що далі? Повторно кешуйте політику на кілька тисяч членів?
Раул

Існують рішення кешу з терміном експлуатації (наприклад, абстракції кеш-пам'яті laravel), а також ви можете видалити кешовані індекси, порушені відразу після будь-яких змін. Кеш - це справжній змінник ігор, якщо ви правильно його використовуєте. Як налаштувати кеш, залежить від читання та оновлення даних.
Гонсало

2

Коротка відповідь

Третій варіант: 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. Запитайте модель Form, використовуючи ідентифікатори в операторі 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, в пам'яті, продуктивність:

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

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

ПРО:

  • Час : сума разів одиничних запитів менша за час великого запиту з об'єднаннями та АБО.
  • Ресурси БД : Ресурси MySQL, які використовуються в запиті з приєднанням або операторами, більше, ніж використані сумою окремих запитів.
  • Гроші : менше ресурсів бази даних (процесор, оперативна пам’ять, читання дисків тощо), які дорожчі, ніж ресурси PHP.
  • Блокування : Якщо ви не запитуєте підлеглий сервер, який працює лише для читання, ваші запити зроблять менше рядків блоку читання блоків (блокування читання поділяється в MySQL, тому він не блокує іншого читання, але блокує будь-яке записування).
  • Масштабованість : Цей підхід дозволяє зробити більше оптимізацій продуктивності, таких як фрагменти запитів.

Мінуси:

  • Кодові ресурси : Здійснюючи обчислення в коді, а не в базі даних, очевидно, буде витрачено більше ресурсів в екземплярі коду, але особливо в оперативній пам'яті, зберігаючи середню інформацію. У нашому випадку це був би лише масив ідентифікаторів, що насправді не повинно бути проблемою.
  • Технічне обслуговування : Якщо ви використовуєте властивості та методи Laravel і вносите будь-які зміни в базу даних, оновлення коду буде простіше, ніж якщо ви робите більш чіткі запити та обробку.
  • Завищення? : У деяких випадках, якщо дані не такі великі, оптимізація продуктивності може бути надмірною.

Як виміряти продуктивність

Деякі підказки про те, як виміряти продуктивність?

  1. Повільні журнали запитів
  2. ТАБЛИКА АНАЛІЗУ
  3. ПОКАЗУЙТЕ ТАБЛИЧНИЙ СТАТУС, ЩО СПІЛЬНО
  4. ПОЯСНІТЬ ; Розширений EXPLAIN вихідний формат ; використовуючи пояснення ; пояснити вихід
  5. ПОКАЗУЙТЕ ПОПЕРЕДЖЕННЯ

Деякі цікаві інструменти профілювання:


Що це за перший рядок? Практично завжди кращою ефективністю є використання запиту, тому що запускати різні цикли або маніпулювати масивом у PHP повільніше.
Полум’я

Якщо у вас є невелика база даних або ваша машина бази даних значно потужніша, ніж ваш примірник коду, або затримка бази даних дуже погана, то так, MySQL швидше, але це зазвичай не так.
Гонсало

Оптимізуючи запит до бази даних, потрібно враховувати час виконання, кількість повернутих рядків і, головне, кількість досліджуваних рядків. Якщо Тім каже, що запити стають повільними, то я припускаю, що дані зростають, а отже, кількість вивчених рядків. Крім того, база даних не оптимізована для обробки, оскільки мова програмування є.
Гонсало

Але мені не потрібно мені довіряти, ви можете запустити EXPLAIN для свого рішення, тоді ви можете запустити його для мого вирішення простих запитів, і побачити різницю, а потім подумати, чи не буде простим array_merge()і array_unique()купою ідентифікаторів дійсно сповільнити ваш процес.
Гонсало

У 9 з 10 випадків база даних mysql працює на тій же машині, що працює з кодом. Рівень даних призначений для використання для пошуку даних і оптимізований для вибору фрагментів даних з великих наборів. Я ще не бачив ситуації, коли a array_unique()швидше, ніж GROUP BY/ SELECT DISTINCTтвердження.
Полум’я

0

Чому не можна просто запитувати потрібні вам форми, замість того, щоб робити, 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()функцію, яка перевіряє красномовні ідентифікатори моделі. Таким чином, ви не можете використовувати цей трюк при злитті 2 різних вмісту колекції, таких як, Postsі Usersтому, що користувач з ідентифікатором 3та публікацією з ідентифікатором 3конфліктуватимуть у цьому випадку, і лише остання (Повідомлення) буде знайдена в об'єднаній колекції.


Якщо ви хочете, щоб це було ще швидше, вам слід створити користувальницький запит, використовуючи фасад DB, щось за рядками:

// 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();

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

Основне підвищення продуктивності тут пов'язане з тим, що важка робота (підзапит) повністю обходить логічну красномовну модель. Тоді все, що залишилося зробити, - це передати список ідентифікаторів у whereInфункцію для отримання списку Formоб’єктів.


0

Я вважаю, що ви можете використовувати для цього колекції Lazy Collection (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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.