Question

I have the following three models (massively simplified):

class A < ActiveRecord::Base
  has_many :bs
  has_many :cs, :through => :bs
end

class B < ActiveRecord::Base
  belongs_to :a
  has_many :cs
end

class C < ActiveRecord::Base
  belongs_to :b
end

It seems that A.cs gets cached the first time it is used (per object) when I'd really rather it not.

Here's a console session that highlights the problem (the fluff has been edited out)

First, the way it should work

rails console
001 > b = B.create
002 > c = C.new
003 > c.b = b
004 > c.save
005 > a = A.create
006 > a.bs << b
007 > a.cs
=> [#<C id: 1, b_id: 1>]

This is indeed as you would expect. The a.cs is going nicely through the a.bs relation.

And now for the caching infuriations

008 > a2 = A.create
009 > a2.cs
=> []
010 > a2.bs << b
011 > a2.cs
=> []

So the first call to a2.cs (resulting in a db query) quite correctly returned no Cs. The second call, however, shows a distinct lack of Cs even though they jolly well should be there (no db queries occurred).

And just to test my sanity is not to blame

012 > A.find(a2.id).cs
=> [#<C id: 1, b_id: 1>]

Again, a db query was performed to get both the A record and the associated C's.

So, back to the question: How do I force rails to not use the cached result? I could of course resign myself to doing this workaround (as shown in console step 12), but since that would result in an extra two queries when only one is necessary, I'd rather not.

Was it helpful?

Solution

I did some more research into this issue. While using clear_association_cache was convenient enough, adding it after every operation that invalidated the cache did not feel DRY. I thought Rails should be able to keep track of this. Thankfully, there is a way!

I will use your example models: A (has many B, has many C through B), B (belongs to A, has many C), and C (belongs to B).

We will need to use the touch: true option for the belongs_to method. This method updates the updated_at attribute on the parent model, but more importantly it also triggers an after_touch callback. This callback allows to us to automatically clear the association cache for any instance of A whenever a related instance of B or C is modified, created, or destroyed.

First modify the belongs_to method calls for B and C, adding touch:true

class B < ActiveRecord::Base
  belongs_to :a, touch: true
  has_many   :cs
end

class C < ActiveRecord::Base
  belongs_to :b, touch: true
end

Then add an after_touch callback to A

class A < ActiveRecord::Base
  has_many :bs
  has_many :cs, through: :bs

  after_touch :clear_association_cache
end

Now we can safely hack away, creating all sorts of methods that modify/create/destroy instances of B and C, and the instance of A that they belong to will automatically have its cache up to date without us having to remember to call clear_association_cache all over the place.

Depending on how you use model B, you may want to add an after_touch callback there as well.

Documentation for belongs_to options and ActiveRecord callbacks:

http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to

Hope this helps!

OTHER TIPS

All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example:

customer.orders                 # retrieves orders from the database
customer.orders.size            # uses the cached copy of orders
customer.orders.empty?          # uses the cached copy of orders

But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass true to the association call:

customer.orders                 # retrieves orders from the database
customer.orders.size            # uses the cached copy of orders
customer.orders(true).empty?    # discards the cached copy of orders
                                # and goes back to the database

Source http://guides.rubyonrails.org/association_basics.html

(Edit: See Daniel Waltrip's answer, his is far better than mine)

So, after typing all that out and just checking something unrelated, my eyes happened upon section "3.1 Controlling Caching" of Association Basics guide.

I'll be a good boy and share the answer, since I've just spent about eight hours of frustrating fruitless Googling.

But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass true to the association call:

013 > a2.cs(true)
C Load (0.2ms)  SELECT "cs".* FROM "cs" INNER JOIN "bs" ON "cs"."b_id" = "bs"."id" WHERE "bs"."a_id" = 2
=> [#<C id: 1, b_id: 1>]

So the moral of the story: RTFM; all of it.

Edit: So having to put true all over the place is probably not such a good thing as the cache would be bypassed even when it doesn't need to be. The solution proffered in the comments by Daniel Waltrip is much better: use clear_association_cache

013 > a2.clear_association_cache
014 > a2.cs
C Load (0.2ms)  SELECT "cs".* FROM "cs" INNER JOIN "bs" ON "cs"."b_id" = "bs"."id" WHERE "bs"."a_id" = 2
=> [#<C id: 1, b_id: 1>]

So now, not only should we RTFM, we should also search the code for :nodoc:s!

I found another way to disable query cache. In your model, just add a default_scope

class B < ActiveRecord::Base
  belongs_to :a
  has_many   :cs
end

class C < ActiveRecord::Base
  default_scope { } # can be empty too
  belongs_to :b
end

Verified it working locally. I found this by looking at active_record source code in active_record/associations/association.rb:

# Returns true if statement cache should be skipped on the association reader.
def skip_statement_cache?
  reflection.scope_chain.any?(&:any?) ||
    scope.eager_loading? ||
    klass.current_scope ||
    klass.default_scopes.any? ||
    reflection.source_reflection.active_record.default_scopes.any?
end

To clear the cache, use .reload

author.books                 # retrieves books from the database
author.books.size            # uses the cached copy of books
author.books.empty?          # uses the cached copy of books

author.books                 # retrieves books from the database
author.books.size            # uses the cached copy of books
author.books.reload.empty?   # discards the cached copy of books
                         # and goes back to the database

Source: Controlling caching

You can use the extend option and provide a module to reset the loading before it takes place like so:

  # Usage:
  # ---
  #
  # has_many :versions,
  #   ...
  #   extend: UncachedAssociation
  #

  module UncachedAssociation
    def load_target
      @association.reset
      super
    end
  end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top