问题
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