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.
post.bodyintended to becomment.body?