2

In my rails 4 app I'm trying to take off with caching, but I'm a bit confused thanks to the different versions of cache-key-settings, cache helpers and auto-expiration.

So let me ask this through few examples. I don't move the examples to different questions on purpose since I feel this way anybody can understand the subtle differences at one glance.

1: latest users in sidebar

I'd like to display the latest users. This of course is the same for all the users in the app and displayed on all the pages. In the railscasts I saw a similar example where it got expired by calling expire_fragment... in a controller. But according to other resources this should expire automatically when something changes (e.g. new user registration). So my question: Am I setting the key properly and will it auto-expire?

_sidebar.html.erb (displayed on all pages in sidebar)

<% cache 'latest-users' %>
  <%= render 'users/user_sidebar' %>
<% end %>

_users_sidebar.html.erb

<% @profiles_sidebar.each do |profile| %>
  <%= profile.full_name %>
  ........
<% end %>

2: product show page

I'd like to display a given product (only on show page). This is the same again for all the users, but there are more versions since there are more products. The question is the same again: Am I setting the key properly and will it auto-expire?

products/show.html.erb

<% cache @product %>
  <%= @product.name.upcase %>
  <%= @product.user.full_name %>
<% end %>

3: products/index (paginated with will-paginate gem)

Here I'd like to cache all the products on a given page of the pagination at once, so products get cached in blocks. This is also the same for all the users, and only gets displayed on the products index page. (Later on I'd like to implement the russian-doll-caching for the individual products on this page.) My question: Am I doing this right and will it auto-expire?

products index.html.erb

<% cache [@products, params[:page]] %>
  <%= render @products %>
<% end %>

_product.html.erb

<%= product.name %>
<%= product.user.full_name %>
.....

