【问题标题】:Best approach for performance when filtering for permissions in Laravel在 Laravel 中过滤权限时的最佳性能方法
【发布时间】:2019-11-02 23:06:12
【问题描述】:

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

用户可以通过以下场景访问表单:

  • 拥有表单
  • 团队拥有表单
  • 拥有对拥有表单的组的权限
  • 拥有拥有表单的团队的权限
  • 拥有表单权限

如您所见,用户可以通过 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() 语句中的标识符查询表单模型

我的问题:

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

【问题讨论】:

  • 如果用户可以访问表单,您也可以使用Many To Many 方法进行链接
  • 创建一个专门用于查询用户表单权限的表呢? user_form_permission 表仅包含 user_idform_id。这将使读取权限变得轻而易举,但更新权限将更加困难。
  • user_form_permissions 表的问题是我们希望将权限扩展到其他实体,这需要为每个实体创建一个单独的表。
  • @Tim 但这仍然是 5 个查询。如果这只是在受保护成员的区域内,则可能不是问题。但是,如果这是在每秒可以收到大量请求的公共 URL 上,我认为您可能需要对其进行一些优化。出于性能原因,每次通过模型观察者添加或删除表单或团队成员时,我都会维护一个单独的表(我可以缓存)。然后,对于每个请求,我都会从缓存中获取。我发现这个问题和问题非常有趣,也很想知道其他人的想法。这个问题值得更多的投票和答案,开始赏金:)
  • 您可以考虑将materialised view 作为计划作业刷新。这样,您始终可以快速获得相对最新的结果。

标签: laravel eloquent


【解决方案1】:

我希望做一个 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

所以允许的是已经配置的polymorphic relationship

因此,关系将是:

  • 拥有表格: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 使用 switch 在内部循环允许。
  • array_values(array_unique()) 避免重复 ID。
  • 在内存中,3 个 id 数组($teamIds$groupIds$formIds
  • 在内存中,相关权限 eloquent 集合(这个可以优化,如果需要的话)。

优点和缺点

优点:

  • 时间:单个查询的时间总和小于连接和 OR 的大查询的时间。
  • DB 资源:带有 join 和 or 语句的查询使用的 MySQL 资源大于其单独查询的总和。
  • 金钱:更少的数据库资源(处理器、RAM、磁盘读取等),比 PHP 资源更昂贵。
  • :如果你不是在查询一个只读的从服务器,你的查询会产生更少的行读锁(读锁在 MySQL 中是共享的,所以它不会锁定另一个读,但是它将阻止任何写入)。
  • 可扩展:这种方法允许您进行更多的性能优化,例如对查询进行分块。

缺点:

  • 代码资源:在代码中而不是在数据库中进行计算,显然会在代码实例中消耗更多的资源,尤其是在存储中间信息的RAM中。在我们的例子中,这只是一个 id 数组,这应该不是问题。
  • 维护:如果你使用 Laravel 的属性和方法,并且你在数据库中进行任何更改,那么在代码中更新将比你进行更明确的查询和处理更容易。
  • 矫枉过正?:在某些情况下,如果数据不是那么大,优化性能可能矫枉过正。

如何衡量绩效

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

  1. Slow query logs
  2. ANALYZE TABLE
  3. SHOW TABLE STATUS LIKE
  4. EXPLAIN; Extended EXPLAIN Output Format; using explain; explain output
  5. SHOW WARNINGS

一些有趣的分析工具:

【讨论】:

  • 第一行是什么?使用查询几乎总是更好的性能明智,因为在 PHP 中运行各种循环或数组操作更慢。
  • 如果你有一个小型数据库,或者你的数据库机器比你的代码实例强大得多,或者数据库延迟非常糟糕,那么是的,MySQL 更快,但通常情况并非如此。
  • 优化数据库查询时,需要考虑执行时间、返回的行数,最重要的是检查的行数。如果 Tim 说查询变慢,那么我假设数据在增长,因此检查的行数也在增长。此外,数据库并未像编程语言那样针对处理进行优化。
  • 但是你不需要相信我,你可以运行EXPLAIN,为你的解决方案,然后你可以为我的简单查询解决方案运行它,看看区别,然后想想如果一堆 id 中的一个简单的 array_merge()array_unique(),真的会减慢你的进程。
  • 在 10 个案例中的 9 个案例中,mysql 数据库在运行代码的同一台机器上运行。数据层旨在用于数据检索,并针对从大​​型集合中选择数据进行了优化。我还没有看到array_unique()GROUP BY/SELECT DISTINCT 语句快的情况。
【解决方案3】:

我相信您可以为此使用 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);
}

【讨论】:

    【解决方案4】:

    您为什么不能简单地查询您需要的表单,而不是执行Form::all(),然后在其后链接filter() 函数?

    像这样:

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

    所以是的,这会进行一些查询:

    • $user 的查询
    • 一个用于$user-&gt;team
    • 一个$user-&gt;team-&gt;forms
    • 一个$user-&gt;permissible
    • $user-&gt;permissible-&gt;groups一个
    • 一个$user-&gt;permissible-&gt;groups-&gt;forms

    但是,优点是您不再需要使用该策略,因为您知道用户允许使用 $forms 参数中的所有表单。

    因此,此解决方案适用于您在数据库中拥有的任何数量的表单。

    使用merge()的注意事项

    merge() 合并集合,并丢弃重复的 它已经找到的表单ID。因此,如果出于某种原因,来自 team 关系也是与user 的直接关系,它只会在合并集合中显示一次。

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


    如果您希望它更快,您应该使用 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 对象列表。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-03-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-06
      • 1970-01-01
      • 2014-03-03
      相关资源
      最近更新 更多