8

Every time I hit an authenticated page, I notice devise issuing an SQL statement :

User Load (0.2ms) SELECT users.* FROM users WHERE (users.id = 1) LIMIT 1

(I'm using Rails 3 btw .. so cache_money seems out as a solution and despite a lot of searching I've found no substitute).

I tried many overrides in the user model and only find_by_sql seems called. Which gets passed a string of the entire SQL statement. Something intuitive like find_by_id or find doesn't seem to get called. I 'can' override this method and glean the user-id and do a reasonable cache system from that - but that's quite ugly.

I also tried overriding authenticate_user which I can intercept one SQL attempt but then calls to current_user seems to try it again.

Simply, my user objects change rarely and its a sad state to keep hitting the db for this instead of a memcache solution. (assume that I'm willing to accept all responsibility for invalidating said cache with :after_save as part but not all of that solution)

user724148
  • 81
  • 1
  • 2
  • Devise does a lot of checking. Checking you *want* it to do (usually). But you're right, some amount of that should be cacheable. Have you fired up Ruby-Debug to figure out where the different finds are coming from? I didn't see a find_by_sql in the source. The common case is the logged in user who is visiting pages protected by Devise and Devise is making sure the user is really valid. If you can cache that round-trip, you should have much of the problem solved. More difficult: there are a number of ways Devise itself can invalidate the cache. For example, if a user resets the password. – Steve Ross Apr 25 '11 at 19:54

3 Answers3

13

The following code will cache the user by its id and invalidate the cache after each modification.

class User < ActiveRecord::Base

  after_save :invalidate_cache
  def self.serialize_from_session(key, salt)
    single_key = key.is_a?(Array) ? key.first : key
    user = Rails.cache.fetch("user:#{single_key}") do
       User.where(:id => single_key).entries.first
    end
    # validate user against stored salt in the session
    return user if user && user.authenticatable_salt == salt
    # fallback to devise default method if user is blank or invalid
    super
  end

  private
    def invalidate_cache
      Rails.cache.delete("user:#{id}")
    end
end
Loqman
  • 1,487
  • 1
  • 12
  • 24
Mic92
  • 1,298
  • 15
  • 13
1

I've been struggling with this, too.

A less convoluted way of doing this is to add this class method to your User model:

def self.serialize_from_session(key, salt)
  single_key = key.is_a?(Array) ? key.first : key
  Rails.cache.fetch("user:#{single_key}") { User.find(single_key) }
end

Note that I'm prepending the model name to the object ID that is passed in for storing/retrieving the object from the cache; you can use whatever scheme fits your needs.

The only thing to worry about, of course, is invalidating the user in the cache when something changes. It would have been nice instead to store the User in the cache using the session ID as part of the key, but the session is not available in the model class, and is not passed in to this method by Devise.

EK0
  • 305
  • 5
  • 16
1

WARNING: There's most likely a better/smarter way to do this.

I chased this problem down a few months back. I found -- or at least, I think I found -- where Devise loads the user object here: https://github.com/plataformatec/devise/blob/master/lib/devise/rails/warden_compat.rb#L31

I created a monkey patch for that deserialized method in /initializers/warden.rb to do a cache fetch instead of get. It felt dirty and wrong, but it worked.

Adam Rubin
  • 777
  • 1
  • 7
  • 16