Laravelで権限をフィルタリングする際のパフォーマンスのための最良のアプローチ


9

ユーザーがさまざまなシナリオで多くのフォームにアクセスできるアプリケーションに取り組んでいます。ユーザーにフォームのインデックスを返すときに、最高のパフォーマンスでアプローチを構築しようとしています。

ユーザーは次のシナリオでフォームにアクセスできます。

  • 所有フォーム
  • チームはフォームを所有しています
  • フォームを所有するグループへのアクセス許可を持っている
  • フォームを所有するチームへのアクセス許可を持っている
  • フォームへのアクセス許可があります

ご覧のとおり、ユーザーがフォームにアクセスする方法は5つあります。私の問題は、ユーザーにアクセス可能なフォームの配列を最も効率的に返す方法です。

フォームポリシー:

モデルからすべてのフォームを取得し、フォームポリシーでフォームをフィルターしようとしました。以下に示すように、各フィルター反復でフォームがcontains()eloquentメソッドを5回通過するため、これはパフォーマンスの問題のようです。データベース内のフォームが多いほど、これは遅くなります。

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

上記の方法は機能しますが、パフォーマンスのボトルネックです。

私が見ることができるものから、以下のオプションがあります:

  • FormPolicyフィルター(現在のアプローチ)
  • すべての権限をクエリし(5)、単一のコレクションにマージする
  • すべてのアクセス許可のすべての識別子をクエリし(5)、IN()ステートメントの識別子を使用してフォームモデルをクエリします

私の質問:

どの方法が最高のパフォーマンスを提供しますか、そしてより良いパフォーマンスを提供する他のオプションはありますか?


ユーザーがフォームにアクセスできる場合は、リンクの多対多のアプローチを作成することもできます
お金のためのコード

ユーザーフォームの権限を照会するためのテーブルを作成するのはどうですか?user_form_permissionただ含むテーブルuser_idform_id。これにより、読み取り権限が簡単になりますが、更新権限は難しくなります。
PtrTon

user_form_permissionsテーブルの問題は、権限を他のエンティティに拡張したいため、エンティティごとに個別のテーブルが必要になることです。
Tim

1
@Timですが、それでも5つのクエリです。これが保護されたメンバーのエリア内にある場合、問題にはならない可能性があります。しかし、これが1秒間に多くのリクエストを受け取る可能性がある公開URLである場合、これを少し最適化したいと思います。パフォーマンス上の理由から、フォームオブザーバーまたはチームメンバーがモデルオブザーバーを介して追加または削除されるたびに、(キャッシュできる)別のテーブルを維持します。その後、すべてのリクエストで、キャッシュから取得します。この質問と問題は非常に興味深いものであり、他の人の意見も知りたいです。この質問はより多くの投票と回答に値し、賞金を始めました:)
Raul

1
スケジュールされたジョブとして更新できるマテリアライズドビューを検討することができます。このようにして、常に比較的最新の結果をすばやく得ることができます。
apokryfos

回答:


2

SQLクエリを実行すると、phpよりもパフォーマンスが向上するため、

このようなもの:

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

私の頭のてっぺんから、これはテストされていません。これにより、ユーザー、彼のグループ、およびこのチームが所有するすべてのフォームが表示されます。

ただし、グループおよびチームのユーザービューフォームの権限は確認しません。

このために認証をどのように設定しているかはわかりません。そのため、クエリとDB構造の違いを変更する必要があります。


答えてくれてありがとう。ただし、問題はデータベースからデータを取得する方法に関するクエリではありませんでした。問題は、アプリに数十万のフォームと多数のチームとメンバーがいる場合に、毎回、すべてのリクエストでそれを効率的に取得する方法です。あなたの結合にはOR節があるので、遅くなると思う。だから、すべてのリクエストでこれを打つことは私は信じられないほど狂気になるでしょう。
Raul、

生のMySQLクエリを使用したり、ビューやプロシージャなどを使用したりすると、速度が向上する可能性がありますが、データが必要になるたびにこのような呼び出しを行う必要があります。結果のキャッシュもここで役立ちます。
Josh、

このパフォーマンスを実現する唯一の方法はキャッシングであると考えていますが、変更を行うたびにこのマップを常に維持するという犠牲を払っています。新しいフォームを作成するとします。チームが自分のアカウントに割り当てられている場合、何千人ものユーザーがフォームにアクセスできる可能性があります。次は何ですか?数千のメンバーポリシーを再キャッシュしますか?
Raul、

