2
votes

I am trying to speed up my app by eager loading some sql calls. I am using the CanCan gem to handle my admin authorization. I have a Roles table with three different roles and a many-to-many table roles_users. Each time a page loads with a CanCan ability set it does three separate sql queries.

 Role Load (0.6ms)  SELECT "roles".* FROM "roles" INNER JOIN "roles_users" ON "roles"."id"    
= "roles_users"."role_id" WHERE "roles_users"."user_id" = 2 AND "roles"."name" = 'admin'  
  LIMIT 1
Role Load (0.4ms)  SELECT "roles".* FROM "roles" INNER JOIN "roles_users" ON "roles"."id" 
  = "roles_users"."role_id" WHERE "roles_users"."user_id" = 2 AND "roles"."name" =  
 'manager' LIMIT 1
 Role Load (0.3ms)  SELECT "roles".* FROM "roles" INNER JOIN "roles_users" ON "roles"."id" 
  = "roles_users"."role_id" WHERE "roles_users"."user_id" = 2 AND "roles"."name" = 'user' 
  LIMIT 1 

I have tried putting a default_scope :include => :roles in the User class and also putting :includes in the has_and_belongs_to_many calls.

Where can I eager load the Roles table to use only 1 SQL query?

2
Welcome to stack overflow. Remember to upvote all useful answers, including those to others' questions. Remember to check/approve the best answer to your own questions. - Larry K

2 Answers

2
votes

Looks like you might have some code that looks like the below. Namely, it takes a string or symbol that you need to compare to some value in a role record, and is not the role itself. So look for something like this:

def do_i_have_role(role_name_to_check)
  self.roles.detect do |role|
    role.name == role_name_to_check
  end
end

You could do some eager loading here to fix it up, but another way to skin this cat is to restructure your query. Namely, look for the role object first, then see if your account has that role.

def do_i_have_role(role_name_to_check)
  role = Role.where(:name => role_name_to_check)
  self.roles.include? role
end

Now it's just one hit to the roles database instead of 3 (and a hit to the accounts table and one more hit to the account_user table which you will probably never be able to avoid).

1
votes

You can improve your question by showing the code that you're using to query the cancan system (the api calls to cancan).

It looks like you're doing separate queries to see if the user is in the admin, manager, or user category.

You should order the queries to have the most likely user type queried first. Eg, if the odds are that a person is most likely a user, then query for that first.

Even better, cache the user's role in the session. That way you're only doing the queries one time.

Added

You can use after login/logout hooks with devise to set the value in the session. You can look up the role (from the session) with Action Controller filters. Session docs

Security issues

  • Use devise after logout hook to clear the role in the session.
  • Make sure your tests cover the sessions' role id
  • You don't need to worry about people giving self-upgrades to their role: sessions are digitally signed against changes. See the session docs.
  • Users being deleted/changed on the fly:

Depending on your level of security, you may need to cover the situation of where a user has been deleted or down-graded and you want the change to take place immediately (rather than waiting until the next time the user logs in).

Doing so can be tricky. Some techniques:

  • One way is to store a role-checked-timestamp in the session along with the role_id. Then invalidate the role_id (look it up again) if its been more than 30 min, etc.
  • Or store the role_id for the user in memcache, not the session store. Then the other process (which did the delete-user) can remove the role_id values from the cache for active user sessions.
  • A simpler, brute force way: give admins a way to clear all current sessions (would require all current users to login again). This would take care of the infrequent situation of having to boot someone off the system.
  • As many systems do, timeout all sessions after a couple of hours. Or clear all sessions once per day. If you do this, it is good to implement and test auto re-login. Including returning to the requested page after re-login. The re-login will stop people whose credentials have been changed.

Use constants You can store role_ids in the session (or memcache). But compare them with constants. Eg user.role_id === ROLE_ADMIN You can set constants at run time by looking up the role_ids from the db. Be sure to do this once per rails process in the initialization system. Not once per incoming request.