CanCan: Performance issue regarding usage of roles(has_role?) – Avoid N + 1 queries for abilities

Reading Time: 2 minutes

Currently, in our project we use CanCanCan to control permissions/privileges. A common usage of CanCanCan is having multiple user roles, in that way we are able to grant/deny access to users depending on their roles and as a result of the intensive usage of user helpers it is important to optimize everything related to database queries whenever we want to check users access, in order to optimize performance accordingly and after looking into the logs generated by the gem Bullet, we noticed that we’re creating more Database queries than needed (n + 1 queries), for that matter, I have decided to write about it, the changes that I made are really simple but they will improve the overall performance of our application.

has_role?

and more specific:

current_user.has_role?(:admin)
class User < ActiveRecord::Base

  def has_role?(role_name)
    # This `exists` makes more queries than needed
    roles.exists? { |role| role.name == role_name }
  end

end

The problem with this line is that every time that you check the user role a new query is made for that.

Let me show you the code that we used to have:

app/models/ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)
    if user.has_role? :Admin
      can :manage, :all
    elsif user.has_role? :Curator
      # logic for this other role
    elsif user.has_role? :OtherRole
      # logic for this role
    end
  end
end

As you see in every controller that uses authorization when granting/denying actions we were making sometimes three queries total, which is not cool 🙁

As I said the problem is related to the usage and declaration of the method:

has_role?

So, instead of making new queries when trying to know what kind of privileges the current user has, we could avoid that using plain ruby(avoid SQL queries), so basically the refactor will look like this:

app/models/user.rb

class User < ActiveRecord::Base

  def curator?
    @curator ||= has_role?(:Curator)
  end

  def admin?
    @admin ||= has_role?(:Admin)
  end

  def has_role?(role_name)
    roles.any? { |role| role.name == role_name }
  end

end

And for the app/models/ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)
    if user.admin?
      can :manage, :all
    elsif user.curator?
      # logic for this role
    elsif user.has_role?(:OtherRole)
      # logic for this other role
    end
  end
end

Next time you work with your ability/abilities class take a moment to make sure you're not having n + 1 queries in there.

That's it, a pretty simple refactor, isn't it?

As always guys keep reading, coding and relax
H.

0 Shares:
You May Also Like