有効期限付きのキャッシュソリューション(laravelのキャッシュ抽象化など)があり、変更を行った直後に影響を受けるキャッシュインデックスを削除することもできます。正しく使用すれば、キャッシュは実際のゲームチェンジャーです。キャッシュの構成方法は、データの読み取りと更新によって異なります。
ゴンザロ

2

簡潔な答え

3番目のオプション: 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();

長い答え

一方では、(ほとんど)コードで実行できることはすべて、クエリで実行するよりもパフォーマンスの点で優れています。

一方、データベースから必要以上のデータを取得すると、すでにデータが多すぎます(RAM使用率など)。

私の観点からは、あなたはその間に何かが必要であり、あなただけが、数に応じて、どこがバランスになるかを知っています。

いくつかのクエリを実行することをお勧めします。最後に提案したオプション(Query all identifiers for all permissions (5), then query the Form model using the identifiers in an IN() statement):

  1. すべての権限について、すべての識別子をクエリします(5クエリ)
  2. すべてのフォーム結果をメモリにマージし、一意の値を取得します array_unique($ids)
  3. IN()ステートメントで識別子を使用して、フォームモデルをクエリします。

いくつかのツールを使用してクエリを複数回実行して、提案した3つのオプションを試してパフォーマンスを監視できますが、最後のオプションが最高のパフォーマンスを発揮すると確信しています。

これは、使用しているデータベースに応じて大幅に変わる可能性がありますが、たとえば、MySQLについて話している場合は、非常に大きなクエリでは、より多くのデータベースリソースを使用するため、単純なクエリよりも時間がかかるだけでなく、テーブルが書き込みからロックされ、これによりデッドロックエラーが発生する可能性があります(スレーブサーバーを使用しない場合)。

一方、フォームIDの数が非常に多い場合は、プレースホルダーが多すぎるとエラーが発生する可能性があるため、たとえば500個のIDのグループでクエリをチャンクにしたい場合があります(これは制限として多くの場合に依存します)サイズであり、バインディングの数ではありません)、結果をメモリにマージします。データベースエラーが発生しなくても、パフォーマンスにも大きな違いが見られる場合があります(私はまだMySQLについて話しています)。


実装

これがデータベーススキームであると想定します。

users
  - id
  - team_id

forms
  - id
  - user_id
  - team_id
  - group_id

permissible
  - user_id
  - permissible_id
  - permissible_type

したがって、許容されるのは、すでに構成された多態的な関係です。

したがって、関係は次のようになります。

  • 所有フォーム: users.id <-> form.user_id
  • チームはフォームを所有しています: users.team_id <-> form.team_id
  • フォームを所有するグループへのアクセス許可があります: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Team'
  • フォームを所有するチームへの権限を持っています: permissible.user_id <-> users.id && permissible.permissible_type = 'App\Group'
  • フォームへのアクセス権があります: permissible.user_id <-> users.id && permissible.permissible_type = 'App\From'

バージョンを簡略化:

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

詳細バージョン:

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

使用されるリソース:

