Cách tiếp cận tốt nhất cho hiệu suất khi lọc các quyền trong Laravel


9

Tôi đang làm việc trên một ứng dụng mà người dùng có thể có quyền truy cập vào nhiều biểu mẫu thông qua nhiều tình huống khác nhau. Tôi đang cố gắng xây dựng cách tiếp cận với hiệu suất tốt nhất khi trả về một chỉ mục biểu mẫu cho người dùng.

Người dùng có thể có quyền truy cập vào các biểu mẫu thông qua các tình huống sau:

  • Sở hữu mẫu
  • Đội sở hữu mẫu
  • Có quyền đối với một nhóm sở hữu Biểu mẫu
  • Có quyền đối với một nhóm sở hữu Biểu mẫu
  • Có quyền đối với Mẫu

Như bạn có thể thấy có 5 cách có thể mà người dùng có thể truy cập vào một biểu mẫu. Vấn đề của tôi là làm thế nào để tôi trả lại một cách hiệu quả nhất một mảng các biểu mẫu có thể truy cập được cho người dùng.

Chính sách mẫu:

Tôi đã cố gắng lấy tất cả các Biểu mẫu từ mô hình và sau đó lọc các biểu mẫu theo chính sách biểu mẫu. Đây dường như là một vấn đề về hiệu năng vì trên mỗi lần lặp bộ lọc, biểu mẫu được truyền qua một phương thức hùng biện chứa () 5 lần như dưới đây. Càng nhiều hình thức trong cơ sở dữ liệu có nghĩa là điều này trở nên chậm hơn.

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

Mặc dù phương pháp trên hoạt động nó là một cổ chai hiệu suất.

Từ những gì tôi có thể thấy các tùy chọn sau đây của tôi là:

  • Bộ lọc FormPolicy (cách tiếp cận hiện tại)
  • Truy vấn tất cả các quyền (5) và hợp nhất thành một bộ sưu tập
  • Truy vấn tất cả các mã định danh cho tất cả các quyền (5), sau đó truy vấn mô hình Biểu mẫu bằng cách sử dụng các mã định danh trong câu lệnh IN ()

Câu hỏi của tôi:

Phương pháp nào sẽ cung cấp hiệu suất tốt nhất và có lựa chọn nào khác cung cấp hiệu suất tốt hơn không?


bạn cũng có thể thực hiện một cách tiếp cận Nhiều đến Nhiều để liên kết nếu người dùng có thể truy cập vào biểu mẫu
mã để kiếm tiền vào

Điều gì về việc tạo một bảng đặc biệt để truy vấn quyền của biểu mẫu người dùng? Các user_form_permissionbảng chứa chỉ user_idform_id. Điều này sẽ làm cho việc đọc quyền trở nên dễ dàng, tuy nhiên việc cập nhật quyền sẽ khó hơn.
PtrTon

Vấn đề với bảng user_form_permissions là chúng tôi muốn mở rộng quyền cho các thực thể khác mà sau đó sẽ yêu cầu một bảng riêng cho mỗi thực thể.
Tim

1
@Tim nhưng đó vẫn là 5 truy vấn. Nếu đây chỉ là trong khu vực của một thành viên được bảo vệ, có thể không phải là một vấn đề. Nhưng nếu đây là một URL phải đối mặt công khai có thể nhận được rất nhiều yêu cầu mỗi giây, tôi sẽ bạn muốn tối ưu hóa điều này một chút. Vì lý do hiệu suất, tôi sẽ duy trì một bảng riêng (mà tôi có thể lưu trữ bộ đệm) mỗi khi một biểu mẫu hoặc thành viên nhóm được thêm hoặc xóa thông qua các trình quan sát mô hình. Sau đó, trên mỗi yêu cầu, tôi sẽ nhận được điều đó từ bộ đệm. Tôi thấy câu hỏi và vấn đề này rất thú vị và rất thích biết người khác nghĩ gì. Câu hỏi này xứng đáng được nhiều phiếu và câu trả lời hơn, bắt đầu một tiền thưởng :)
Raul

1
Bạn có thể xem xét có một cái nhìn cụ thể hóa mà bạn có thể làm mới như một công việc theo lịch trình. Bằng cách này, bạn luôn có thể có kết quả tương đối cập nhật nhanh chóng.
apokryfos

Câu trả lời:


2

Tôi sẽ thực hiện một Truy vấn SQL vì nó sẽ hoạt động tốt hơn nhiều so với php

Một cái gì đó như thế này:

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

Từ đỉnh đầu của tôi và chưa được kiểm tra, điều này sẽ giúp bạn có được tất cả các biểu mẫu thuộc sở hữu của người dùng, nhóm của anh ấy và nhóm này.

Tuy nhiên, nó không nhìn vào các quyền của biểu mẫu xem người dùng trong các nhóm và nhóm.

Tôi không chắc chắn làm thế nào bạn đã thiết lập auth của mình cho việc này và vì vậy bạn sẽ cần sửa đổi truy vấn cho điều này và bất kỳ sự khác biệt nào trong cấu trúc DB của bạn.


cảm ơn câu trả lời Tuy nhiên, vấn đề không phải là truy vấn về cách lấy dữ liệu từ cơ sở dữ liệu. Vấn đề là, làm thế nào để có được nó hiệu quả mọi lúc, trên mọi yêu cầu, khi ứng dụng có hàng trăm ngàn biểu mẫu và rất nhiều nhóm và thành viên. Tham gia của bạn có ORmệnh đề, mà tôi nghi ngờ sẽ có chậm. Vì vậy, nhấn vào điều này trên mọi yêu cầu sẽ là điên rồ tôi tin.
Raul

Bạn có thể có được tốc độ tốt hơn với truy vấn MySQL thô hoặc sử dụng một số thứ như chế độ xem hoặc quy trình nhưng bạn sẽ phải thực hiện các cuộc gọi như thế này mỗi lần bạn muốn có dữ liệu. Bộ nhớ đệm kết quả cũng có thể giúp đỡ ở đây.
Josh

Trong khi tôi nghĩ cách duy nhất để làm cho người biểu diễn này là bộ nhớ đệm, thì điều đó phải trả giá bằng việc luôn duy trì bản đồ này mỗi khi có thay đổi. Hãy tưởng tượng tôi tạo một biểu mẫu mới, nếu một nhóm được gán cho tài khoản của tôi nghĩa là hàng ngàn người dùng có thể có quyền truy cập vào nó. Cái gì tiếp theo? Chính sách lưu trữ lại vài nghìn thành viên?
Raul

Có các giải pháp bộ đệm với thời gian tồn tại (như trừu tượng bộ đệm của laravel) và bạn cũng có thể xóa các chỉ mục bộ đệm bị ảnh hưởng ngay sau khi bạn thực hiện bất kỳ thay đổi nào. Bộ đệm là một công cụ thay đổi trò chơi thực sự nếu bạn sử dụng đúng cách. Cách cấu hình bộ đệm phụ thuộc vào việc đọc và cập nhật dữ liệu.
Gonzalo

2

Câu trả lời ngắn

Tùy chọn thứ ba: 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();

Câu trả lời dài

Một mặt, (hầu hết) mọi thứ bạn có thể làm trong mã, là hiệu năng tốt hơn so với thực hiện trong các truy vấn.

Mặt khác, nhận được nhiều dữ liệu từ cơ sở dữ liệu hơn mức cần thiết sẽ là quá nhiều dữ liệu (sử dụng RAM, v.v.).

Từ quan điểm của tôi, bạn cần một cái gì đó ở giữa, và chỉ có bạn mới biết đâu sẽ là số dư, tùy thuộc vào các con số.

Tôi sẽ đề nghị chạy một số truy vấn, tùy chọn cuối cùng mà bạn đề xuất ( Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. Truy vấn tất cả các định danh, cho tất cả các quyền (5 truy vấn)
  2. Hợp nhất tất cả các kết quả biểu mẫu trong bộ nhớ và nhận các giá trị duy nhất array_unique($ids)
  3. Truy vấn mô hình biểu mẫu, sử dụng các định danh trong câu lệnh IN ().

Bạn có thể thử ba tùy chọn bạn đề xuất và theo dõi hiệu suất, sử dụng một số công cụ để chạy truy vấn nhiều lần, nhưng tôi chắc chắn 99% rằng lựa chọn cuối cùng sẽ mang lại cho bạn hiệu suất tốt nhất.

Điều này cũng có thể thay đổi rất nhiều, tùy thuộc vào cơ sở dữ liệu nào bạn đang sử dụng, nhưng nếu chúng ta đang nói về MySQL chẳng hạn; Trong một truy vấn rất lớn sẽ sử dụng nhiều tài nguyên cơ sở dữ liệu hơn, nó sẽ không chỉ tiêu tốn nhiều thời gian hơn các truy vấn đơn giản mà còn khóa bảng khỏi ghi và điều này có thể tạo ra các lỗi bế tắc (trừ khi bạn sử dụng máy chủ nô lệ).

Mặt khác, nếu số lượng biểu mẫu id rất lớn, bạn có thể có lỗi cho quá nhiều trình giữ chỗ, vì vậy bạn có thể muốn phân chia các truy vấn theo nhóm, giả sử, 500 id (điều này phụ thuộc rất nhiều, vì giới hạn có kích thước, không phải về số lượng ràng buộc) và hợp nhất các kết quả trong bộ nhớ. Ngay cả khi bạn không gặp lỗi cơ sở dữ liệu, bạn cũng có thể thấy một sự khác biệt lớn về hiệu suất (tôi vẫn đang nói về MySQL).


Thực hiện

Tôi sẽ giả định rằng đây là lược đồ cơ sở dữ liệu:

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

Vì vậy, cho phép sẽ là một mối quan hệ đa hình đã được cấu hình .

Do đó, các mối quan hệ sẽ là:

  • Hình thức sở hữu: users.id <-> form.user_id
  • Đội sở hữu Mẫu: users.team_id <-> form.team_id
  • Có quyền đối với một nhóm sở hữu Biểu mẫu: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • Có quyền đối với một nhóm sở hữu Biểu mẫu: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • Có quyền đối với Biểu mẫu: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

Đơn giản hóa phiên bản:

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

Phiên bản chi tiết:

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

Tài nguyên được sử dụng:

Hiệu suất cơ sở dữ liệu:

  • Truy vấn cơ sở dữ liệu (không bao gồm người dùng): 2 ; một để có được sự cho phép và một cái khác để có được các hình thức.
  • Không tham gia !!
  • Các OR tối thiểu có thể ( user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...).

PHP, trong bộ nhớ, hiệu suất:

  • foreach lặp vòng cho phép với một công tắc bên trong.
  • array_values(array_unique()) để tránh lặp lại ids.
  • Trong bộ nhớ, 3 mảng của id ( $teamIds, $groupIds,$formIds )
  • Trong bộ nhớ, bộ sưu tập quyền hùng biện có liên quan (điều này có thể được tối ưu hóa, nếu cần).

Ưu và nhược điểm

PROS:

  • Thời gian : Tổng số lần của các truy vấn đơn nhỏ hơn thời gian của một truy vấn lớn với các phép nối và OR.
  • Tài nguyên DB : Tài nguyên MySQL được sử dụng bởi một truy vấn có liên kết và hoặc câu lệnh, lớn hơn tài nguyên được sử dụng bởi tổng các truy vấn riêng biệt của nó.
  • Tiền : Ít tài nguyên cơ sở dữ liệu (bộ xử lý, RAM, đọc đĩa, v.v.), đắt hơn tài nguyên PHP.
  • Khóa : Trong trường hợp bạn không truy vấn máy chủ nô lệ chỉ đọc, các truy vấn của bạn sẽ tạo ra ít hàng đọc khóa hơn (khóa đọc được chia sẻ trong MySQL, vì vậy nó sẽ không khóa đọc khác, nhưng nó sẽ chặn bất kỳ ghi nào).
  • Khả năng mở rộng : Cách tiếp cận này cho phép bạn thực hiện nhiều tối ưu hóa hiệu suất hơn, chẳng hạn như truy vấn các truy vấn.

Nhược điểm:

  • Tài nguyên mã : Thực hiện tính toán trong mã, thay vì trong cơ sở dữ liệu, rõ ràng sẽ tiêu thụ nhiều tài nguyên hơn trong trường hợp mã, nhưng đặc biệt là trong RAM, lưu trữ thông tin ở giữa. Trong trường hợp của chúng tôi, đây sẽ chỉ là một mảng id, không thực sự là một vấn đề.
  • Bảo trì : Nếu bạn sử dụng các thuộc tính và phương thức của Laravel và bạn thực hiện bất kỳ thay đổi nào trong cơ sở dữ liệu, việc cập nhật mã sẽ dễ dàng hơn so với khi bạn thực hiện nhiều truy vấn và xử lý rõ ràng hơn.
  • Quá mức cần thiết? : Trong một số trường hợp, nếu dữ liệu không lớn, việc tối ưu hóa hiệu suất có thể là quá mức cần thiết.

Làm thế nào để đo lường hiệu suất

Một số manh mối về cách đo hiệu suất?

  1. Nhật ký truy vấn chậm
  2. BẢNG PHÂN TÍCH
  3. HIỂN THỊ BẢNG TÌNH TRẠNG THÍCH
  4. GIẢI THÍCH ; Định dạng đầu ra mở rộng EXPLAIN ; sử dụng giải thích ; giải thích đầu ra
  5. CẢNH BÁO

Một số công cụ định hình thú vị:


Dòng đầu tiên đó là gì? Hầu như luôn luôn sử dụng một truy vấn tốt hơn, vì việc chạy các vòng lặp hoặc thao tác mảng khác nhau trong PHP chậm hơn.
Ngọn lửa

Nếu bạn có một cơ sở dữ liệu nhỏ hoặc máy cơ sở dữ liệu của bạn mạnh hơn thể hiện mã của bạn hoặc độ trễ của cơ sở dữ liệu rất tệ, thì có, MySQL nhanh hơn, nhưng điều này thường không xảy ra.
Gonzalo

Khi bạn tối ưu hóa một truy vấn cơ sở dữ liệu, bạn cần xem xét thời gian thực hiện, số lượng hàng được trả về và quan trọng nhất là số lượng hàng được kiểm tra. Nếu Tim đang nói rằng các truy vấn đang trở nên chậm, thì tôi cho rằng dữ liệu đang tăng lên và do đó số lượng hàng được kiểm tra. Ngoài ra, cơ sở dữ liệu không được tối ưu hóa để xử lý như ngôn ngữ lập trình.
Gonzalo

Nhưng bạn không cần phải tin tưởng tôi, bạn có thể chạy GIẢI THÍCH , cho giải pháp của mình, sau đó bạn có thể chạy nó cho giải pháp truy vấn đơn giản của tôi và xem sự khác biệt, sau đó suy nghĩ nếu một id đơn giản array_merge()array_unique()một loạt các id, sẽ thực sự làm chậm quá trình của bạn
Gonzalo

Trong 9 trên 10 trường hợp, cơ sở dữ liệu mysql chạy trên cùng một máy chạy mã. Lớp dữ liệu có nghĩa là được sử dụng để truy xuất dữ liệu và nó được tối ưu hóa để chọn các phần dữ liệu từ các bộ lớn. Tôi vẫn chưa thấy tình huống nào array_unique()nhanh hơn câu lệnh GROUP BY/ SELECT DISTINCT.
Ngọn lửa

0

Tại sao bạn không thể truy vấn các Biểu mẫu bạn cần, thay vì thực hiện Form::all()và sau đó xâu chuỗifilter() hàm sau nó?

Thích như vậy:

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

Vì vậy, có, đây là một vài truy vấn:

  • Một truy vấn cho $user
  • Một cho $user->team
  • Một cho $user->team->forms
  • Một cho $user->permissible
  • Một cho $user->permissible->groups
  • Một cho $user->permissible->groups->forms

Tuy nhiên, khía cạnh chuyên nghiệp là bạn không còn cần phải sử dụng chính sách này nữa , vì bạn biết tất cả các biểu mẫu trong $formstham số đều được phép cho người dùng.

Vì vậy, giải pháp này sẽ hoạt động cho bất kỳ số lượng biểu mẫu bạn có trong cơ sở dữ liệu.

Một lưu ý khi sử dụng merge()

merge()hợp nhất các bộ sưu tập và sẽ loại bỏ các id mẫu trùng lặp mà nó đã tìm thấy. Vì vậy, nếu vì lý do nào đó, một hình thức từ teammối quan hệ cũng là một mối quan hệ trực tiếp đếnuser nó, nó sẽ chỉ hiển thị một lần trong bộ sưu tập được hợp nhất.

Điều này là do nó thực sự là một hàm có chức năng Illuminate\Database\Eloquent\Collectionriêng merge()kiểm tra các id mô hình Eloquent. Vì vậy, bạn không thể thực sự sử dụng thủ thuật này khi hợp nhất 2 nội dung bộ sưu tập khác nhau như PostsUsers, bởi vì người dùng có id 3và bài đăng với id 3sẽ xung đột trong trường hợp này và chỉ có phần sau (Bài đăng) sẽ được tìm thấy trong bộ sưu tập được hợp nhất.


Nếu bạn muốn nó nhanh hơn nữa, bạn nên tạo một truy vấn tùy chỉnh bằng cách sử dụng mặt tiền DB, một cái gì đó dọc theo dòng:

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

Truy vấn thực tế của bạn lớn hơn nhiều vì bạn có rất nhiều mối quan hệ.

Sự cải thiện hiệu suất chính ở đây xuất phát từ thực tế là công việc nặng (truy vấn phụ) hoàn toàn bỏ qua logic mô hình Eloquent. Sau đó, tất cả những gì còn lại phải làm là chuyển danh sách id vào whereInhàm để lấy danh sách các Formđối tượng của bạn .


0

Tôi tin rằng bạn có thể sử dụng Bộ sưu tập Lười cho điều đó (Laravel 6.x) và háo hức tải các mối quan hệ trước khi chúng được truy cập.

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);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.