Pendekatan terbaik untuk kinerja saat memfilter untuk izin di Laravel

9

Saya sedang mengerjakan aplikasi di mana pengguna dapat memiliki akses ke berbagai bentuk melalui banyak skenario yang berbeda. Saya mencoba membangun pendekatan dengan kinerja terbaik saat mengembalikan indeks formulir kepada pengguna.

Seorang pengguna dapat memiliki akses ke formulir melalui skenario berikut:

  • Formulir Milik
  • Tim memiliki Formulir
  • Memiliki izin ke grup yang memiliki Formulir
  • Memiliki izin ke tim yang memiliki Formulir
  • Memiliki izin untuk Formulir

Seperti yang Anda lihat ada 5 cara yang memungkinkan pengguna dapat mengakses formulir. Masalah saya adalah bagaimana cara paling efisien mengembalikan array formulir yang dapat diakses kepada pengguna.

Formulir Kebijakan:

Saya telah mencoba untuk mendapatkan semua Formulir dari model dan kemudian menyaring formulir dengan kebijakan formulir. Ini tampaknya menjadi masalah kinerja karena pada setiap iterasi filter formulir diteruskan melalui metode () fasih 5 kali seperti yang ditunjukkan di bawah ini. Semakin banyak formulir dalam database berarti ini menjadi lebih lambat.

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

Meskipun metode di atas berhasil, itu adalah kinerja botol leher.

Dari apa yang saya dapat melihat opsi saya berikut ini adalah:

  • Filter FormPolicy (pendekatan saat ini)
  • Permintaan semua izin (5) dan gabungkan menjadi satu koleksi
  • Query semua pengidentifikasi untuk semua izin (5), kemudian permintaan model Form menggunakan pengidentifikasi dalam IN () pernyataan

Pertanyaan saya:

Metode mana yang akan memberikan kinerja terbaik dan apakah ada opsi lain yang akan memberikan kinerja yang lebih baik?

Tim
sumber
Anda juga dapat membuat pendekatan Banyak Ke Banyak untuk menautkan jika pengguna dapat mengakses formulir
kode untuk uang
Bagaimana dengan membuat tabel khusus untuk menanyakan izin formulir pengguna? The user_form_permissiontabel yang berisi hanya user_iddan form_id. Ini akan membuat izin membaca menjadi lebih mudah, namun memperbarui izin akan lebih sulit.
PtrTon
Masalah dengan tabel user_form_permissions adalah bahwa kami ingin memperluas izin ke entitas lain yang kemudian akan membutuhkan tabel terpisah untuk setiap entitas.
Tim
1
@Tim, tapi itu masih 5 pertanyaan. Jika ini hanya di dalam area anggota yang dilindungi, mungkin tidak menjadi masalah. Tetapi jika ini adalah URL yang menghadap publik yang bisa mendapatkan banyak permintaan per detik, saya rekomendasikan Anda ingin sedikit mengoptimalkan ini. Untuk alasan kinerja, saya akan mempertahankan tabel terpisah (yang dapat saya cache) setiap kali formulir atau anggota tim ditambahkan atau dihapus melalui pengamat model. Kemudian, pada setiap permintaan, saya akan mendapatkannya dari cache. Saya menemukan pertanyaan dan masalah ini sangat menarik dan ingin tahu apa yang dipikirkan orang lain juga. Pertanyaan ini layak mendapat lebih banyak suara dan jawaban, memulai hadiah :)
Raul
1
Anda dapat mempertimbangkan memiliki tampilan terwujud yang dapat Anda menyegarkan sebagai pekerjaan yang dijadwalkan. Dengan cara ini Anda dapat selalu mendapatkan hasil yang relatif terbaru dengan cepat.
apokryfos

Jawaban:

2

Saya akan mencari untuk melakukan SQL Query karena itu akan melakukan jauh lebih baik daripada php

Sesuatu seperti ini:

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

Dari atas kepala saya dan belum teruji, ini akan memberi Anda semua formulir yang dimiliki oleh pengguna, grupnya, dan tim ini.

Namun itu tidak melihat izin formulir tampilan pengguna dalam grup dan tim.

Saya tidak yakin bagaimana Anda mengatur auth Anda untuk ini dan jadi Anda perlu memodifikasi permintaan untuk ini dan perbedaan dalam struktur DB Anda.

Josh
sumber
Terima kasih atas jawabannya. Namun, masalahnya bukan pada pertanyaan tentang cara mendapatkan data dari database. Masalahnya adalah, bagaimana cara mendapatkannya secara efisien setiap kali, atas setiap permintaan, ketika aplikasi memiliki ratusan ribu formulir dan banyak tim serta anggota. Gabungan Anda memiliki ORklausa, yang saya curigai akan lambat. Jadi memukul ini pada setiap permintaan akan gila saya percaya.
Raul
Anda mungkin bisa mendapatkan kecepatan yang lebih baik dengan permintaan MySQL mentah atau menggunakan beberapa hal seperti tampilan atau prosedur tetapi Anda harus melakukan panggilan seperti ini setiap kali Anda menginginkan data. Caching hasil juga dapat membantu di sini.
Josh
Sementara saya berpikir satu-satunya cara untuk membuat pemain ini adalah melakukan caching, itu harus dibayar dengan selalu menjaga peta ini setiap kali ada perubahan. Bayangkan saya membuat formulir baru, yang, jika sebuah tim ditugaskan ke akun saya berarti ribuan pengguna mungkin mendapatkan akses ke sana. Apa berikutnya? Men-cache ulang beberapa ribu kebijakan anggota?
Raul
Ada solusi cache dengan masa pakai (seperti abstraksi cache laravel), dan Anda juga dapat menghapus indeks cache yang terpengaruh setelah Anda melakukan perubahan apa pun. Cache adalah pengubah permainan nyata jika Anda menggunakannya dengan benar. Cara mengkonfigurasi cache tergantung dari pembacaan dan pembaruan data.
Gonzalo
2

Jawaban singkat

Opsi ketiga: 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();

Jawaban panjang

Di satu sisi, (hampir) semua yang dapat Anda lakukan dalam kode, adalah kinerja yang lebih baik, daripada melakukannya dalam kueri.

Di sisi lain, mendapatkan lebih banyak data dari basis data dari yang diperlukan akan terlalu banyak data (penggunaan RAM dan sebagainya).

Dari sudut pandang saya, Anda membutuhkan sesuatu di antaranya, dan hanya Anda yang akan tahu di mana keseimbangannya, tergantung pada jumlahnya.

