Gabriel Hilal
13 Nov, 2015 • 4 min read

Rolify, Active Model Serializers and the N+1 Problem

Rolify

Rolify is a popular gem that helps to manage roles in a Rails application. It allows you to create three types of roles:

@user.add_role(:admin)               # global role
@user.add_role(:admin, Forum)        # class role
@user.add_role(:admin, Forum.first)  # instance role

The gem adds two tables to your database (roles and users_roles), and provides useful methods to easily add, remove, list and check roles. In this article, we will discuss the has_role? method, which checks if the user has a specific role in the system:

@user.add_role(:admin, Forum.first)    #=> adds the admin role for @user in the first forum

@user.has_role?(:admin, Forum.first)   #=> true
@user.has_role?(:admin, Forum.last)    #=> false

Active Model Serializers

Active Model Serializers is another popular gem used to serialize your API JSON responses. You can easily specify the attributes to be included in the JSON response.

Let’s say we have an index action that lists all the forums in your application, and we want to return only the title, description and the list of allowed actions based on the current user roles. If the current_user has the role admin in a specific forum, we can add the update permission to the list of allowed actions:

class ForumSerializer < ActiveModel::Serializer
  attributes :title, :description, :actions

  def actions
    current_user.has_role?(:admin, object) ? ['update'] : []
  end
end

If you have three forums and the current user is admin only in the first forum, the JSON response would return something like:

[
 {'title': 'forum 1', 'description': 'some description', 'actions': ['update']},
 {'title': 'forum 2', 'description': 'some description', 'actions': []},
 {'title': 'forum 3', 'description': 'some description', 'actions': []}
]

N+1 Problem

At first glance everything looks good, our roles work just fine, and the serializer is returning the expected json format. However, if you keep adding more forums to your system, you will notice at some point a considered decrease in performance.

The problem lies in the has_role? method, which calls the where condition, creating a new query everytime we use the has_role?. So, if you have a list of 5K forums, you should go have a coffee and wait for the 5k queries to finish…

Rolify N+1 Issue

Solution

In order to solve the N+1 problem, we could preload the user roles and then use ruby enumerables to check if there is any role matching the conditions. Having that in mind, we can change our serializer to avoid the has_role? method:

class ForumSerializer < ActiveModel::Serializer
  attributes :title, :description, :actions

  def actions
    roles = current_user.roles.find_all do |role|
      role.resource_id == object.id && role.resource_type == 'Forum' && role.name == 'admin'
    end
    roles.any? ? ['update'] : []
  end
end

In this way we can achieve the same result with a couple of queries:

Rolify

Or, you could create a generic solution for your application implementing a method in the User model as discussed in this issue.

Good News

Based on the above issue, a pull request was recently merged into the master branch. This PR adds the has_cached_role? feature to the Rolify gem.
At the time this article was written the latest version of gem didn’t have the has_cached_role? yet, but you can take advantage of it using the master branch. Just add the following to your Gemfile:

gem "rolify", git: 'git://github.com/RolifyCommunity/rolify.git'

In this way, you should be able to use the has_cached_role? method in the serializer:

class ForumSerializer < ActiveModel::Serializer
  attributes :title, :description, :actions

  def actions
    current_user.has_cached_role?(:admin, object) ? ['update'] : []
  end
end
Post by: Gabriel Hilal