简体   繁体   中英

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.

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 (eg Book.all , or just even Book.order(:title) it returns you a ActiveRecord::Relation , which is basically wrapper for array of Book s (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, eg

[{
  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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM