Il miglior approccio per le prestazioni quando si filtrano le autorizzazioni in Laravel


9

Sto lavorando a un'applicazione in cui un utente può avere accesso a molte forme attraverso molti scenari diversi. Sto cercando di costruire l'approccio con le migliori prestazioni quando restituisco un indice di moduli all'utente.

Un utente può avere accesso ai moduli tramite i seguenti scenari:

  • Modulo proprietario
  • Il team possiede il modulo
  • Ha le autorizzazioni per un gruppo che possiede un modulo
  • Ha le autorizzazioni per un team che possiede un modulo
  • Ha l'autorizzazione per un modulo

Come puoi vedere ci sono 5 possibili modi in cui l'utente può accedere a un modulo. Il mio problema è come restituire all'utente in modo più efficiente un array di moduli accessibili.

Politica del modulo:

Ho cercato di ottenere tutti i moduli dal modello e quindi filtrare i moduli in base alla politica del modulo. Questo sembra essere un problema di prestazioni in quanto su ogni iterazione di filtro il modulo viene passato attraverso un metodo eloquent Includes () 5 volte come mostrato di seguito. Più moduli nel database significano che questo diventa più lento.

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

Anche se il metodo sopra funziona, è un collo di bottiglia performante.

Da quello che posso vedere le mie seguenti opzioni sono:

  • Filtro FormPolicy (approccio attuale)
  • Richiedi tutte le autorizzazioni (5) e uniscile in un'unica raccolta
  • Query tutti gli identificatori per tutti i permessi (5), quindi interrogare il modello Form utilizzando gli identificatori in una IN () dichiarazione

La mia domanda:

Quale metodo fornirebbe le migliori prestazioni e c'è qualche altra opzione che fornirebbe prestazioni migliori?


puoi anche creare un approccio Many to Many per collegare se l'utente può accedere al modulo
codice per soldi

Che dire della creazione di una tabella specifica per l'interrogazione delle autorizzazioni dei moduli utente? La user_form_permissiontabella contenente solo il user_ide il form_id. Ciò renderà le autorizzazioni di lettura un gioco da ragazzi, tuttavia l'aggiornamento delle autorizzazioni sarà più difficile.
PtTon,

Il problema con la tabella user_form_permissions è che vogliamo espandere le autorizzazioni ad altre entità che richiederebbero quindi una tabella separata per ciascuna entità.
Tim

1
@Tim ma sono ancora 5 query. Se questo si trova all'interno di un'area riservata, potrebbe non essere un problema. Ma se questo si trova su un URL pubblico che può ricevere molte richieste al secondo, riconosco che dovresti ottimizzarlo un po '. Per motivi di prestazioni, manterrei una tabella separata (che posso memorizzare nella cache) ogni volta che un modulo o un membro del team viene aggiunto o rimosso tramite osservatori di modelli. Quindi, su ogni richiesta, lo prenderei dalla cache. Trovo questa domanda e questo problema molto interessante e mi piacerebbe sapere cosa pensano anche gli altri. Questa domanda merita più voti e risposte, è iniziata una taglia :)
Raul,

1
Potresti considerare di avere una visione materializzata che potresti aggiornare come lavoro pianificato. In questo modo puoi sempre avere risultati relativamente aggiornati rapidamente.
apokryfos,

Risposte:


2

Vorrei fare una query SQL in quanto funzionerà molto meglio di php

Qualcosa come questo:

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

Dalla cima della mia testa e non testato questo dovrebbe ottenere tutti i moduli che sono di proprietà dell'utente, dei suoi gruppi e di questi team.

Tuttavia, non esamina le autorizzazioni dei moduli di visualizzazione utente in gruppi e team.

Non sono sicuro di come sia stata configurata la tua autorizzazione per questo e quindi dovresti modificare la query per questo e qualsiasi differenza nella tua struttura DB.


Grazie per la risposta. Tuttavia, il problema non era la query su come ottenere i dati dal database. Il problema è come ottenerlo in modo efficiente ogni volta, su ogni richiesta, quando l'app ha centinaia di migliaia di moduli e molti team e membri. I tuoi iscritti hanno delle ORclausole, che sospetto ci saranno lente. Quindi, colpire questo su ogni richiesta sarà folle, credo.
Raul,

Potresti essere in grado di ottenere una migliore velocità con query MySQL non elaborate o utilizzando alcune cose come viste o procedure, ma dovrai fare chiamate in questo modo ogni volta che desideri i dati. Anche la memorizzazione nella cache dei risultati potrebbe essere d'aiuto.
Josh,

