1

I have the following structure in my rails4 app for the posts. Users can comment on the post and replies can be written on the comments. I'd like to use russian-doll-caching with auto-expiring keys on the page, but I don't know how I should exactly do it in this case.

Can sby tell me how to use it in this case?

Models:

#post.rb

belongs_to :user
has_many :post_comments, dependent: :destroy

#post_comments.rb

belongs_to :user
belongs_to :post
has_many :post_comment_replies, dependent: :destroy

#post_comment_replies.rb

belongs_to :user
belongs_to :post_comments

posts/index.html.erb

 <div class="post-index new-post-insert">
   <%= render @posts %>
 </div>

_post.html.erb

<%= post.body %>
<%= post.user.full_name %>
....
<%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>

_post_comment.html.erb

<%= post_comment.body %>
<%= post_comment.user.full_name %>
......
<%= render partial: 'posts/post_comment_replies/post_comment_reply', collection: post_comment.post_comment_replies.ordered.included, as: :post_comment_reply, locals: { post_comment: post_comment } %>

_post_comment_reply.html.erb

<%= post_comment_reply.user.full_name %>
<%= post_comment_reply.body %>
Sean Magyar
  • 2,360
  • 1
  • 25
  • 57
  • This repo README could help https://github.com/rails/cache_digests lol – abookyun Mar 31 '16 at 17:44
  • abookyun, I've checked it, but this is the old syntax. I just would like to make sure I do everything fine. This is my first time implementing caching and a bunch of articles are out of date just like this repo. – Sean Magyar Mar 31 '16 at 18:16

1 Answers1

1

You need to do a few things

Add touch to your belongs_to relations

The children and grandchildren of Post needs to touch their parents so that the updated_at column updates which in turn invalidates the cache keys.

#post_comments.rb

belongs_to :user
belongs_to :post, touch: true
has_many :post_comment_replies, dependent: :destroy

#post_comment_replies.rb

belongs_to :user
belongs_to :post_comments, touch: true

Add the cache command to your views

posts/index.html.erb

In the main list of posts we want to cache on the latest updated_at for posts and the latest updated_at for the respective user.

 <div class="post-index new-post-insert">
    <% cache ["posts", @posts.maximum(:updated_at).to_i, @posts.map {|p| p.user.try(:updated_at).to_i}.max] %>
     <%= render @posts %>
    <% end %>
 </div>

_post.html.erb

<% cache ["postlist", post, post.user] %>
  <%= post.body %>
  <%= post.user.full_name %>
  ....
  <%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
<% end %>

_post_comment.html.erb

<% cache ["postcommentlist", post_comment, post_comment.user] %>
  <%= post_comment.body %>
  <%= post_comment.user.full_name %>
  ......
  <%= render partial: 'posts/post_comment_replies/post_comment_reply', collection: post_comment.post_comment_replies.ordered.included, as: :post_comment_reply, locals: { post_comment: post_comment } %>
<% end %>

_post_comment_reply.html.erb

<% cache ["postcommentreplylist", post_comment_reply, post_comment_reply.user] %>
  <%= post_comment_reply.user.full_name %>
  <%= post_comment_reply.body %>
<% end %>

This could be improved by using cached: true in the render partial function. However since we want to expire the cache if the user changes their username it becomes a bit tricky.

You can do that if you override all the models cache_key functions.

Why should I use cached: true in render partial?

Instead of calling cache inside each partial (like we do above) we could do

<%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments, cached: true %>

If we only need to cache on the post_comment´s updated_at.

The difference between the two are that when we cache inside the partial Rails issues a get command to the cachestore (e.g. memcache) one time per object. Therefore if you have 50 postcomments, there will be 50 separate requests to memcached to retrieve all.

But if we instead use cached: true in the render call Rails will issue a multi_get request to memcached and retrieve all 50 objects in one request. Thus improving page loadtime. In tests we have conducted in our production env. it decreased the page loadtime by ~50ms - ~200ms depending on the amount of data.

