How would I change this to prevent numerous queries against the database to check the user role?

527 Views Asked by At

Last Updated: 29 Aug 2013 18:54 EST

I have the following module defined and then included into my model. I am using the rolify gem to give my users roles.

module Permissions::Offer
  extend ActiveSupport::Concern

  included do
    # `user` is a context of security
    protect do |user, offer|
      # Admins can retrieve anything
      if user.has_role? :administrator
        scope { all }

        # ... and view, create, update, or destroy anything
        can :view
        can :create
        can :update
        can :destroy
      elsif user.present?
        # Allow to read any field
        can :view
        can :create

        # Checks offered_by_id keeping possible nil in mind
        # Allow sellers to modify/delete their own offers
        if offer.try(:offered_by_id) == user.id
          can :update
          can :destroy
        end
      else
        # Guests can't read the text
        cannot :view
      end
    end
  end
end

What I am experiencing is that when I do the following...

respond_with Offer.restrict!(current_user)

It queries the roles table for every offer that is returned. Is there anyway to have it not make this request repeatedly when requesting a list of offers? I'm sure I could cache the response to avoid the database hit, but I'd rather it not hit the cache either.

If I open a rails console and do the following I get the same result:

current_user = User.first

Offer.restrict!(current_user).to_a

I have installed the bullet gem to see if it considers it an N+1 query, and it doesn't not detect it. I believe because the included gets called every time a new instance of offer gets created it fires off this call to verify permissions. That coupled with the fact that rolify does not cache it's user role checks for any length of time makes this less than ideal. I suppose rolify does this to allow for the changing of roles on the fly without having to deal with clearing the cache. As of now the only way I can see to solve this is to implement caching of my own.

1

There are 1 best solutions below

0
On

I opened an issue with rolify to see if they are interested in creating a more permanent solution. For anyone else that encounters this, here's what I did int eh meantime.

def has_role?(role)
  roles = Rails.cache.fetch(roles_for: { object_id: self.object_id }, expires_in: 10.seconds, race_condition_ttl: 2.seconds) { self.roles.map(&:name) }
  roles.include?(role)
end

This doesn't do everything the real method does.. but it suits my purposes.

Here is a link to the source for anyone that wishes to implement something like this on all the methods.

https://github.com/EppO/rolify/blob/master/lib/rolify/role.rb