Bester Ansatz für die Leistung beim Filtern nach Berechtigungen in Laravel

9

Ich arbeite an einer Anwendung, bei der ein Benutzer über viele verschiedene Szenarien auf viele Formulare zugreifen kann. Ich versuche, den Ansatz mit der besten Leistung zu erstellen, wenn ein Index von Formularen an den Benutzer zurückgegeben wird.

Ein Benutzer kann über die folgenden Szenarien auf Formulare zugreifen:

  • Besitzt Formular
  • Team besitzt Form
  • Hat Berechtigungen für eine Gruppe, die ein Formular besitzt
  • Hat Berechtigungen für ein Team, das ein Formular besitzt
  • Hat die Erlaubnis zu einem Formular

Wie Sie sehen, gibt es 5 Möglichkeiten, wie der Benutzer auf ein Formular zugreifen kann. Mein Problem ist, wie ich ein Array der zugänglichen Formulare am effizientesten an den Benutzer zurückgeben kann.

Formularrichtlinie:

Ich habe versucht, alle Formulare aus dem Modell abzurufen und die Formulare dann nach der Formularrichtlinie zu filtern. Dies scheint ein Leistungsproblem zu sein, da bei jeder Filteriteration das Formular fünfmal durch eine beredte Methode mit enthält () geleitet wird, wie unten gezeigt. Je mehr Formulare in der Datenbank vorhanden sind, desto langsamer wird dies.

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);
}

Obwohl die obige Methode funktioniert, handelt es sich um einen Leistungsflaschenhals.

Nach dem, was ich sehen kann, sind meine folgenden Optionen:

  • FormPolicy-Filter (aktueller Ansatz)
  • Fragen Sie alle Berechtigungen ab (5) und führen Sie sie zu einer einzigen Sammlung zusammen
  • Fragen Sie alle Bezeichner nach allen Berechtigungen ab (5) und fragen Sie dann das Formularmodell mithilfe der Bezeichner in einer IN () -Anweisung ab

Meine Frage:

Welche Methode bietet die beste Leistung und gibt es eine andere Option, die eine bessere Leistung bietet?

Tim
quelle
Sie können auch einen machen viele zu viele Ansatz Link , wenn der Benutzer das Formular zugreifen können
Code - Leistungs - Verhältnis
Was ist mit dem Erstellen einer Tabelle speziell zum Abfragen von Benutzerformularberechtigungen? Die user_form_permissionTabelle enthält nur das user_idund das form_id. Dadurch werden Leseberechtigungen zum Kinderspiel, das Aktualisieren von Berechtigungen wird jedoch schwieriger.
PtrTon
Das Problem mit der Tabelle user_form_permissions besteht darin, dass wir die Berechtigungen auf andere Entitäten erweitern möchten, für die dann für jede Entität eine separate Tabelle erforderlich wäre.
Tim
1
@ Tim, aber das sind noch 5 Fragen. Wenn sich dies nur innerhalb des Bereichs eines geschützten Mitglieds befindet, ist dies möglicherweise kein Problem. Aber wenn dies auf einer öffentlich zugänglichen URL ist, die viele Anfragen pro Sekunde erhalten kann, sollten Sie dies ein wenig optimieren. Aus Leistungsgründen würde ich jedes Mal, wenn ein Formular oder ein Teammitglied über Modellbeobachter hinzugefügt oder entfernt wird, eine separate Tabelle verwalten (die ich zwischenspeichern kann). Dann würde ich das bei jeder Anfrage aus dem Cache bekommen. Ich finde diese Frage und dieses Problem sehr interessant und würde gerne wissen, was andere auch denken. Diese Frage verdient mehr Stimmen und Antworten, begann ein Kopfgeld :)
Raul
1
Sie könnten eine materialisierte Ansicht in Betracht ziehen, die Sie als geplanten Job aktualisieren können. Auf diese Weise können Sie immer schnell relativ aktuelle Ergebnisse erzielen.
Apokryfos

Antworten:

2

Ich würde versuchen, eine SQL-Abfrage durchzuführen, da dies viel besser als PHP ist

Etwas wie das:

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

Von oben und ungetestet sollten Sie alle Formulare erhalten, die dem Benutzer, seinen Gruppen und diesen Teams gehören.

