Memcacheable

A Rails concern to make caching ActiveRecord objects as dead-simple as possible. Uses the built-in Rails.cache mechanism, and implements the new finder methods available in Rails 4.0 to ensure maximum future compatibility.
Installation
Add this line to your application's Gemfile:
gem 'memcacheable'
And then execute:
$ bundle
Or install it yourself as:
$ gem install memcacheable
Usage
Let's do some caching!
class Person < ActiveRecord::Base
include Memcacheable
end
Boom! Now you can fetch a person by their id, like below. When the person gets updated or touched, it will flush the cache, and the person will be reloaded on the next fetch.
person = Person.fetch id # caches the person
person.touch # flushes the cache
person = Person.fetch id # the cache misses, and the person is reloaded
Cache by criteria
That's easy-sauce. Time to step up our caching game! "I want to cache queries by criteria, not just id's!" No probs:
class Person < ActiveRecord::Base
include Memcacheable
cache_index :name
cache_index :height, :weight
end
Mathematical! cache_index adds these index combinations to the list of cacheable things, so we can fetch single records with fetch_by, like this:
person = Person.fetch_by name: 'Scott' # caches an awesome dude
person.update_attributes name: 'Scottie' # flushes the cache
person = Person.fetch_by name: 'Scott' # => nil (he's got a new identity!)
# You can also do multiple criteria, and order doesn't matter.
person = Person.fetch_by weight: 175, height: 72
person.update_attributes height: 71 # he shrunk? oh well, cache flushed
person = Person.fetch_by weight: 175, height: 71 # fetched and cached with new height
Like noise in your life? Try fetch_by! (hard to say: "fetch-by-bang!").
person = Person.fetch_by! name: 'Mork' # => raises ActiveRecord::RecordNotFound
While fetch_by just pulls back just one record, you can fetch a collection with fetch_where:
people = Person.fetch_where weight: 42, height: 120 # => an array of tall, skinny people
people.first.update_attributes weight: 43 # one guy gained a little weight --> cache flushed
people = Person.fetch_where weight: 42, height: 120 # => an array of everyone but that first guy
Trying to fetch_by or fetch_where by criteria that you didn't specify with cache_index will raise an error, because Memcacheable won't know how to bust the cache when things get changed. For example:
Person.fetch_by name: 'Scott' # good
Person.fetch_by favorite_color: 'red' # shame on you! this wasn't cache_index'd!
Caveat: hash-style criteria is currently required for fetch_by and fetch_where. Something like person.fetch_where 'height < ?', 71 will raise an error.
Btw, don't do something stupid like trying to call scope methods on the result of a fetch_where. It returns an Array, not an ActiveRecord::Relation. That means this will blow up on you:
Person.fetch_where(height: 60).limit(5)
I may fix this later, though, because I like scopes.
Cache associations
If you love Rails, then you know you love ActiveRecord associations. Memcacheable loves them too. Check this out:
class Person < ActiveRecord::Base
has_one :dog
has_many :kittens
include Memcacheable
cache_has_one :dog
cache_has_many :kittens
end
class Dog < ActiveRecord::Base
belongs_to :person, touch: true
include Memcacheable
cache_belongs_to :person
end
class Kitten < ActiveRecord::Base
belongs_to :person, touch: true
include Memcacheable
cache_index :person_id
end
Notice the touch: true above. That's important to bust the parent cache when a child record is updated!
So what do we get with all of this caching magic? Why a bunch of dynamic association fetch methods of course! Observe:
dog = person.fetch_dog # his name's Bruiser, btw
dog.update_attributes name: 'Fido' # flushes the cached association on the person
dog = person.fetch_dog # finds and caches Fido with his new name
dog.fetch_person # gets the cached owner
For a slight optimization, specify a cache_index on the foreign key of the association, like in the Kitten example above. Memcacheable will then do a fetch_by or fetch_where as appropriate. The cost: two copies in the cache. The gain: when the parent changes but the children don't, the children can be reloaded from the cache. Like this:
person.fetch_kittens # caches the kittens both by criteria and as an association
person.touch # association cache is flushed, but not the fetch_where
person.fetch_kittens # reloads the kittens from the cache, and caches as an association
Inspiration
None of the caching options out there really satisfied my needs, so I wrote this gem. But I was not without inspiration. I learned the basics of Rails caching from the RailsCasts episode on Model Caching, and I borrowed a lot of syntax from the very popular IdentityCache gem from our friends at Shopify.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request