Saya sarankan menjalankan beberapa kueri, opsi terakhir yang Anda usulkan ( Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. Permintaan semua pengidentifikasi, untuk semua izin (5 pertanyaan)
  2. Menggabungkan semua formulir menghasilkan memori, dan dapatkan nilai unik array_unique($ids)
  3. Permintaan model Formulir, menggunakan pengidentifikasi dalam pernyataan IN ().

Anda dapat mencoba tiga opsi yang Anda usulkan dan memantau kinerja, menggunakan beberapa alat untuk menjalankan kueri beberapa kali, tetapi saya 99% yakin bahwa yang terakhir akan memberi Anda kinerja terbaik.

Ini juga dapat banyak berubah, tergantung pada basis data yang Anda gunakan, tetapi jika kita berbicara tentang MySQL, misalnya; Dalam Permintaan yang sangat besar akan menggunakan lebih banyak sumber daya database, yang tidak hanya akan menghabiskan lebih banyak waktu daripada permintaan sederhana, tetapi juga akan mengunci tabel dari menulis, dan ini dapat menghasilkan kesalahan kebuntuan (kecuali jika Anda menggunakan server slave).

Di sisi lain, jika jumlah bentuk id sangat besar, Anda dapat memiliki kesalahan untuk placeholder terlalu banyak, jadi Anda mungkin ingin memotong kueri dalam grup, katakanlah, 500 id (ini tergantung banyak, sebagai batas dalam ukuran, bukan dalam jumlah binding), dan menggabungkan hasilnya dalam memori. Bahkan jika Anda tidak mendapatkan kesalahan basis data, Anda mungkin melihat perbedaan besar dalam kinerja juga (saya masih berbicara tentang MySQL).


Penerapan

Saya akan berasumsi bahwa ini adalah skema basis data:

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

Jadi diizinkan adalah hubungan polimorfik yang sudah terkonfigurasi .

Karena itu, hubungannya adalah:

  • Formulir Milik: users.id <-> form.user_id
  • Tim memiliki Formulir: users.team_id <-> form.team_id
  • Memiliki izin ke grup yang memiliki Formulir: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • Memiliki izin ke tim yang memiliki Formulir: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • Memiliki izin untuk Formulir: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

Sederhanakan versi:

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

Versi detail:

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

Sumber daya yang digunakan:

Kinerja basis data:

  • Permintaan ke database (tidak termasuk pengguna): 2 ; satu untuk mendapatkan yang diizinkan dan satu lagi untuk mendapatkan formulir.
  • Tidak ada yang bergabung !!
  • OR minimum yang mungkin ( user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...).

PHP, dalam memori, kinerja:

  • foreach perulangan diizinkan dengan saklar di dalamnya.
  • array_values(array_unique()) untuk menghindari pengulangan id.
  • Dalam memori, 3 array id ( $teamIds, $groupIds, $formIds)
  • Dalam memori, izin relevan koleksi fasih (ini dapat dioptimalkan, jika perlu).

Pro dan kontra

PROS:

  • Waktu : Jumlah waktu kueri tunggal kurang dari waktu kueri besar dengan bergabung dan ATAU.
  • Sumber Daya DB : Sumber daya MySQL yang digunakan oleh kueri dengan gabungan dan atau pernyataan, lebih besar dari yang digunakan oleh jumlah kueri yang terpisah.
  • Uang : Lebih sedikit sumber daya basis data (prosesor, RAM, disk baca, dll.), Yang lebih mahal daripada sumber daya PHP.
  • Kunci : Jika Anda tidak meminta server slave baca-saja, kueri Anda akan membuat lebih sedikit baris kunci baca (kunci baca dibagi di MySQL, sehingga tidak akan mengunci read lain, tetapi itu akan memblokir penulisan apa pun).
  • Dapat diskalakan : Pendekatan ini memungkinkan Anda untuk membuat lebih banyak optimasi kinerja seperti memotong kueri.

CONS:

  • Sumberdaya kode : Membuat perhitungan dalam kode, bukan dalam database, jelas akan mengkonsumsi lebih banyak sumber daya dalam contoh kode, tetapi terutama dalam RAM, menyimpan informasi tengah. Dalam kasus kami, ini akan menjadi array id, yang seharusnya tidak menjadi masalah.
  • Pemeliharaan : Jika Anda menggunakan properti dan metode Laravel, dan Anda melakukan perubahan apa pun dalam database, akan lebih mudah untuk memperbarui dalam kode daripada jika Anda membuat pertanyaan dan pemrosesan yang lebih eksplisit.
  • Terlalu banyak membunuh? : Dalam beberapa kasus, jika data tidak sebesar itu, mengoptimalkan kinerja mungkin terlalu banyak.

Bagaimana mengukur kinerja

Beberapa petunjuk tentang cara mengukur kinerja?

  1. Log kueri lambat
  2. TABEL ANALISIS
  3. TAMPILKAN STATUS TABEL SEPERTI
  4. MENJELASKAN ; Format Output EXPLAIN yang Diperpanjang ; menggunakan jelaskan ; jelaskan output
  5. TAMPILKAN PERINGATAN

Beberapa alat profil yang menarik:

Gonzalo
sumber
Apa itu baris pertama? Ini hampir selalu kinerja yang lebih baik untuk menggunakan kueri, karena menjalankan berbagai loop atau manipulasi array di PHP lebih lambat.
Nyala
Jika Anda memiliki basis data kecil atau mesin basis data Anda jauh lebih kuat daripada contoh kode Anda, atau latensi basis data sangat buruk, maka ya, MySQL lebih cepat, tetapi biasanya tidak demikian.
Gonzalo
Saat Anda mengoptimalkan kueri basis data, Anda perlu mempertimbangkan waktu eksekusi, jumlah baris yang dikembalikan, dan yang paling penting, jumlah baris yang diperiksa. Jika Tim mengatakan bahwa kueri menjadi lambat, maka saya berasumsi bahwa data bertambah, dan karena itu jumlah baris diperiksa. Selain itu, basis data tidak dioptimalkan untuk diproses seperti bahasa pemrograman.
Gonzalo
Tetapi Anda tidak perlu mempercayai saya, Anda dapat menjalankan EXPLAIN , untuk solusi Anda, kemudian Anda dapat menjalankannya untuk solusi saya dari pertanyaan sederhana, dan melihat perbedaannya, dan kemudian berpikir jika yang sederhana array_merge()dan array_unique()banyak id, akan sangat memperlambat proses Anda.
Gonzalo
Dalam 9 dari 10 kasus database mysql berjalan pada mesin yang sama yang menjalankan kode. Lapisan data dimaksudkan untuk digunakan untuk pengambilan data dan dioptimalkan untuk memilih potongan data dari set besar. Saya belum melihat situasi di mana a array_unique()lebih cepat dari GROUP BY/ SELECT DISTINCTpernyataan.
Api
0

Mengapa Anda tidak bisa langsung menanyakan Formulir yang Anda butuhkan, alih-alih melakukan Form::all()dan kemudian merantai filter()fungsi setelahnya?

Seperti itu:

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

Jadi ya, ini beberapa pertanyaan:

  • Permintaan untuk $user
  • Untuk satu $user->team
  • Untuk satu $user->team->forms
  • Untuk satu $user->permissible
  • Untuk satu $user->permissible->groups
  • Untuk satu $user->permissible->groups->forms

Namun, sisi pro adalah bahwa Anda tidak perlu lagi menggunakan kebijakan , karena Anda tahu semua formulir di $formsparameter diperbolehkan untuk pengguna.

Jadi solusi ini akan bekerja untuk berapa pun jumlah formulir yang Anda miliki di database.

Catatan tentang penggunaan merge()

merge()menggabungkan koleksi, dan akan membuang id formulir duplikat yang sudah ditemukan. Jadi jika karena alasan tertentu bentuk dari teamrelasi juga merupakan relasi langsung ke user, itu hanya akan ditampilkan sekali dalam koleksi gabungan.

Ini karena itu sebenarnya adalah Illuminate\Database\Eloquent\Collectionyang memiliki merge()fungsi sendiri yang memeriksa id model Eloquent. Jadi Anda benar-benar tidak dapat menggunakan trik ini ketika menggabungkan 2 konten koleksi yang berbeda seperti Postsdan Users, karena pengguna dengan id 3dan posting dengan id 3akan mengalami konflik dalam kasus ini, dan hanya yang terakhir (Posting) yang akan ditemukan dalam koleksi gabungan.


Jika Anda menginginkannya menjadi lebih cepat, Anda harus membuat kueri khusus menggunakan fasad DB, sesuatu di sepanjang baris:

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

Permintaan Anda yang sebenarnya jauh lebih besar karena Anda memiliki begitu banyak hubungan.

Peningkatan kinerja utama di sini berasal dari fakta bahwa pekerjaan berat (subquery) sepenuhnya memotong logika model Eloquent. Maka yang tersisa untuk dilakukan adalah memasukkan daftar id ke dalam whereInfungsi untuk mengambil daftar Formobjek Anda.

Api
sumber
0

Saya percaya Anda dapat menggunakan Koleksi Malas untuk itu (Laravel 6.x) dan ingin memuat hubungan sebelum mereka diakses.

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
sumber