Example code I tried to use (not sure if it's a good one):

First with index page and with no russian doll.

enter image description here

Second is with russian doll for the show page with comments.

enter image description here

Sean Magyar
  • 2,360
  • 1
  • 25
  • 57
  • in case you don't read this line: You could read the [article](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) written by DHH, it described Russian Doll Caching very well. And the [API doc](http://api.rubyonrails.org/classes/ActionView/Helpers/CacheHelper.html) – abookyun Mar 31 '16 at 06:35
  • Thanks abookyun! I had read the article before but didn't like the article that much since it's using the rails3 syntax in which you have to use "v1", "v5", etc. It's kinda confusing if you are new to the topic. I will check out the api doc. – Sean Magyar Mar 31 '16 at 07:37

2 Answers2

2

IMHO, cache key is the spirit of the whole concept.

Now let's discuss these examples.

  1. latest users in sidebar: fixed string as cache key

cache_key could be views/latest-users/7a1156131a6928cb0026877f8b749ac9 which the digest 7a11561.. is MD5 of cache block literal.

In this case, the cache only expires when you change the template or anything in this block.

<% cache 'latest-users' do %>
  <%= render 'users/user_sidebar' %>
<% end %>
  1. product show page: object as cache key

cache_key could be views/product/123-20160330214154/9be3eace37fa886b0816f107b581ed67, notice the cache is namespace with #{product.to_s}/#{product.id}-#{product.updated_at}.

In this case, the cache expires when (1) product.updated_at changed or (2) cache block literal changed.

And notice the cache differs from different product by id.

<% cache @product %>
  <%= @product.name.upcase %>
  <%= @product.user.full_name %>
<% end %>
  1. products/index (paginated with will-paginate gem): array as cache key

cache_key could be views/product/123-20160330214154/product/456-20160330214154/d5f56b3fdb0dbaf184cc7ff72208195e not sure about this. but anyway, it should be expand something like this.

In this case, the cache expires when (1) either product-123 or product-456 changed(product.updated_at changed) or (2) cache block literal changed.

And the cache differs from different content of @products by their ids, so there's no need to append params[:page], it will cache each different page because of their different @products content.

<% cache [@products, params[:page]] %>
  <%= render @products %>
<% end %>

You could read the article written by DHH, it described Russian Doll Caching very well. And the API doc

abookyun
  • 457
  • 5
  • 13
2

There is a pretty big difference between caching a single record and a collection of records.

You can quite simply tell if a single record has been changed by looking at the timestamp. The default cache_key method works like this:

Product.new.cache_key     # => "products/new" - never expires
Product.find(5).cache_key # => "products/5" (updated_at not available)
Person.find(5).cache_key  # => "people/5-20071224150000" (updated_at available)

However telling when a collection is stale depends very much on how it is used.

In your first example you only really care about the created_at timestamp - in other situations you might want to look at when records are updated or even when associated records have been inserted / updated. There is no right answer here.

1.

First you would pull N number of users ordered by created_at:

@noobs = User.order(created_at: :desc).limit(10)

We can simply invalidate the cache here by looking at the first user in the collection.

<!-- _sidebar.html.erb -->
<% cache(@noobs.first) do %>
  <%= render partial: 'users/profile', collection: @noobs %>
<% end %>

We can do this since we know that if a new user is registered (s)he will bump the previous number one down a slot.

We can then cache each individual user with russian doll caching. Notice that since the partial is called profile the partial gets passed the local variable profile:

<!-- users/_profile.html.erb -->
<% cache(profile) do %>
  <%= profile.full_name %>
<% end %>

2.

Yes. It will expire. You might want to a partial with russian doll caching like the other examples.

3.

For a paginated collection you can use the ids of the records in the array to create a cache key.

Edited.

Since you don't want serve a stale representation of a product that may be updated you would also want to use updated_at as a cache key in the "outer layer" of the russian doll.

In this case it makes sense to load the records entirely. You can ignore my previous comment about .ids.

products/index.html.erb:

<% cache([@products.map(&:id), @products.map(&:updated_at).max]) do %>
  <%= render @products %>
<% end %>

products/_product.html.erb:

<% cache(product) do %>
  <%= product.name %>
<% end %>
max
  • 96,212
  • 14
  • 104
  • 165
  • max, well explained. I have some questions though thanks to my lack of experience. How could I use russian-doll-caching in the case of the 2. example? There is only 1 record on the page. My other 2 questions are regarding the `map` vs `ids` : `map` is faster if data is loaded? As far as I know the point of the pagination is that the data shouldn't be loaded. So how can the data be loaded in this case? – Sean Magyar Mar 30 '16 at 20:42
  • Sorry, I totally confused the example in 2 with 3. russian-doll-caching is not very relevant when you are just rendering a single record. – max Mar 30 '16 at 21:04
  • 3. If you are using the collection anywhere else it might already be loaded. However I just realized that you might want to use the `updated_at` as a key as well if so that you don't serve a stale version. Edited. – max Mar 30 '16 at 21:16
  • max, I'm totally new to caching. Sry, but I don't see where you edited your answer with the updated_at. I also update my question with the sample I think it differs from your answer. – Sean Magyar Mar 30 '16 at 21:24
  • max, I have a last question if you don't mind: Why can't I use simply `<% cache 'sidebar_users' %>` in the 1. example. Is it because of the optional russian-doll? – Sean Magyar Mar 30 '16 at 21:48
  • You could do it like `<% cache 'sidebar_users' %>` but that means that you need to manually invalidate the cache when you create users in your controller - I don't like it since it makes your controller too aware and responsible about what a completely unrelated view is doing. – max Mar 30 '16 at 21:53
  • max, during implementing this I ran into a new problem. I have the `<% cache(profile) do %>` in the `_user_sidebar.html.erb`, which works fine as it is. But on the `profile#show` page I would use there the same cache name for the single profile, so on that page the cache name would appear twice. Can that cause any problem? – Sean Magyar Mar 31 '16 at 11:17
  • hmm - yeah using the same cache key might cause the content to get mixed up. You could use `<% cache([profile, 'sidebar']) do %>` which would put `-sidebar` on the end of the key. – max Mar 31 '16 at 11:22
  • max, I create a new question soon and I move some stuff from the comments there. You answer a bunch of question here. I will send the link here. – Sean Magyar Mar 31 '16 at 11:34
  • Here is the question: http://stackoverflow.com/questions/36332396/rails4-after-caching-query-still-runs – Sean Magyar Mar 31 '16 at 11:41
  • max, I have here new one for double nested models. Could you pls take a look at it? You understand pretty well this topic and I could learn a lot from your insight: http://stackoverflow.com/questions/36340017/rails4-double-nested-models-russian-doll-caching – Sean Magyar Mar 31 '16 at 17:36
  • max, why you use `<% cache (product) do %>` in the 3rd example for the inner caching rather than `<% cache (product.updated_at) do %>`? – Sean Magyar Apr 02 '16 at 11:34
  • Because `Product#cache_key` will create a cache key based on the `updated_at` which is actually better since it includes the ID so if you had two resources created at the exact same time there is no risk of collision. – max Apr 07 '16 at 10:39
  • Thanks max! Could you also take a look at this one?http://stackoverflow.com/questions/36460001/rails-leaving-out-authorized-parts-from-fragment-caching I could solve it, but I don't know the rails conventions, so would be great to see your solution. – Sean Magyar Apr 07 '16 at 19:24
  • max, could you pls take a look at this? https://stackoverflow.com/questions/51689938/rails-5-api-low-level-caching?noredirect=1#comment90348060_51689938 – Sean Magyar Aug 05 '18 at 18:23