Jacob Rastad
  • 1,153
  • 10
  • 24
  • Jacbo, how could be this improved with the `cached: true` ? I mean everything seems to be cached. – Sean Magyar Mar 31 '16 at 19:09
  • I see, thanks Jacob for the explanation. I realized one more thing regarding your answer. Why don't you put cache around the `<%= render @posts %>`. Something like `<% cache(['index-posts', @posts.map(&:id), @posts.map(&:updated_at).max]) do %>`? – Sean Magyar Mar 31 '16 at 19:59
  • You can do that too but you need to take into account that you need to cache on the posts users `updated_at` as well. Otherwise the cache won't be invalidated when a user changes their username. – Jacob Rastad Mar 31 '16 at 20:03
  • Yes, that's what I would like to do. There is no point in this case, cuz nested stuff will definitely change. But I have other models where would be super useful. Could you pls add to the code how you would do it? So here the point is that you display the "child" models (comment, reply) on the "parent" model's (post) page. I have hard time figuring out the other way. I will write a new question for that. – Sean Magyar Mar 31 '16 at 20:09
  • Done! I never cache on the specific ids when caching the full list of objects if I'm getting the latest `updated_at`. This because the latest `updated_at` will change at the same time if I'm getting new posts with new ids and then cache key will change anyway. – Jacob Rastad Mar 31 '16 at 20:38
  • However if the object list is sortable by the user, then you have to take that into account but that is a bit outside the scope of the question. – Jacob Rastad Mar 31 '16 at 20:39
  • Here is what I meant http://stackoverflow.com/questions/36343150/rails-4-cache-expiration-not-working. Btw., why do you apply `updated_at` twice in the key? – Sean Magyar Mar 31 '16 at 20:49
  • It's actually two different `updated_at`, the first one is the newest from posts and the second one is the newest updated_at from the users who have written the posts. And if either of them changes, the cache will expire and forced to be updated from the DB with the new data. I also answered your linked question :) – Jacob Rastad Mar 31 '16 at 21:12
  • Jacob the answer to the linked question is clear, thanks a lot. This one though still causes some confusion. Why do you have to check the newest from posts, why is not enough the second one? And why do you have to use the `try` method in this case? – Sean Magyar Mar 31 '16 at 21:34
  • I need both the newest from posts and from users so that if I create or edit a post or if I edit a user the cache invalidates. If I only have the newest user, the cache won't expire if I create a new post. The `try` method is a form of best-practice when calling a function on a related model (IMHO), if for some reason we have a post in the DB with a nil reference to a `user` I will get a 500-error if I don't have `try`, – Jacob Rastad Mar 31 '16 at 21:41
  • Okay, now it's clear, thanks a lot. I see why you would use try on the updated_at. However, for that I prefer using null constraint on the db. Or you think I should use both? – Sean Magyar Mar 31 '16 at 21:47
  • Null constraints on the DB is really good, but it could be that the user is deleted from the DB and then you would still get a 500 because post.user would return nil (even if there is a value in the column user_id). If you have foreign key constraints in the DB stating that a user cannot be removed if there are posts linked then it's more of a question of taste, I like the peace-of-mind with try ;) – Jacob Rastad Mar 31 '16 at 21:56
  • Jacob, the reason you are not using `updated_at` on the `post_comment.user` and the `post_comment_reply.user` is the `touch: true` in the model? If so, why can't we also use `belongs_to :user, touch: true` on the `post`? – Sean Magyar Apr 01 '16 at 15:39
  • I am actually using `updated_at`on `post_comment.user`and `post_comment_reply.user`. When you add an ActiveRecord object in the `cache` function call, Rails will call a function called `cache_key` on the object which in turn uses `updated_at` to return the string used for creating the key. Adding `touch: true` between `post` and `user` is in this case counterproductive, this would lead to the cache for **all** of the user's posts being invalidated each the user creates or edits one `post`. – Jacob Rastad Apr 05 '16 at 15:35
  • Thanks Jacob again. Could you also take a look at this question? http://stackoverflow.com/questions/36460001/rails-leaving-out-authorized-parts-from-fragment-caching You seem to be pretty good at caching, so would be great to see your solution. – Sean Magyar Apr 06 '16 at 19:58