How to avoid N+1 in Pundit policy for show?/update?/destroy?
Asked Answered
E

1

6

I'm using ActiveAdmin gem together with Pundit (and Rolify) gem.

This is how I wrote my policy (taken from: https://github.com/activeadmin/activeadmin/blob/master/spec/support/templates/policies/application_policy.rb):

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def show?
    scope.where(id: record.id).exists?
  end

  def create?
    user.has_role?(:staff, record.company)
  end

  def update?
    scope.where(id: record.id).exists?
  end

  def destroy?
    scope.where(id: record.id).exists?
  end

  def destroy_all?
    true
  end

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      if user.admin?
        scope.all
      else
        company_ids = Company.with_role(:staff, user).map(&:id)
        scope.where(company_id: company_ids)
      end
    end
  end
end

This causes N+1 query each time scope.where(id: record.id).exists?. On index page, show?, update? and destroy? are called for each record in the table.

How can I avoid the N+1 query in this case?

I'm trying to: 1) Include/preload roles together with user for calls to current_user 2) I'm trying to memoize the scope or use some array method to prevent hitting the db with where and exists? methods. But scope.find still makes the db query for every new row.

Thanks!

Equivalency answered 10/6, 2020 at 12:20 Comment(0)
M
2

first of all, I suggest adding a method to the User object to return company_ids where it is staff helps.

class User #or AdminUser right?
  def company_ids
      @company_ids ||= Company.with_role(:staff, self).map(&:id)
  end
end

than you can change

 def destroy?
    scope.where(id: record.id).exists?
  end

to

 def destroy?
    return true user.admin?
    user.company_ids.include?(record.company_id)
  end

and resolve method for Scope now looks this way

def resolve
      if user.admin?
        scope.all
      else
        scope.where(company_id: user.company_ids)
      end
    end
  end
Magnifico answered 1/9, 2021 at 12:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.