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 () 문의 식별자를 사용하여 양식 모델을 쿼리하십시오 .

내 질문:

어떤 방법이 최고의 성능을 제공하고 더 나은 성능을 제공 할 수있는 다른 옵션이 있습니까?


다대 다를 만들 수도 있습니다 사용자가 양식에 액세스 할 수 있는지 링크에 접근을
코드를 돈을

사용자 양식 권한을 쿼리하기 위해 특별히 테이블을 작성하는 것은 어떻습니까? 그만큼user_form_permissionuser_idand 만 포함 테이블 form_id입니다. 이렇게하면 읽기 권한이 산들 바람이되지만 권한을 업데이트하는 것이 더 어려워집니다.
PtrTon

user_form_permissions 테이블의 문제점은 권한을 다른 엔티티로 확장하여 각 엔티티마다 별도의 테이블이 필요하다는 것입니다.
Tim

1
@Tim이지만 여전히 5 개의 쿼리입니다. 이것이 보호 회원의 영역 안에있는 경우에는 문제가되지 않을 수 있습니다. 그러나 이것이 초당 많은 요청을 얻을 수있는 공개 URL에있는 경우이 부분을 조금 최적화하고 싶습니다. 성능상의 이유로, 양식 또는 팀 구성원이 모델 옵저버를 통해 추가되거나 제거 될 때마다 별도의 테이블 (캐시 할 수 있음)을 유지합니다. 그런 다음 모든 요청에 ​​따라 캐시에서 가져옵니다. 나는이 질문과 문제가 매우 흥미로워 다른 사람들도 어떻게 생각하는지 알고 싶습니다. 이 질문은 더 많은 투표와 답변이 필요하고 현상금을 시작했습니다 :)
Raul

1
스케줄 된 작업으로 화면 갱신 할 수 있는 구체화 된보기 를 고려할 수 있습니다. 이렇게하면 항상 최신 결과를 빠르게 얻을 수 있습니다.
apokryfos

답변:


2

PHP보다 훨씬 나은 성능을 발휘하므로 SQL 쿼리를 수행하려고합니다.

이 같은:

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

수명이 긴 캐시 솔루션 (라 라벨의 캐시 추상화와 같은)이 있으며 변경을 수행 한 직후 영향을받는 캐시 인덱스를 제거 할 수도 있습니다. 캐시는 올바르게 사용하면 실제 게임 체인저입니다. 캐시를 구성하는 방법은 데이터 읽기 및 업데이트에 따라 다릅니다.
Gonzalo

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 () 문의 식별자를 사용하여 양식 모델을 쿼리하십시오.

몇 가지 도구를 사용하여 쿼리를 여러 번 실행하여 제안한 세 가지 옵션을 시도하고 성능을 모니터링 할 수 있지만 마지막 옵션이 최상의 성능을 제공 할 것이라고 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 ; 하나는 허용되며 다른 하나는 양식을 가져옵니다.
  • 조인이 없습니다 !!
  • 가능한 최소의 논리합 ( 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 리소스 : join 및 / 또는 문이있는 쿼리에서 사용하는 MySQL 리소스는 별도의 쿼리 합계에서 사용되는 것보다 큽니다.
  • : PHP 리소스보다 비싼 데이터베이스 리소스 (프로세서, RAM, 디스크 읽기 등)가 적습니다.
  • 잠금 : 읽기 전용 슬레이브 서버를 쿼리하지 않는 경우 쿼리에서 더 적은 행 읽기 잠금을 만듭니다 (읽기 잠금은 MySQL에서 공유되므로 다른 읽기는 잠그지 않지만 쓰기는 차단합니다).
  • 확장 성 :이 방법을 사용하면 쿼리 청크와 같은 성능을 최적화 할 수 있습니다.

단점 :

  • 코드 리소스 : 데이터베이스가 아닌 코드로 계산하면 코드 인스턴스, 특히 RAM에서 더 많은 리소스가 소비되므로 중간 정보가 저장됩니다. 우리의 경우, 이것은 단지 ID의 배열 일 것입니다. 실제로 문제가되지는 않습니다.
  • 유지 관리 : Laravel의 속성 및 방법을 사용하고 데이터베이스를 변경하면보다 명시적인 쿼리 및 처리를 수행하는 것보다 코드를 업데이트하는 것이 더 쉽습니다.
  • 과잉? : 경우에 따라 데이터가 크지 않은 경우 성능 최적화가 과도 할 수 있습니다.

성능 측정 방법

성능을 측정하는 방법에 대한 단서가 있습니까?

  1. 느린 쿼리 로그
  2. 분석 테이블
  3. 테이블 상태 표시처럼
  4. 설명 ; 확장 된 EXPLAIN 출력 형식 ; Explain 사용 ; 출력 설명
  5. 경고 표시

흥미로운 프로파일 링 도구 :


첫 줄은 무엇입니까? PHP에서 다양한 루프 또는 배열 조작을 실행하는 것이 느리기 때문에 쿼리를 사용하는 것이 거의 항상 현명합니다.
Flame

작은 데이터베이스가 있거나 데이터베이스 시스템이 코드 인스턴스보다 훨씬 강력하거나 데이터베이스 대기 시간이 매우 나쁜 경우에는 MySQL이 더 빠르지 만 일반적으로 그렇지는 않습니다.
Gonzalo

데이터베이스 쿼리를 최적화 할 때는 실행 시간, 반환 된 행 수, 가장 중요하게는 검사 된 행 수를 고려해야합니다. Tim이 쿼리 속도가 느려지고 있다고 말하면 데이터가 커지고 있다고 가정하므로 행 수를 검사합니다. 또한 데이터베이스는 프로그래밍 언어로 처리하기 위해 최적화되지 않았습니다.
Gonzalo

그러나 나를 믿을 필요는 없으며 솔루션에 대해 EXPLAIN 을 실행 한 다음 간단한 쿼리 솔루션에 대해 실행하고 차이점을 확인하고 단순 array_merge()하고 array_unique()많은 ID가 있는지 생각할 수 있습니다. 프로세스 속도를 늦 춥니 다.
Gonzalo

10 건 중 9 건에서 mysql 데이터베이스가 코드를 실행하는 동일한 시스템에서 실행됩니다. 데이터 계층은 데이터 검색에 사용되며 대규모 세트에서 데이터를 선택하는 데 최적화되어 있습니다. 나는 / 문 array_unique()보다 빠른 상황을 아직 보지 못했습니다 . GROUP BYSELECT 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 경우 병합 된 컬렉션에 한 번만 표시됩니다.

이는 실제로 Eloquent 모델 ID를 확인 Illuminate\Database\Eloquent\Collection하는 자체 merge()기능을 가지고 있기 때문입니다 . 따라서 Postsand와 같은 2 개의 다른 컬렉션 내용을 병합 할 때 실제로이 트릭을 사용할 수 없습니다.이 경우 Usersid를 가진 사용자와 id 3를 가진 게시물 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객체 목록을 검색하는 것입니다 .


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);
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.