Mentre penso che l'unico modo per rendere questo performer sia la memorizzazione nella cache, ciò ha il costo di mantenere sempre questa mappa ogni volta che viene apportata una modifica. Immagina di creare un nuovo modulo che, se un team è assegnato al mio account, significa che migliaia di utenti potrebbero accedervi. Qual è il prossimo? Riscrivere in cache alcune migliaia di membri politica?
Raul,

Esistono soluzioni di cache a vita (come le astrazioni di cache di laravel) e puoi anche rimuovere gli indici di cache interessati subito dopo aver apportato qualsiasi modifica. La cache è un vero punto di svolta se la usi correttamente. Come configurare la cache dipende dalle letture e dagli aggiornamenti dei dati.
Gonzalo,

2

Risposta breve

La terza opzione: 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();

Risposta lunga

Da un lato, (quasi) tutto ciò che puoi fare nel codice, è meglio dal punto di vista delle prestazioni, che farlo nelle query.

D'altra parte, ottenere più dati dal database del necessario sarebbe già troppi dati (utilizzo della RAM e così via).

Dal mio punto di vista, hai bisogno di qualcosa nel mezzo e solo tu saprai dove sarebbe l'equilibrio, a seconda dei numeri.

Suggerirei di eseguire diverse query, l'ultima opzione che hai proposto ( Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. Interroga tutti gli identificatori, per tutte le autorizzazioni (5 query)
  2. Unisci tutti i risultati dei moduli in memoria e ottieni valori univoci array_unique($ids)
  3. Eseguire una query sul modello di modulo, utilizzando gli identificativi in ​​un'istruzione IN ().

Puoi provare le tre opzioni che hai proposto e monitorare le prestazioni, usando alcuni strumenti per eseguire la query più volte, ma sono sicuro al 99% che l'ultimo ti darà le migliori prestazioni.

Questo può anche cambiare molto, a seconda del database che stai usando, ma se stiamo parlando di MySQL, per esempio; In Una query molto grande utilizzerebbe più risorse di database, che non solo impiegheranno più tempo delle semplici query, ma bloccheranno anche la tabella dalle scritture e questo può produrre errori di deadlock (a meno che non si usi un server slave).

D'altra parte, se il numero di id di moduli è molto grande, puoi avere errori per troppi segnaposto, quindi potresti voler dividere le query in gruppi di, diciamo, 500 id (questo dipende molto, come limite è di dimensioni, non in numero di associazioni) e unisce i risultati in memoria. Anche se non ricevi un errore del database, potresti notare anche una grande differenza nelle prestazioni (sto ancora parlando di MySQL).


Implementazione

Presumo che questo sia lo schema del database:

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

Così ammissibile sarebbe una relazione polimorfica già configurata .

Pertanto, le relazioni sarebbero:

  • Modulo proprietario: users.id <-> form.user_id
  • Il team possiede un modulo: users.team_id <-> form.team_id
  • Ha le autorizzazioni per un gruppo che possiede un modulo: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • Ha le autorizzazioni per un team che possiede un modulo: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • Ha l'autorizzazione per un modulo: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

Semplifica la versione:

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

Versione dettagliata:

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

Risorse utilizzate:

