在Laravel中过滤权限时的最佳性能方法


9

我正在开发一个应用程序,在该应用程序中,用户可以通过许多不同的场景访问许多表单。当我向用户返回表单索引时,我试图构建具有最佳性能的方法。

用户可以通过以下方案访问表单:

  • 拥有表格
  • 团队拥有表格
  • 拥有拥有表单的组的权限
  • 拥有拥有表单的团队的权限
  • 有权使用表格

如您所见,用户可以使用5种可能的方式来访问表单。我的问题是如何最有效地将一系列可访问的表单返回给用户。

表格政策:

我试图从模型中获取所有表单,然后通过表单策略过滤表单。这似乎是一个性能问题,因为在每次过滤器迭代中,表单都会通过一个contains()雄辩的方法传递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()语句中的标识符查询Form模型

我的问题:

哪种方法可以提供最佳性能,还有其他选择可以提供更好的性能吗?


如果用户可以访问表格,您也可以采用“ 多对多”方法进行链接
钱的代码

如何创建专门用于查询用户表单权限的表?在user_form_permission只包含表user_idform_id。这将使阅读权限变得轻而易举,但是更新权限将更加困难。
PtrTon

user_form_permissions表的问题在于我们希望将权限扩展到其他实体,然后每个实体都需要一个单独的表。
蒂姆(Tim)

1
@Tim,但仍然是5个查询。如果这只是在受保护成员的区域内,则可能不是问题。但是,如果这是一个面向公众的URL,该URL每秒可以收到很多请求,那么我想您会对此进行一些优化。出于性能原因,每次通过模型观察者添加或删除表单或团队成员时,我都会维护一个单独的表(可以缓存)。然后,根据每个请求,我将从缓存中获取该请求。我发现这个问题非常有趣,很想知道其他人的想法。这个问题值得更多投票和答案,开始了悬赏计划:)
Raul

1
您可以考虑拥有一个物化视图,可以将其作为计划作业刷新。这样,您始终可以快速获得相对最新的结果。
apokryfos

Answers:


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

从我的头顶开始,未经测试,这应该为您提供用户,他的团队和这个团队拥有的所有表格。

但是,它不会查看组和团队中用户视图表单的权限。

我不确定如何为此设置身份验证,因此您需要为此和数据库结构中的任何差异修改查询。


感谢您的回答。但是,问题不在于如何从数据库中获取数据的查询。问题是,当应用程序具有成千上万的表单以及许多团队和成员时,如何在每次请求时都有效地获取它。您的联接中有一些OR子句,我怀疑这些子句会很慢。因此,我相信,在每个请求上都遇到这个问题将是疯狂的。
拉乌尔

使用原始MySQL查询或使用诸如视图或过程之类的方法,您也许可以提高速度,但是每次需要数据时,您都必须进行类似的调用。缓存结果在这里也可能有所帮助。
乔什

虽然我在考虑使这种性能的唯一方法是缓存,但这是以每次进行更改时始终维护此映射为代价的。想象一下,我创建了一个新表单,如果将一个团队分配给我的帐户,则意味着成千上万的用户可以访问该表单。下一步是什么?重新缓存几千个会员政策?
拉乌尔

有些缓存解决方案具有生命周期(例如laravel的缓存抽象),并且您还可以在进行任何更改后立即删除受影响的缓存索引。如果正确使用缓存,它将真正改变游戏规则。如何配置缓存取决于数据的读取和更新。
贡萨洛

2

简短答案

第三种选择: 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()语句中的标识符查询Form模型。

您可以尝试使用建议的三个选项并使用某种工具多次运行查询来监视性能,但我99%确信最后一个选项将为您提供最佳性能。

这可能也会发生很大变化,具体取决于您使用的是哪个数据库,但是例如,如果我们谈论的是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 ; 一个获得许可,另一个获得表格。
  • 没有加入!
  • 可能的最小OR(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的大型查询的时间。
  • 数据库资源:带有join和or语句的查询所使用的MySQL资源大于其单独查询的总和所使用的MySQL资源。
  • 资金:更少的数据库资源(处理器,RAM,磁盘读取等),比PHP资源昂贵。
  • :如果您不查询只读从属服务器,则查询将使更少的行具有读取锁(该读取锁在MySQL中是共享的,因此它不会锁定其他读取,但会阻止任何写入)。
  • 可扩展:此方法使您可以进行更多性能优化,例如对查询进行分块。

缺点:

  • 代码资源:用代码而不是数据库进行计算显然会在代码实例中消耗更多的资源,但是特别是在RAM中,存储中间信息。在我们的例子中,这只是一个id数组,这实际上不是问题。
  • 维护:如果您使用Laravel的属性和方法,并且对数据库进行了任何更改,则与进行更明确的查询和处理相比,更新代码将更加容易。
  • 杀人过度?:在某些情况下,如果数据不是那么大,那么优化性能可能会导致过大的损失。

如何衡量绩效

有关如何衡量性能的一些线索?

  1. 慢查询日志
  2. 分析表
  3. 显示表格状态
  4. 说明 ; 扩展的EXPLAIN输出格式 ; 使用说明 ; 解释输出
  5. 显示警告

一些有趣的分析工具:


那第一行是什么?使用查询几乎总是在性能上更好,因为在PHP中运行各种循环或数组操作速度较慢。
火焰

如果您的数据库较小,或者数据库计算机比代码实例更强大,或者数据库延迟非常严重,那么可以,MySQL更快,但是通常不是这种情况。
贡萨洛

优化数据库查询时,需要考虑执行时间,返回的行数,最重要的是,要检查的行数。如果蒂姆说查询正在变慢,那么我认为数据在增长,因此检查的行数也在增加。此外,数据库没有像编程语言那样进行优化处理。
贡萨洛

但是您不需要信任我,可以为您的解决方案运行EXPLAIN,然后可以为我的简单查询的解决方案运行它,看看有什么区别,然后考虑是否有一个简单的ID array_merge()array_unique()一堆ID确实减慢了您的进度。
贡萨洛

在十分之九的情况下,mysql数据库在运行代码的同一台计算机上运行。数据层旨在用于数据检索,并且已针对从大集合中选择数据进行了优化。我还没有看到a array_unique()快于GROUP BY/ SELECT DISTINCT语句的情况。
Flame

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,则它将仅在合并集合中显示一次。

这是因为它实际上是一个 Illuminate\Database\Eloquent\Collection具有自己merge()功能的工具,用于检查Eloquent模型ID。因此,在合并2个不同的集合内容(例如Posts和)时Users,您实际上不能使用此技巧,因为在这种情况下,3具有id 的用户和具有id 的帖子3将发生冲突,并且在合并的集合中只会找到后者(Post)。


如果您希望它更快,则应该使用DB Facade创建一个自定义查询,大致如下:

// 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

我相信您可以为此使用Lazy Collections(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);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.