Die Berechtigungen der Benutzeransichtsformulare in Gruppen und Teams werden jedoch nicht berücksichtigt.

Ich bin nicht sicher, wie Sie Ihre Authentifizierung dafür eingerichtet haben, und daher müssten Sie die Abfrage für diese und alle Unterschiede in Ihrer DB-Struktur ändern.

Josh
quelle
Danke für die Antwort. Das Problem war jedoch nicht die Abfrage, wie die Daten aus der Datenbank abgerufen werden sollen. Das Problem ist, wie man es jedes Mal und bei jeder Anfrage effizient erhält, wenn die App hunderttausende Formulare und viele Teams und Mitglieder hat. Ihre Joins enthalten ORKlauseln, von denen ich vermute, dass sie langsam sein werden. Ich glaube, es wird verrückt sein, dies bei jeder Anfrage zu tun.
Raul
Möglicherweise können Sie mit rohen MySQL-Abfragen oder mithilfe von Ansichten oder Prozeduren eine bessere Geschwindigkeit erzielen, müssen jedoch jedes Mal, wenn Sie die Daten benötigen, solche Aufrufe ausführen. Hier kann auch das Zwischenspeichern von Ergebnissen hilfreich sein.
Josh
Während ich denke, dass der einzige Weg, diesen Performanten zu machen, das Zwischenspeichern ist, geht dies zu Lasten der ständigen Pflege dieser Karte bei jeder Änderung. Stellen Sie sich vor, ich erstelle ein neues Formular. Wenn einem Team ein Konto zugewiesen wird, erhalten möglicherweise Tausende von Benutzern Zugriff darauf. Was kommt als nächstes? Einige tausend Mitglieder neu zwischenspeichern?
Raul
Es gibt Cache-Lösungen mit Lebensdauer (wie die Cache-Abstraktionen von Laravel), und Sie können die betroffenen Cache-Indizes auch direkt nach einer Änderung entfernen. Der Cache ist ein echter Game-Changer, wenn Sie ihn richtig verwenden. Wie der Cache konfiguriert wird, hängt von den Lesevorgängen und Aktualisierungen der Daten ab.
Gonzalo
2

Kurze Antwort

Die dritte Option: 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();

Lange Antwort

Einerseits ist (fast) alles, was Sie in Code tun können, in Bezug auf die Leistung besser als in Abfragen.

Andererseits würde das Abrufen von mehr Daten aus der Datenbank als erforderlich bereits zu viele Daten sein (RAM-Nutzung usw.).

Aus meiner Sicht brauchen Sie etwas dazwischen, und nur Sie werden wissen, wo das Gleichgewicht sein würde, abhängig von den Zahlen.

