10

I recently read an article on the 37Signals blog and I'm left wondering how it is that they get the cache key.

It's all well and good having a cache key that includes the object's timestamp (this means that when you update the object the cache will be invalidated); but how do you then use the cache key in a template without causing a DB hit for the very object that you are trying to fetch from the cache.

Specifically, how does this affect One to Many relations where you are rendering a Post's Comments for example.

Example in Django:

{% for comment in post.comments.all %}
   {% cache comment.pk comment.modified %}
     <p>{{ post.body }}</p>
   {% endcache %}
{% endfor %}

Is caching in Rails different to just requests to memcached for example (I know that they convert your cache key to something different). Do they also cache the cache key?

5
  • Take a look at rossp.org/blog/2012/feb/29/fragment-caching for a Django example!
    – vdboor
    Commented Mar 8, 2012 at 16:10
  • I already had a look at that and that seems to suffer from exactly the same problem. The data he's trying to cache is required in order to access the cache. The only thing he seems to be saving on is in the inner expensive operation which is unlike most use cases for this type of caching. Commented Mar 13, 2012 at 9:11
  • That's true, an also happens with the 37signals code, it is focussed on the rendering code. The trick is to cache the whole list in another container too, or cache the retrieval of the object elsewhere.
    – vdboor
    Commented Mar 13, 2012 at 10:29
  • Actually their caching strategy seems a little bit more educated. I recommend this article as well: 37signals.com/svn/posts/…
    – JensG
    Commented Nov 25, 2013 at 17:35
  • It looks like your code snippet has a typo - was post.body intended to be comment.body?
    – Izkata
    Commented May 16, 2014 at 12:44

2 Answers 2

3
+100

For caching a straight dump of a single already-loaded object, yes, you gain nothing or next-to-nothing. That's not what those examples are describing - they're describing a hierarchy, where any change to something lower should also trigger an update to everything higher up in the hierarchy.

The first example, from the 37signals blog, uses Project -> Todolist -> Todo as the hierarchy. A populated example might look like this:

Project: Foo (last_modified: 2014-05-10)
   Todolist:  Bar1 (last_modified: 2014-05-10)
       Todo:  Bang1 (last_modified: 2014-05-09)
       Todo:  Bang2 (last_modified: 2014-05-09)

   Todolist:  Bar2 (last_modified: 2014-04-01)
       Todo:  Bang3 (last_modified: 2014-04-01)
       Todo:  Bang4 (last_modified: 2014-04-01)

So, let's say Bang3 was updated. All its parents also get updated:

Project: Foo (last_modified: 2014-05-16)
   Todolist:  Bar2 (last_modified: 2014-05-16)
       Todo:  Bang3 (last_modified: 2014-05-16)

Then when it comes time to render, loading Project from the database is basically inevitable. You need a point to start at. However, because its last_modified is an indicator of all its children, that's what you use as the cache key before attempting to load the children.


While the blog posts use separate templates, I'm going to lump them together into one. Hopefully seeing the complete interaction in one place will make it a little clearer.

So, the Django template might look something like this:

{% cache 9999 project project.cache_key %}
<h2>{{ project.name }}<h2>
<div>
   {% for list in project.todolist.all %}
   {% cache 9999 todolist list.cache_key %}
      <ul>
         {% for todo in list.todos.all %}
            <li>{{ todo.body }}</li>
         {% endfor %}
      </ul>
   {% endcache %}
   {% endfor %}
</div>
{% endcache %}

Say we pass in a Project whose cache_key still exists in the cache. Because we propagate changes to all related objects to the parent, the fact that that particular key still exists means the entire rendered contents can be pulled from the cache.

If that particular Project had just been updated - for example, as with Foo above - then it will have to render its children, and only then will it run the query for all Todolists for that Project. Likewise for a specific Todolist - if that list's cache_key exists, then the todos inside it have not changed, and the whole thing can be pulled from the cache.

Also notice how I'm not using todo.cache_key in this template. It's not worth it, since as you say in the question, body has already been pulled from the database. However, database hits aren't the only reason you might cache something. For example, taking raw markup text (such as what we type into question/answer boxes on StackExchange) and converting it to HTML may well take sufficient time that caching the result would be more efficient.

If that were so, the inner loop in the template might look more like this:

         {% for todo in list.todos.all %}
            {% cache 9999 todo todo.cache_key %}
               <li>{{ todo.body|expensive_markup_parser }}</li>
            {% endcache %}
         {% endfor %}

So to pull everything together, let's go back to my original data at the top of this answer. If we assume:

  • All the objects had been cached in their original state
  • Bang3 was just updated
  • We're rendering the modified template (including expensive_markup_parser)

Then this is how everything would be loaded:

  • Foo is retrieved from the database
  • Foo.cache_key (2014-05-16) does not exist in the cache
  • Foo.todolists.all() is queried: Bar1 and Bar2 are retrieved from the database
  • Bar1.cache_key (2014-05-10) already exists in the cache; retrieve and output it
  • Bar2.cache_key (2014-05-16) does not exist in the cache
  • Bar2.todos.all() is queried: Bang3 and Bang4 are retrieved from the database
  • Bang3.cache_key (2014-05-16) does not exist in the cache
  • {{ Bang3.body|expensive_markup_parser }} is rendered
  • Bang4.cache_key (2014-04-01) already exists in the cache; retrieve and output it

Savings from the cache in this tiny example are:

  • Database hit avoided: Bar1.todos.all()
  • expensive_markup_parser avoided 3 times: Bang1, Bang2, and Bang4

And of course, next time it's viewed, Foo.cache_key would be found, so the only cost to rendering is retrieving Foo alone from the database and querying the cache.

-2

Your example is good if it needs some data retrieval or processing for each comment. If you just take body and display it - cache will be useless. But you can cache all comments tree (including {% for %}). It this case you need to invalidate it with every added comment, so you can put last comment timestamp or comments count somewhere into Post and build comments cache key with it. If you prefer more normalized data, and use comments on only one page, you can just clear a cache key on comment save.

For me, saving comments count in Post looks good enough (if you don't allow to delete and edit comments) - you have a value to show anywhere with the Post and a key for caching.

Not the answer you're looking for? Browse other questions tagged or ask your own question.