Laravel'deki izinlere filtre uygularken performans için en iyi yaklaşım


9

Bir kullanıcının birçok farklı senaryo üzerinden birçok forma erişebildiği bir uygulama üzerinde çalışıyorum. Kullanıcıya bir form dizini döndürürken en iyi performans ile yaklaşım oluşturmaya çalışıyorum.

Bir kullanıcı aşağıdaki senaryolardan formlara erişebilir:

  • Form sahibi
  • Takımın formu var
  • Form sahibi bir gruba izinleri vardır
  • Form sahibi bir ekibe izinleri vardır
  • Bir Forma İzin Verme

Gördüğünüz gibi, kullanıcının bir forma erişmesinin 5 olası yolu vardır. Benim sorunum nasıl en verimli bir şekilde erişilebilir formları bir dizi kullanıcıya dönmek için.

Form Politikası:

Tüm Formları modelden alıp formları form ilkesine göre filtrelemeye çalıştım. Bu, her filtre yinelemesinde, formun aşağıda gösterildiği gibi 5 kez bir include () eloquent yönteminden geçirildiği gibi bir performans sorunu gibi görünüyor. Veritabanındaki daha fazla form bunun yavaşladığı anlamına gelir.

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

Yukarıdaki yöntem işe yarar olmasına rağmen, performans şişe boynudur.

Aşağıdaki seçenekleri görebildiğim kadarıyla:

  • FormPolicy filtresi (mevcut yaklaşım)
  • Tüm izinleri sorgula (5) ve tek bir koleksiyonda birleştir
  • Tüm izinler için tüm tanımlayıcıları sorgulayın (5), ardından IN () ifadesindeki tanımlayıcıları kullanarak Form modelini sorgulayın

Benim sorum:

Hangi yöntem en iyi performansı sağlar ve daha iyi performans sağlayacak başka bir seçenek var mı?


Ayrıca , kullanıcı forma erişebiliyorsa bağlantı için Çok Sayıda yaklaşım da yapabilirsiniz
para için kod

Özellikle kullanıcı formu izinlerini sorgulamak için bir tablo oluşturmaya ne dersiniz? user_form_permissionSadece içeren tablo user_idve form_id. Bu, okuma izinlerini kolaylaştıracak, ancak güncelleme izinleri daha zor olacaktır.
PtrTon

User_form_permissions tablosundaki sorun, daha sonra her bir varlık için ayrı bir tablo gerektiren izinleri diğer varlıklara genişletmek istememizdir.
Tim

1
@ Zaman ama yine de 5 sorgu. Bu, korunan bir üye alanının içindeyse sorun olmayabilir. Ancak bu, saniyede çok sayıda istek alabilen herkese açık bir URL'de ise, bunu biraz optimize etmek isteyeceğinizi tahmin ediyorum. Performans nedeniyle, bir form veya ekip üyesi her model gözlemci aracılığıyla eklendiğinde veya kaldırıldığında ayrı bir tablo (önbelleğe alabileceğim) tutarım. Sonra, her istek üzerine, önbellekten alırsınız. Bu soruyu ve problemi çok ilginç buluyorum ve başkalarının da ne düşündüğünü bilmek isterim. Bu soru daha fazla oy ve cevap hak ediyor, bir ödül başladı :)
Raul

1
Zamanlanmış bir iş olarak yenileyebileceğiniz, somutlaştırılmış bir görünüme sahip olmayı düşünebilirsiniz . Bu şekilde, her zaman hızlı bir şekilde nispeten güncel sonuçlara sahip olabilirsiniz.
apokryfos

Yanıtlar:


2

Ben php çok daha iyi performans olacak gibi bir SQL Sorgu yapmak için bakmak istiyorum

Bunun gibi bir şey:

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

Başımın üstünden ve test edilmemiş olan bu size kullanıcının, gruplarının ve bu ekiplerin sahip olduğu tüm formları almalıdır.