Ich würde vorschlagen, mehrere Abfragen auszuführen, die letzte Option, die Sie vorgeschlagen haben ( Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. Fragen Sie alle Bezeichner für alle Berechtigungen ab (5 Abfragen).
  2. Das Zusammenführen aller Formulare führt zum Speicher und zu eindeutigen Werten array_unique($ids)
  3. Fragen Sie das Formularmodell mit den Bezeichnern in einer IN () - Anweisung ab.

Sie können die drei von Ihnen vorgeschlagenen Optionen ausprobieren und die Leistung überwachen, indem Sie die Abfrage mit einem Tool mehrmals ausführen. Ich bin jedoch zu 99% sicher, dass die letzte Option die beste Leistung bietet.

Dies kann sich auch stark ändern, je nachdem, welche Datenbank Sie verwenden, aber wenn wir zum Beispiel über MySQL sprechen; In einer sehr großen Abfrage werden mehr Datenbankressourcen verwendet, die nicht nur mehr Zeit als einfache Abfragen aufwenden, sondern auch die Tabelle vor Schreibvorgängen schützen. Dies kann zu Deadlock-Fehlern führen (es sei denn, Sie verwenden einen Slave-Server).

Wenn andererseits die Anzahl der Formular-IDs sehr groß ist, können Fehler für zu viele Platzhalter auftreten. Daher möchten Sie die Abfragen möglicherweise in Gruppen von beispielsweise 500 IDs aufteilen (dies hängt stark von der Grenze ab ist in der Größe, nicht in der Anzahl der Bindungen) und führen die Ergebnisse im Speicher zusammen. Selbst wenn Sie keinen Datenbankfehler erhalten, können Sie auch einen großen Leistungsunterschied feststellen (ich spreche immer noch über MySQL).


Implementierung

Ich gehe davon aus, dass dies das Datenbankschema ist:

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

Zulässig wäre also eine bereits konfigurierte polymorphe Beziehung .

Daher wären die Beziehungen:

  • Besitzt Formular: users.id <-> form.user_id
  • Team besitzt Form: users.team_id <-> form.team_id
  • Hat Berechtigungen für eine Gruppe, die ein Formular besitzt: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • Hat Berechtigungen für ein Team, das ein Formular besitzt: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • Hat die Erlaubnis zu einem Formular: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

Version vereinfachen:

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

Detaillierte Version:

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

Verwendete Ressourcen:

Datenbankleistung:

  • Abfragen an die Datenbank (ohne den Benutzer): 2 ; eine, um das Zulässige zu bekommen, und eine andere, um die Formulare zu bekommen.
  • Keine Joins !!
  • Die minimal möglichen OPs ( user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...).

PHP, im Speicher, Leistung:

  • foreach Schleifen der zulässigen mit einem Schalter Inneren.
  • array_values(array_unique()) um zu vermeiden, dass die IDs wiederholt werden.
  • Im Speicher, 3 Anordnungen von IDs ( $teamIds, $groupIds,$formIds )
  • Im Speicher relevante Berechtigungen beredte Sammlung (diese kann bei Bedarf optimiert werden).

Vor-und Nachteile

PROS:

  • Zeit : Die Summe der Zeiten einzelner Abfragen ist kürzer als die Zeit einer großen Abfrage mit Verknüpfungen und ODER.
  • DB-Ressourcen : Die MySQL-Ressourcen, die von einer Abfrage mit Join- und / oder Anweisungen verwendet werden, sind größer als die Summe, die von der Summe ihrer separaten Abfragen verwendet wird.
  • Geld : Weniger Datenbankressourcen (Prozessor, RAM, Lesen von Datenträgern usw.), die teurer sind als PHP-Ressourcen.
  • Schlösser : Wenn Sie keinen schreibgeschützten Slave-Server abfragen, führen Ihre Abfragen zu weniger Lesesperren für Zeilen (die Lesesperre wird in MySQL gemeinsam genutzt, sodass kein weiterer Lesevorgang gesperrt wird, aber kein Schreibvorgang blockiert wird).
  • Skalierbar : Mit diesem Ansatz können Sie weitere Leistungsoptimierungen vornehmen, z. B. die Abfragen aufteilen.

Nachteile:

  • Code-Ressourcen : Durch Berechnungen im Code und nicht in der Datenbank werden offensichtlich mehr Ressourcen in der Code-Instanz, insbesondere aber im RAM, verbraucht, wodurch die mittleren Informationen gespeichert werden. In unserem Fall wäre dies nur eine Reihe von IDs, was eigentlich kein Problem sein sollte.
  • Wartung : Wenn Sie die Eigenschaften und Methoden von Laravel verwenden und Änderungen an der Datenbank vornehmen, ist die Aktualisierung des Codes einfacher als bei expliziteren Abfragen und Verarbeitungen.
  • Übermaß? : In einigen Fällen kann die Optimierung der Leistung zu viel des Guten sein, wenn die Daten nicht so groß sind.

So messen Sie die Leistung

Einige Hinweise zur Messung der Leistung?

  1. Langsame Abfrageprotokolle
  2. ANALYSETABELLE
  3. TABELLENSTATUS WIE ANZEIGEN
  4. EXPLAIN ; Erweitertes EXPLAIN-Ausgabeformat ; mit EXPLAIN ; Ausgabe erklären
  5. WARNHINWEISE ANZEIGEN

Einige interessante Profiling-Tools:

Gonzalo
quelle
Was ist das für eine erste Zeile? In Bezug auf die Leistung ist es fast immer besser, eine Abfrage zu verwenden, da das Ausführen verschiedener Schleifen oder Array-Manipulationen in PHP langsamer ist.
Flamme
Wenn Sie eine kleine Datenbank haben oder Ihr Datenbankcomputer viel leistungsfähiger als Ihre Codeinstanz ist oder die Datenbanklatenz sehr schlecht ist, ist MySQL zwar schneller, dies ist jedoch normalerweise nicht der Fall.
Gonzalo
Wenn Sie eine Datenbankabfrage optimieren, müssen Sie die Ausführungszeit, die Anzahl der zurückgegebenen Zeilen und vor allem die Anzahl der untersuchten Zeilen berücksichtigen. Wenn Tim sagt, dass die Abfragen langsam werden, gehe ich davon aus, dass die Daten wachsen und daher die Anzahl der untersuchten Zeilen. Außerdem ist die Datenbank nicht für die Verarbeitung als Programmiersprache optimiert.
Gonzalo
Aber Sie müssen mir nicht vertrauen, Sie können EXPLAIN für Ihre Lösung ausführen , dann können Sie es für meine Lösung einfacher Abfragen ausführen und den Unterschied erkennen und dann überlegen , ob eine einfache array_merge()und array_unique()eine Reihe von IDs dies tun würden verlangsamen Sie wirklich Ihren Prozess.
Gonzalo
In 9 von 10 Fällen wird die MySQL-Datenbank auf demselben Computer ausgeführt, auf dem der Code ausgeführt wird. Die Datenschicht soll zum Abrufen von Daten verwendet werden und ist für die Auswahl von Daten aus großen Mengen optimiert. Ich habe noch keine Situation gesehen, in der a array_unique()schneller ist als eine GROUP BY/ SELECT DISTINCT-Anweisung.
Flamme
0

Warum können Sie nicht einfach die Formulare abfragen, die Sie benötigen, anstatt a zu tun Form::all()und dann zu verketten?filter() Funktion ?

Wie so:

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

Also ja, das macht ein paar Fragen:

  • Eine Abfrage für $user
  • Eins für $user->team
  • Eins für $user->team->forms
  • Eins für $user->permissible
  • Eins für $user->permissible->groups
  • Eins für $user->permissible->groups->forms

Der Vorteil ist jedoch, dass Sie die Richtlinie nicht mehr verwenden müssen , da Sie wissen, dass alle Formulare im $formsParameter für den Benutzer zulässig sind.

Diese Lösung funktioniert also für jede Anzahl von Formularen, die Sie in der Datenbank haben.

Ein Hinweis zur Verwendung merge()

merge()führt die Sammlungen zusammen und verwirft doppelte Formular-IDs, die bereits gefunden wurden. Wenn also aus irgendeinem Grund ein Formular aus der teamBeziehung auch eine direkte Beziehung zu der ist user, wird es in der zusammengeführten Sammlung nur einmal angezeigt.

Dies liegt daran, dass es sich tatsächlich um eine Funktion handelt, Illuminate\Database\Eloquent\Collectiondie über eine eigene merge()Funktion verfügt, die nach den eloquenten Modell-IDs sucht. Sie können diesen Trick also nicht verwenden, wenn Sie 2 verschiedene Sammlungsinhalte wie Postsund zusammenführen Users, da in diesem Fall ein Benutzer mit ID 3und ein Beitrag mit ID 3in Konflikt geraten und nur letzterer (der Beitrag) in der zusammengeführten Sammlung gefunden wird.


Wenn Sie möchten, dass es noch schneller geht, sollten Sie mithilfe der DB-Fassade eine benutzerdefinierte Abfrage erstellen.

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

Ihre eigentliche Abfrage ist viel größer, da Sie so viele Beziehungen haben.

Die Hauptverbesserung der Leistung ergibt sich aus der Tatsache, dass die schwere Arbeit (die Unterabfrage) die eloquente Modelllogik vollständig umgeht. Dann müssen Sie nur noch die Liste der IDs an die whereInFunktion übergeben, um Ihre Liste der FormObjekte abzurufen .

Flamme
quelle
0

Ich glaube, Sie können Lazy Collections dafür verwenden (Laravel 6.x) und die Beziehungen eifrig laden, bevor auf sie zugegriffen wird.

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);
}
IGP
quelle