Prestazioni del database:

  • Domande al database (escluso l'utente): 2 ; uno per ottenere il permesso e un altro per ottenere i moduli.
  • Nessuna partecipazione !!
  • Gli OR minimi possibili ( user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...).

PHP, in memoria, prestazioni:

  • foreach ad anello il ammissibile con un interruttore all'interno.
  • array_values(array_unique()) per evitare di ripetere gli ID.
  • In memoria, 3 matrici di ids ( $teamIds, $groupIds, $formIds)
  • In memoria, raccolta eloquente delle autorizzazioni pertinenti (questo può essere ottimizzato, se necessario).

Pro e contro

PROFESSIONISTI:

  • Tempo : la somma dei tempi delle singole query è inferiore al tempo di una query di grandi dimensioni con join e OR.
  • Risorse DB : le risorse MySQL utilizzate da una query con join e o istruzioni sono maggiori di quelle utilizzate dalla somma delle sue query separate.
  • Denaro : meno risorse del database (processore, RAM, lettura del disco, ecc.), Che sono più costose delle risorse PHP.
  • Blocchi : nel caso in cui non si stia eseguendo una query su un server slave di sola lettura, le query eseguiranno un numero inferiore di blocchi di lettura delle righe (il blocco di lettura è condiviso in MySQL, quindi non bloccherà un'altra lettura, ma bloccherà qualsiasi scrittura).
  • Scalabile : questo approccio consente di apportare ulteriori ottimizzazioni delle prestazioni, ad esempio bloccare le query.

CONS:

  • Risorse di codice : fare calcoli nel codice, piuttosto che nel database, consumerà ovviamente più risorse nell'istanza del codice, ma soprattutto nella RAM, memorizzando le informazioni di mezzo. Nel nostro caso, questo sarebbe solo un array di ID, che non dovrebbe essere un problema.
  • Manutenzione : se si utilizzano le proprietà e i metodi di Laravel e si apportano modifiche al database, sarà più semplice aggiornarlo nel codice che se si effettuano query ed elaborazioni più esplicite.
  • Overkilling? : In alcuni casi, se i dati non sono così grandi, l'ottimizzazione delle prestazioni potrebbe essere eccessiva.

Come misurare le prestazioni

Alcuni indizi su come misurare le prestazioni?

  1. Log delle query lenti
  2. TABELLA ANALISI
  3. MOSTRA STATO TABELLA COME
  4. SPIEGARE ; Formato di output EXPLAIN esteso ; usando spiega ; spiegare l'output
  5. MOSTRA AVVERTENZE

Alcuni strumenti di profilazione interessanti:


Qual è quella prima linea? È quasi sempre meglio usare una query in termini di prestazioni, poiché l'esecuzione di vari loop o manipolazione di array in PHP è più lenta.
Fiamma

Se hai un piccolo database o la tua macchina database è molto più potente della tua istanza di codice, o la latenza del database è molto bassa, allora sì, MySQL è più veloce, ma di solito non è così.
Gonzalo,

Quando si ottimizza una query del database, è necessario considerare il tempo di esecuzione, il numero di righe restituite e, soprattutto, il numero di righe esaminate. Se Tim sta dicendo che le query stanno diventando lente, allora presumo che i dati stiano crescendo e quindi il numero di righe esaminate. Inoltre, il database non è ottimizzato per l'elaborazione come lo è un linguaggio di programmazione.
Gonzalo,

Ma non hai bisogno di fidarti di me, puoi eseguire EXPLAIN , per la tua soluzione, quindi puoi eseguirlo per la mia soluzione di query semplici, e vedere la differenza, e poi pensare se un semplice array_merge()e array_unique()un gruppo di ID, rallenta davvero il tuo processo.
Gonzalo,

In 9 casi su 10 il database mysql viene eseguito sulla stessa macchina che esegue il codice. Il livello dati è pensato per essere utilizzato per il recupero dei dati ed è ottimizzato per la selezione di pezzi di dati da insiemi di grandi dimensioni. Devo ancora vedere una situazione in cui a array_unique()è più veloce di un'istruzione GROUP BY/ SELECT DISTINCT.
Fiamma

0

Perché non puoi semplicemente interrogare i moduli di cui hai bisogno, invece di fare Form::all()e poi concatenare una filter()funzione dopo di essa?

Così:

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

Quindi sì, questo fa alcune domande:

  • Una query per $user
  • Uno per $user->team
  • Uno per $user->team->forms
  • Uno per $user->permissible
  • Uno per $user->permissible->groups
  • Uno per $user->permissible->groups->forms

Tuttavia, il lato positivo è che non è più necessario utilizzare la politica , poiché si sa che tutti i moduli nel $formsparametro sono consentiti per l'utente.

Quindi questa soluzione funzionerà per qualsiasi quantità di moduli tu abbia nel database.

Una nota sull'uso merge()

merge()unisce le raccolte e eliminerà gli ID dei moduli duplicati che ha già trovato. Quindi, se per qualche motivo un modulo della teamrelazione è anche una relazione diretta con la user, verrà mostrato solo una volta nella raccolta unita.

Questo perché in realtà è uno Illuminate\Database\Eloquent\Collectionche ha una propria merge()funzione che controlla gli ID del modello eloquente. Quindi non puoi effettivamente usare questo trucco quando unisci 2 diversi contenuti della raccolta come Postse Users, poiché un utente con ID 3e un post con ID 3entreranno in conflitto in questo caso e solo quest'ultimo (il Post) verrà trovato nella raccolta unita.


Se vuoi che sia ancora più veloce, dovresti creare una query personalizzata usando la facciata DB, qualcosa del tipo di:

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

La tua query effettiva è molto più grande poiché hai così tante relazioni.

Il principale miglioramento delle prestazioni qui deriva dal fatto che il lavoro pesante (la sottoquery) bypassa completamente la logica del modello Eloquent. Quindi non resta che passare l'elenco di ID nella whereInfunzione per recuperare l'elenco di Formoggetti.


0

Credo che tu possa usare Lazy Collections per questo (Laravel 6.x) e desiderare di caricare le relazioni prima di accedervi.

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);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.