Ancak, gruplar ve ekipler halinde kullanıcı görüntüleme formlarının izinlerine bakmaz.

Nasıl bu sizin için auth ayarlanmış olduğundan emin değilim ve bu yüzden bu sorgu ve DB yapınızdaki herhangi bir fark değiştirmek gerekir.


Cevap için teşekkürler. Ancak, sorun veritabanından veri almak için sorgu değildi. Sorun, uygulamanın yüz binlerce form ve çok sayıda takım ve üyeye sahip olduğu her talepte, her istekte nasıl verimli bir şekilde elde edileceğidir. Birleşimlerinizde ORyavaş olacağından şüphelendiğim hükümler var. Yani her istek üzerine bu vurmak delilik olacak inanıyorum.
Raul

Ham MySQL sorgusu ile veya görünümler veya prosedürler gibi bir şey kullanarak daha iyi hız elde edebilirsiniz, ancak verileri her istediğinizde bu gibi çağrılar yapmanız gerekecektir. Sonuçların önbelleğe alınması da burada yardımcı olabilir.
Josh

Bu performansı yapmanın tek yolunun önbellekleme olduğunu düşünürken, her değişiklik yapıldığında bu haritayı her zaman korumanın maliyeti geliyor. Yeni bir form oluşturduğumu düşünün. Hesabıma bir ekip atanırsa, binlerce kullanıcı buna erişebilir. Sıradaki ne? Birkaç bin üye politikasını yeniden önbelleğe al?
Raul

