Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Low level caching for collection

I want to use Redis to do some low level caching in my Rails app. In a controller I normally us this to get all books:

class BooksController < ApplicationController
  def index
    @books = Book.order(:title)
  end
end

And the view iterates over this:

<ul>
  - @books.each do |book|
    <li>= "#{book.title} - #{book.author}"</li>
</ul>

Now I want the exact same result but then cached. I have Redis setup and running. So should I use a cached_books method in the controller like this:

@books = Book.cached_books.order(:title)

And leave the view as it is, or use book.cached_title and book.cached_author in the view and leave the controller as it is?

And how would a cached_books method look like in the Book model?

class Book < ActiveRecord::Base
  ...

  def cached_books
    Rails.cache.fetch([self, "books"]) { books.to_a }
  end
end

For simplicity sake I leave out expire strategies for now, but obviously they need to be there.

like image 995
John Avatar asked Jun 04 '15 15:06

John


People also ask

What is Russian doll caching?

You may want to nest cached fragments inside other cached fragments. This is called Russian doll caching. The advantage of Russian doll caching is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment.

When should you cache data?

Caches are generally used to keep track of frequent responses to user requests. It can also be used in the case of storing results of long computational operations. Caching is storing data in a location different than the main data source such that it's faster to access the data.


1 Answers

So should I use a cached_books method in the controller like this:

Yes, you can. Although there's some gotchas you have to be aware of. Book is ActiveRecord. When you call Book.something (e.g. Book.all, or just even Book.order(:title) it returns you a ActiveRecord::Relation, which is basically wrapper for array of Books (this wrapper prevents of firing unnecessary queries, boosting perfomance).

You can't save the whole result of a query in Redis. Say, you can save a JSON string of an array of hashes with model attributes, e.g.

[{
  id: 1,
  title: 'How to make a sandwich",
  author: 'Mr. cooker'
}, {
  id: 2,
  title: 'London's Bridge',
  author: 'Fergie'
}]

And then you can 'decrypt' this thing into the array after. Something like

def cached_books(key)
  # I suggest you to native wrapper
  if result = $redis.hget 'books_cache', key
    result.map do { |x| Book.new(x) }
  end
end

And also, you will have to serialise attributes before putting them into the cache.

Ok, now you have collections which can be iterated in the view with same data, although you can't call order on a cached collection, as it is a plain array (you may call sort, but the idea is to cache already sorted data).

Well... does it worth it? Actually, not really. If you need to cache this piece – probably, best way is to cache a rendered page, not a query result.

Should you use cached_title and cached_author – that's the good question. First of all, it depends on what cached_title may be. If it is a string – there's nothing you can cache. You get a Book through DB request, or you get Book from cache – in any way the title will be presented in it, as it is a simple type. But let's look closer to the author. Most likely it is going to be a relation to another model Author and this is the place where cache suits perfectly great. You can re-define author method inside the book (or define new and avoid nasty effects Rails may have in complex queries in the future) and see if there's a cache. If yes, return the cache. If not – query the DB, save result to the cache and return it.

def author
  Rails.cache.fetch("#{author_id}/info", expires_in: 12.hours) do
    # block executed if cache is not founded
    # it's better to alias original method and call it here
    #instead of directly Author.find call though
    Author.find(author_id) 
  end
end

Or less convenient, but more "safe":

def cached_author
  Rails.cache.fetch("#{author_id}/info", expires_in: 12.hours) do
    author
  end
end
like image 59
shlajin Avatar answered Sep 18 '22 13:09

shlajin