2

I have a rails 4 app using pundit gem for authorization. If I do russian-doll fragment caching like the code below, the conditional statement used for authorization will be also cached, which is not good, since edit/delete buttons should only be available for the post.user.

What is the good way to get around this? Should I split the cache into smaller parts or is there a way to exclude some parts of the caching? What's the rails convention in this case?

index.html.erb

<% cache ["posts-index", @posts.map(&:id), @posts.map(&:updated_at).max, @posts.map {|post| post.user.profile.updated_at}.max] do %>
  <%= render @posts %>
<% end %>

_post.html.erb

<% cache ['post', post, post.user.profile ] do %>

  <div class="row>
    <div class="col-md-2">
      <%= link_to user_path(post.user) do %>
        <%= image_tag post.user.avatar.url(:base_thumb), class: 'post-avatar' %>
      <% end %>
    </div>

    <div class="col-md-8">
      <span class="post-user-name"><%= post.user.full_name %></span>
      <span class="post-updated"><%= local_time_ago(post.updated_at) %></span>
      <div class="post-body">
        <%= post.body %>
      </div>

    <div class="col-md-2" style="text-align:right;">

      <!--############### THIS IS THE PART THAT SHOULD NOT BE CACHED #############-->

      <% if policy(post).edit? && policy(post).delete? %> 
        <li class="dropdown">
          <ul class = "dropdown-menu dropdown-menu-right">
            <li>
              <%= link_to "Edit Post", edit_post_path(post), remote: true, type: "button", 'data-toggle' => "modal", 'data-target' => "#updatepost_#{post.id}" %>
            </li>   
            <li>
              <a href="#" data-toggle="modal" role="button" data-target="#deletepost_<%= post.id %>">Delete Post</a>
            </li>
          </ul>
        </li>
      <% end %>

      <!--########################## UNTIL HERE ############################-->

    </div>
  </div>

  <div class = "row comment-top-row" style="padding-bottom:10px;">
    <div class="col-md-12 post-comment-form">
      <%= render partial: 'posts/post_comments/post_comment_form', locals: { post: post } %>
    </div>
  </div>

  <div class = "row">
    <div class="col-md-12 post-comment-insert-<%= post.id%>">
      <%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
    </div>
  </div>

  <% if policy(post).edit? %>
    <div class="modal fade updatepost" id="updatepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
       <!-- FORM GETS RENDERED HERE VIA JS -->
    </div>
  <% end %>

  <% if policy(post).delete? %>
    <div class="modal fade" id="deletepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
      ......
    </div>
  <% end %>

<% end %>
Sean Magyar
  • 2,360
  • 1
  • 25
  • 57

1 Answers1

4

Russian Doll Caching is simple but handy way for caching, there's no complex options or convention to exclude part of fragment from it. Above that, It's more relative to cache strategies. Here are two strategies for this user specific situation:

  1. Rearrange individually and Cache fragments manually, which I don't recommend. Because it's more complex and doesn't leverage the advantages of Russian Doll Caching. Not so maintainable as well. Here is an example:

index.html.erb

<% # pull out cache %>
<%= render @posts %>

_post.html.erb

<% cache post %>
  <%= # first part %>
<% end %>

<% # without cache %>
<%= # user specific part %>

<% cache post %>
  <%= # third part %>
<% end %>
  1. Preferred way: Add current_user as part of cache_key, which means you will have as many fragment caches as approximately your users and the fragments will automatically invalidate whenever the post or the user has changed their fingerprint. This is more elegant and maintainable. Here is an example:

index.html.erb

<% cache ["posts-index", @posts.map(&:id), @posts.map(&:updated_at).max, @posts.map {|post| post.user.profile.updated_at}.max] do %>
  <%= render @posts %>
<% end %>

_post.html.erb

<% cache ['post', post, post.user.profile, current_user ] do %>
  <div class="row>
    <div class="col-md-2">
      <%= link_to user_path(post.user) do %>
        <%= image_tag post.user.avatar.url(:base_thumb), class: 'post-avatar' %>
      <% end %>
    </div>

    <div class="col-md-8">
      <span class="post-user-name"><%= post.user.full_name %></span>
      <span class="post-updated"><%= local_time_ago(post.updated_at) %></span>
      <div class="post-body">
        <%= post.body %>
      </div>

    <div class="col-md-2" style="text-align:right;">
      <% if policy(post).edit? && policy(post).delete? %> 
        <li class="dropdown">
          <ul class = "dropdown-menu dropdown-menu-right">
            <li>
              <%= link_to "Edit Post", edit_post_path(post), remote: true, type: "button", 'data-toggle' => "modal", 'data-target' => "#updatepost_#{post.id}" %>
            </li>   
            <li>
              <a href="#" data-toggle="modal" role="button" data-target="#deletepost_<%= post.id %>">Delete Post</a>
            </li>
          </ul>
        </li>
      <% end %>
    </div>
  </div>

  <div class = "row comment-top-row" style="padding-bottom:10px;">
    <div class="col-md-12 post-comment-form">
      <%= render partial: 'posts/post_comments/post_comment_form', locals: { post: post } %>
    </div>
  </div>

  <div class = "row">
    <div class="col-md-12 post-comment-insert-<%= post.id%>">
      <%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
    </div>
  </div>

  <% if policy(post).edit? %>
    <div class="modal fade updatepost" id="updatepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
       <!-- FORM GETS RENDERED HERE VIA JS -->
    </div>
  <% end %>

  <% if policy(post).delete? %>
    <div class="modal fade" id="deletepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
      ......
    </div>
  <% end %>
<% end %>
abookyun
  • 457
  • 5
  • 13
  • abookyun, is this second way good enough? Won't the caching lose its purpose? I mean as you mentioned the fragment will be rewritten for every single user. Besides that, won't it consume too much memory in the long run? – Sean Magyar Apr 18 '16 at 14:44
  • Pls also read my prev comment. I forgot to mention (maybe you saw) that I have `post_comment` nested into the `post` and `post_comment_reply` nested into `post_comment`. Those also should be editable only by the `current_user`, so those also have some parts visible only to the `current_user`. Do I have to change those like this or `touch` takes care of that? – Sean Magyar Apr 18 '16 at 15:03
  • @SzilardMagyar IMHO: As I said, It's a strategy issue, only if there's opportunity to save heavy requests like nested post and comments query, I think it's good enough, in contrast, if the template vary frequently, you need another template caching structure. You could read this article to feel how caching affecting your app speed https://signalvnoise.com/posts/3690-the-performance-impact-of-russian-doll-caching – abookyun Apr 19 '16 at 10:37