Reputation: 10946
It seems to me that fragment caching and eager loading are -- at least sometimes -- somewhat at odds with each other. Let's say I have a User who has many posts which each has many comments which in turn also can have many comments and so on.
When I have to render the page, I could choose to do eager loading of the user, all her posts, all their comments and so on, in order to avoid hitting the database n-1 times. Or I could load each object lazily and rely on fragment caching to only query the database for objects that are new or have been changed. Using both fragment caching and eager loading seems to be wasteful, as I would potentially do a very complex query and instantiate a lot of objects only to use a small part of them.
But what if I have an application in which a User has many Foos which in turn has many Bars and so on, but in which each Foo is created complete with all its Bars and their associated objects at the same time and from then on never changes. In that case, I would like to use fragment caching for Foos which have been rendered, but to use eager loading when I have to load a new Foo with all its associated objects. After all, there's nothing to be gained from caching fragments at a more granular level.
What is the best way in Rails to accomplish this? I suppose I could do one query to get just the ids of the Foo, and then do an explicit find with eager loading when I have to render each Foo. Is there a better/more elegant/more idiomatic way of doing this?
Upvotes: 15
Views: 1426
Reputation: 1
You can use Russian Doll Caching for this type of problem, say we have Post that have comments and author, in home page you could use
# HomeController
def index
@posts = Post.includes(:author, :comments).limit(10)
end
view
- cache ["v1#home", @posts.maximum(:updated_at).to_i] do
- @post.each do |post|
- cache ["v1#post_in_home", @post.id, @post.updated_at.to_i] do
= post.title
= post.author.name
= post.content
- cache ["v1#comments_in_post", @post.comments.maximum(:updated_at).to_i] do
- @comments.each do |comment|
= comment.author
= comment.content
Upvotes: 0
Reputation: 774
I did something similar with caching using lambda's. Below you can get an idea. The problem what it solves - getting most popular users is a heavy operation and requires >5 sec. But with lambda you can cache list of users.
controller:
def index
@users = -> { User.by_rating }
end
view:
= cache "rating-list", expires_in: 1.day do
- @users.call.each do |user|
= render user
Upvotes: 0
Reputation: 4855
Building on @daviddb answer for Rails 4 specifically, I found it necessary to skip_digest
in the views as recreating the MD5 Hash for the templates within the controller for comparison seemed a bit much.
Also, without finding the object the first time, it will be difficult to get the object with last modified timestamp, so I found it helpful to do an initial query without .includes(:object1, :object2)
views/customers/show.html.slim
(adjust for your preferred templating engine)
- cache['customers/show', @customer], skip_digest: true do
h1
= @customer.account.name
= render 'account_summary', account: @customer.account
= render 'account_details', transactions: @customer.account.transactions
...
controllers/customers_controller.rb
def show
customer = Customer.find(params[:id])
if fragment_exist?(['customers/show', customer])
@customer = customer
else
@customer = Customer.includes(account: :transactions).find(params[:id])
end
end
Note: By setting skip_digest: true
you will need to clear your cache on deploy when altering the this view and any partials that it depends upon in order to ensure your new layouts are rendered properly.
Upvotes: 0
Reputation: 8954
You can use the fragment_exists?
method in the controller to prevent eager loading all objects when they are already in cache. This will only not be the case at the first time the page is called.
Like this:
if fragment_exists? "my_cache_key_#{id}"
# load your object without eager loading here
else
# eager load your objects here
end
Then in the view use fragment chaching:
<% cache("my_cache_key_#{@object.id}") do %>
...
...
...
<% end %>
That should do it for you!
Upvotes: 0