データベースのパフォーマンス:

  • データベースへのクエリ(ユーザーを除く):2 ; 1つは許可を取得するためのもので、もう1つはフォームを取得するためのものです。
  • 参加しません!!
  • 可能な最小ORs(user_id = ? OR id IN (?..) OR team_id IN (?...) OR group_id IN (?...)

PHP、メモリ内、パフォーマンス:

  • 内部のスイッチでforeachループが許容されます。
  • array_values(array_unique()) IDの繰り返しを避けるため。
  • IDSのメモリにおいて、3つのアレイ($teamIds$groupIds$formIds
  • メモリ内で、関連するアクセス許可の雄弁なコレクション(これは、必要に応じて最適化できます)。

長所と短所

長所:

  • 時間:単一のクエリの合計時間は、結合とORを使用した大きなクエリの時間よりも短くなります。
  • DBリソース:結合やステートメントを含むクエリで使用されるMySQLリソースは、個別のクエリの合計で使用されるものよりも大きくなります。
  • お金:データベースリソース(プロセッサ、RAM、ディスク読み取りなど)が少なく、PHPリソースよりも高価です。
  • ロック:読み取り専用のスレーブサーバーにクエリを実行しない場合、クエリは読み取りロックの行数を減らします(読み取りロックはMySQLで共有されるため、別の読み取りはロックされませんが、書き込みはブロックされます)。
  • スケーラブル:このアプローチにより、クエリのチャンクなど、より多くのパフォーマンス最適化を行うことができます。

短所:

  • コードリソース:データベースではなくコードで計算を行うと、コードインスタンス、特にRAMで中間の情報を保存するためにより多くのリソースが消費されます。私たちの場合、これは単なるidの配列ですが、これは実際には問題になりません。
  • メンテナンス:Laravelのプロパティとメソッドを使用し、データベースに変更を加える場合、より明示的なクエリと処理を行うよりもコードで更新する方が簡単です。
  • 殺し過ぎ?:場合によっては、データがそれほど大きくない場合、パフォーマンスの最適化が過剰になる可能性があります。

パフォーマンスを測定する方法

パフォーマンスを測定する方法についての手がかりはありますか?

  1. クエリログが遅い
  2. 分析テーブル
  3. 表の状態を表示
  4. 説明する ; 拡張EXPLAIN出力フォーマット ; 説明を使用して ; 出力を説明する
  5. 警告を表示

いくつかの興味深いプロファイリングツール:


その最初の行は何ですか?PHPでさまざまなループや配列操作を実行すると速度が低下するため、ほとんどの場合、クエリを使用するとパフォーマンスが向上します。
フレーム

データベースが小さい場合、またはデータベースマシンがコードインスタンスよりも強力である場合、またはデータベースのレイテンシが非常に悪い場合、はい、MySQLはより高速ですが、通常はそうではありません。
ゴンザロ

データベースクエリを最適化するときは、実行時間、返される行数、最も重要なことには、検査される行数を考慮する必要があります。クエリが遅くなっているとTimが言っている場合は、データが増加しているため、検査された行数が増加していると思います。さらに、プログラミング言語のようにデータベースは処理用に最適化されていません。
ゴンザロ

しかし、あなたは私を信頼する必要はありません、あなたが実行できるEXPLAIN、あなたの解決のために、あなたは単純なクエリの私の解決策のためにそれを実行し、その差を参照して、簡単な場合と考えることができますarray_merge()し、array_unique()IDSの束の、でしょうあなたのプロセスを本当に遅くします。
ゴンザロ

10ケースのうち9ケースでは、mysqlデータベースはコードを実行するのと同じマシンで実行されます。データ層は、データの取得に使用するためのものであり、大きなセットからデータの一部を選択するために最適化されています。array_unique()aがGROUP BY/ SELECT DISTINCTステートメントよりも速い状況はまだ見ていません。
フレーム

0

関数を実行Form::all()してから連鎖させるのではなく、必要なフォームを単純にクエリできないのはなぜfilter()ですか?

そのようです:

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

つまり、これはいくつかのクエリを実行します。

  • のクエリ $user
  • 一つのために $user->team
  • 一つのために $user->team->forms
  • 一つのために $user->permissible
  • 一つのために $user->permissible->groups
  • 一つのために $user->permissible->groups->forms

ただし、パラメーターのすべてのフォームがユーザーに許可されていることがわかっているため、ポリシーを使用する必要がなくなるというのが良い面です$forms

したがって、このソリューションは、データベースにあるフォームの量に関係なく機能します。

使用上の注意 merge()

merge()コレクションをマージし、すでに見つかった重複するフォームIDを破棄します。したがって、何らかの理由でteam関係のフォーム がとの直接の関係でもあるuser場合、マージされたコレクションに表示されるのは1回だけです。

これは、実際 にはEloquentモデルIDをチェックするIlluminate\Database\Eloquent\Collection独自のmerge()関数があるためです。したがって、Postsandのような2つの異なるコレクションコンテンツをマージするときに、このトリックを実際に使用することはできません。この場合、id を持つUsersユーザーとid 3を持つ投稿3は競合し、マージされたコレクションでは後者(投稿)のみが見つかるからです。


さらに高速にしたい場合は、DBファサードを使用してカスタムクエリを作成する必要があります。

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

関係が非常に多いため、実際のクエリははるかに大きくなります。

ここでの主なパフォーマンスの向上は、重い作業(サブクエリ)がEloquentモデルロジックを完全にバイパスするという事実から来ています。その後、あとは、IDのリストをwhereIn関数に渡してFormオブジェクトのリストを取得するだけです。


0

私はあなたがそのためにレイジーコレクション(Laravel 6.x)を使用でき、それらがアクセスされる前に関係を熱心にロードできると信じています。

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);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.