How to make Low-Level caching collaborate with Association caching in Rails?

元气小坏坏 提交于 2021-02-09 09:25:12

问题


I am currently working on a project with Rails 5. I want to boost the performance, so I decided to use Low-Level caching like the following:

class User < ApplicationRecord
  has_one :profile

  def cached_profile 
    Rails.cache.fetch(['Users', id, 'profile', updated_at.to_i]) do
      profile
    end
  end
end

class Profile < ApplicationRecord
  belongs_to :user, touch: true
end

It works fine respectively. But now I want to make the two caches to collaborate. What I want is that the associated object doesn't need to be retrieved from the database once it is retrieved from the cache store(redis here).

irb> u = User.take
irb> u.cached_profile # fetch from the redis. Can I set the association caching in `cached_profile`?
irb> u.profile # fetch from the database
  Profile Load (1.4ms) SELECT  "profiles".* FROM "profiles" WHERE "profiles"."user_id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
irb> u.profile # fetch from the association caching

u.profile shouldn't fetch from the database because of it's already retrived from the redis, how to achieve this?

Update 1:

I found that there's an instance variable @association_cache in the instance of ActiveRecord::Base, which stores cached associations and determines if an association should be retrieved from the database.

I think I could do something like user.instance_variable_get(:@association_cache)['profile'] = cached_profile to make it work. But the value in the @association_cache is an instance of ActiveRecord::Associations::HasOneAssociation and I don't know how to build the user to it currently.


回答1:


You may override directly the profile method to apply your low level cache.

class User
  has_one :profile

  def profile 
    Rails.cache.fetch(['Users', id, 'profile', updated_at.to_i]) do
      super
    end
  end
end

class Profile
  belongs_to :user, touch: true
end

But beware, as you may encounter some surprises. You should udpate the cache on the profile= setter as well for example.




回答2:


There's an instance variable @association_cache in the instance of ActiveRecord::Base, which stores cached associations and determines if an association should be retrieved from the database.

We can achieve it with @association_cache like:

class User < ApplicationRecord
  has_one :profile

  def cached_profile
    cache = Rails.cache.fetch(['Users', id, 'profile', updated_at.to_i]) do
      profile
    end

    reflection = self.class.reflect_on_association(:profile)
    if association_instance_get(name).nil?
      association = reflection.association_class.new(self, reflection)
      association.target = cache
      association_instance_set(:profile, association)
    end

    cache
  end
end

class Profile < ApplicationRecord
  belongs_to :user, touch: true
end

Now we can avoid to refetch data from the database if it has been retrieved from the cache store.

irb> u = User.take
irb> u.cached_profile # fetch the profile from the database and use the redis to cache it
  Profile Load (1.4ms) SELECT  "profiles".* FROM "profiles" WHERE "profiles"."user_id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
irb> u.profile # use the cached version from the redis
irb> u.profile.reload # reload from the database
  Profile Load (1.4ms) SELECT  "profiles".* FROM "profiles" WHERE "profiles"."user_id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

UPDATE:

I built a gem cache_associations for this.

class User < ApplicationRecord
  include CacheAssociations

  has_one :profile
  cache_association :profile
end

It's doing the same thing, but it's neat and much easier to use.



来源:https://stackoverflow.com/questions/47407542/how-to-make-low-level-caching-collaborate-with-association-caching-in-rails

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!