Ömür boyu önbellek çözümleri vardır (laravel'in önbellek soyutlamaları gibi) ve herhangi bir değişiklik yaptıktan hemen sonra etkilenen önbellek dizinlerini kaldırabilirsiniz. Eğer doğru kullanırsanız önbellek gerçek bir oyun değiştirici. Önbelleğin nasıl yapılandırılacağı, verilerin okunmasına ve güncellenmesine bağlıdır.
Gonzalo

2

Kısa cevap

Üçüncü seçenek: 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();

Uzun cevap

Bir yandan, (neredeyse) kodda yapabileceğiniz her şey, sorgularda olduğundan daha iyi performans açısından daha iyidir.

Öte yandan, veritabanından gerekenden daha fazla veri almak zaten çok fazla veri olacaktır (RAM kullanımı vb.).

Benim açımdan, aralarında bir şeye ihtiyacınız var ve sayılara bağlı olarak dengenin nerede olacağını yalnızca siz bileceksiniz.

Birkaç sorgu çalıştırmanızı öneririm, önerilen son seçenek ( Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. Tüm izinler için tüm tanımlayıcıları sorgula (5 sorgu)
  2. Tüm form sonuçlarını bellekte birleştirin ve benzersiz değerler elde edin array_unique($ids)
  3. Bir IN () deyimindeki tanımlayıcıları kullanarak Form modelini sorgulayın.

Sorguyu birden çok kez çalıştırmak için bazı araçlar kullanarak önerdiğiniz üç seçeneği deneyebilir ve performansı izleyebilirsiniz, ancak sonuncunun size en iyi performansı vereceğinden% 99 eminim.

Bu, hangi veritabanını kullandığınıza bağlı olarak da çok değişebilir, ancak örneğin MySQL'den bahsediyorsak; Çok büyük bir sorguda sadece basit sorgulardan daha fazla zaman harcamakla kalmayacak, aynı zamanda tabloyu yazmalardan kilitleyecek daha fazla veritabanı kaynağı kullanılacaktır ve bu da (bağımlı bir sunucu kullanmıyorsanız) kilitlenme hataları üretebilir.

Diğer tarafta, form kimliği sayısı çok büyükse, çok fazla yer tutucu için hatalarınız olabilir, bu nedenle sorguları 500 id'lik gruplar halinde yığınlamak isteyebilirsiniz (diyelim ki bu sınır olarak çok şey bağlıdır) boyutunda, ciltleme sayısında değil) ve sonuçları bellekte birleştirin. Bir veritabanı hatası almasanız bile, performansta da büyük bir fark görebilirsiniz (hala MySQL'den bahsediyorum).


uygulama

Bunun veritabanı şeması olduğunu varsayacağım:

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

Dolayısıyla izin verilebilir, önceden yapılandırılmış bir polimorfik ilişki olacaktır .

Bu nedenle, ilişkiler şöyle olacaktır:

  • Form sahibi: users.id <-> form.user_id
  • Takımın Formu: users.team_id <-> form.team_id
  • Form sahibi bir gruba izinleri vardır: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • Form sahibi bir ekibe izinleri vardır: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • Bir Form için izne sahip: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

Sürümü basitleştirin:

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

Ayrıntılı sürüm:

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

Kullanılan kaynaklar:

Veritabanı performansı:

  • Veritabanına sorgular (kullanıcı hariç): 2 ; biri müsaade edilebilir, diğeri ise formları alır.
  • Katılma yok !!
  • Mümkün olan minimum OR'ler ( user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...).

PHP, bellekte, performansta:

  • foreach içinde bir anahtar ile izin verilen döngü .
  • array_values(array_unique()) kimlikleri tekrarlamaktan kaçınmak için.
  • Hafızada, kimlikleri 3 diziler ( $teamIds, $groupIds, $formIds)
  • Bellekte, ilgili izinler etkili toplama (gerekirse bu optimize edilebilir).

Lehte ve aleyhte olanlar

Artıları:

  • Zaman : Tek sorguların toplamları, birleştirmeler ve OR ile büyük bir sorgu zamanından daha azdır.
  • DB Kaynakları : join ve veya ifadeleri içeren bir sorgu tarafından kullanılan MySQL kaynakları, ayrı sorgularının toplamı tarafından kullanılandan daha büyüktür.
  • Para : PHP kaynaklarından daha pahalı olan daha az veritabanı kaynağı (işlemci, RAM, disk okuma vb.)
  • Kilitler : Salt okunur bir bağımlı sunucuyu sorgulamıyorsanız, sorgularınız daha az sayıda satır okuma kilidi yapar (okuma kilidi MySQL'de paylaşılır, bu nedenle başka bir okumayı kilitlemez, ancak herhangi bir yazmayı engeller).
  • Ölçeklenebilir : Bu yaklaşım, sorguları yığınlamak gibi daha fazla performans optimizasyonu yapmanızı sağlar.

EKSİLERİ:

  • Kod kaynakları : Veritabanından ziyade kodda hesaplamalar yapmak, kod örneğinde, özellikle de RAM'de orta bilgileri depolayarak daha fazla kaynak tüketecektir. Bizim durumumuzda, bu gerçekten bir sorun olmamalı sadece bir dizi kimlik olurdu.
  • Bakım : Laravel'in özelliklerini ve yöntemlerini kullanırsanız ve veritabanında herhangi bir değişiklik yaparsanız, kodda güncelleme yapmak, daha açık sorgular ve işlem gerçekleştirmekten daha kolay olacaktır.
  • Overkilling? : Bazı durumlarda, veriler o kadar büyük değilse, performansı optimize etmek aşırıya kaçabilir.

Performans nasıl ölçülür

Performansın nasıl ölçüleceğine dair bazı ipuçları?

  1. Yavaş sorgu günlükleri
  2. ANALİZ TABLOSU
  3. TABLO DURUMUNU GİBİ GÖSTER
  4. AÇIKLAMA ; Genişletilmiş EXPLAIN Çıktı Biçimi ; açıklama kullanarak ; çıktıyı açıkla
  5. UYARILARI GÖSTER

Bazı ilginç profil oluşturma araçları:


İlk satır nedir? PHP'de çeşitli döngüler veya dizi manipülasyonu çalıştırmak daha yavaş olduğu için, bir sorgu kullanmak neredeyse her zaman daha iyi performanslıdır.
Alev

Küçük bir veritabanınız varsa veya veritabanı makineniz kod örneğinizden çok daha güçlüse veya veritabanı gecikmesi çok kötü ise, evet, MySQL daha hızlıdır, ancak genellikle durum böyle değildir.
Gonzalo

Bir veritabanı sorgusunu optimize ettiğinizde, yürütme süresini, döndürülen satır sayısını ve en önemlisi incelenen satır sayısını dikkate almanız gerekir. Tim, sorguların yavaşladığını söylüyorsa, verilerin büyüdüğünü ve dolayısıyla incelenen satır sayısının olduğunu varsayıyorum. Ayrıca, veritabanı bir programlama dili olarak işlemek için optimize edilmemiştir.
Gonzalo

Ama bana güvenmene gerek yok , çözümün için EXPLAIN çalıştırabilirsin , o zaman basit sorguları çözümüm için çalıştırabilir ve farkı görebilir ve daha sonra basit array_merge()ve array_unique()bir grup kimliğin, gerçekten yavaşlatır.
Gonzalo

10 durumdan 9'unda mysql veritabanı, kodu çalıştıran makinede çalışır. Veri katmanı veri almak için kullanılır ve büyük setlerden veri parçaları seçmek için optimize edilmiştir. Henüz array_unique()bir GROUP BY/ SELECT DISTINCTifadesinden daha hızlı bir durum görmedim .
Alev

0

Neden Form::all()bir filter()işlevi daha sonra yapmak ve zincirlemek yerine ihtiyacınız olan Formları sorgulayamıyorsunuz ?

Şöyle ki:

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

Evet, bu birkaç sorgu yapar:

  • İçin sorgu $user
  • Tek için $user->team
  • Tek için $user->team->forms
  • Tek için $user->permissible
  • Tek için $user->permissible->groups
  • Tek için $user->permissible->groups->forms

Ancak, yan taraf, parametrede tüm formlara kullanıcı için izin verildiğini bildiğiniz için artık ilkeyi kullanmanıza gerek olmamasıdır$forms .

Bu nedenle, bu çözüm veritabanında bulunan formlar için çalışacaktır.

Kullanmayla ilgili bir not merge()

merge()koleksiyonları birleştirir ve önceden bulduğu yinelenen form kimliklerini atar. Dolayısıyla, herhangi bir nedenden dolayı teamilişkiden gelen bir form aynı zamanda doğrudan bir ilişkiyse user, birleştirilmiş koleksiyonda yalnızca bir kez gösterilir.

Bunun nedeni , Eloquent model kimliklerini kontrol Illuminate\Database\Eloquent\Collectioneden kendi merge()işlevine sahip olmasıdır. Bu nedenle , Postsve gibi 2 farklı koleksiyon içeriğini birleştirirken bu hileyi kullanamazsınız Users, çünkü kimliğe sahip bir kullanıcı ve kimliğine sahip 3bir yayın 3bu durumda çakışacaktır ve birleştirilmiş koleksiyonda yalnızca ikincisi ( Yayın ) bulunacaktır.


Daha da hızlı olmasını istiyorsanız, DB cephesini kullanarak özel bir sorgu oluşturmanız gerekir:

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

Çok fazla ilişkiniz olduğundan gerçek sorgunuz çok daha büyük.

Buradaki ana performans iyileştirmesi, ağır işin (alt sorgu) Eloquent model mantığını tamamen atlaması gerçeğinden kaynaklanmaktadır. Daha sonra tek yapmanız gereken, nesne whereInlistenizi almak için kimlik listesini işleve aktarmaktır Form.


0

Bunun için Lazy Collections'ı (Laravel 6.x) kullanabileceğinizi ve ilişkileri erişmeden önce istekli yükleyebileceğinizi düşünüyorum.

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);
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.