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.