HFX
HFX

Reputation: 592

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

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.

Upvotes: 4

Views: 2352

Answers (2)

HFX
HFX

Reputation: 592

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.

Upvotes: 0

Xavier Delamotte
Xavier Delamotte

Reputation: 3599

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.

Upvotes: 2

Related Questions