Skip to main content

Mobomo webinars-now on demand! | learn more.

Rails ActiveRecord models have a lifecycle that developers are allowed to hook into. But while most of us know about before_save and after_update, there are a few lesser unknown callbacks that are good to know about before you reinvent them. In this post, I'll cover all of the available ActiveRecord lifecycle callbacks, and also show how you can define custom callbacks for normal ruby objects.

Meet the Callbacks

The Rails guide for ActiveRecord Validations and Callbacks is a good starting point for an introduction of the available callbacks and what they do. Most developers will be familiar with the validation and persistence callbacks, so let's start with these

:before_validation, :after_validation :before_save, :after_save :before_create, :after_create :before_update, :after_update :before_destroy, :after_destroy 

The callbacks above are self explanatory and commonly used, but if you're unfamiliar with them, or need a refresher, check out the Rails guide on the topic.

Around Callbacks

For save, create, update, and destroy, Rails also gives extra helper methods for defining both a before and after save callback at the same time.

For example, suppose you wanted to trigger your own custom callback while a model was being destroyed. You can do so by defining and triggering your own callback as follows:

class SomeModel < ActiveRecord::Base   define_callbacks :custom_callback    around_destroy :around_callback    def around_callback     run_callbacks :custom_callback do       yield  # runs the actual destroy here     end   end end 

Custom Callbacks without ActiveRecord

Most of the time, your Rails models will be using ActiveModel, but sometimes it makes sense to use a plain old ruby object. Wouldn't it be nice if we could define callbacks in the same way? Fortunately, the callback system is neatly abstracted into ActiveSupport::Callbacks so it's easy to mix into any ruby class.

# Look Ma, I'm just a normal ruby class! class Group   include ActiveSupport::Callbacks   define_callbacks :user_added    def initialize(opts = {})     @users = []   end    # Whenever we add a new user to our array, we wrap the code   # with `run_callbacks`. This will run any defined callbacks   # in order.   def add_user(u)     run_callbacks :user_added do       @users << u     end   end end 

For a fully documented and runnable example, check out this github project. It'll also give some extra explanation about call order and inheritance.

Other Useful Callbacks

  • :after_initialize is called right after an object has been unmarshalled from the database. This allows you to do any other custom initialization you want. Instead of defining an initialize method on a model, use this instead.
  • :after_find hasn't been useful in my experience. I haven't run into a case where I wanted to manipulate documents after a find action. It could potentially be useful for metrics and profiling.
  • :after_touch. ActiveRecord allows you to touch a record or its association to refresh its updated_at attribute. I've found this callback useful to triggering notifications to users after a model has been marked as updated, but not actually changed.
  • :after_commit is an interesting and tricky callback. Whenever ActiveRecord wants to make a change to a record (create, update, destroy), it wraps it around a transaction. after_commit is called after you're positive that something has been written out to the database. Because it is also called for destroys, it makes sense to scope the callback if you intend to use it only for saves. Be warned that after_commit can be tricky to use if you're using nested transactions. That'll probably be the topic of another post though.
# call for creates, updates, and deletes after_commit :all_callback  # call for creates and updates after_commit :my_callback, :if => :persisted? 
  • :after_rollback is the complement to after_commit. I haven't used it yet, but I can see it as being useful for doing manual cleanup after a failed transaction.

Go Forth and Callback!

While many of our models will be backed with ActiveRecord, or some ActiveModel compatitible datastore, it's nice to see how easy it is to follow a similar pattern in normal ruby without having to depend on Rails.

Categories
Author

When you use Rails built-in helpers for page, action, and fragment caching, the cached data is stored into an instance of ActiveSupport::Cache::Store. But while the interface for using the cache stores are the same, each cache store implementation has different performance characteristics and are suited for different jobs. In this post, I'll cover what cache stores are available with Rails by default, how to tune them, and how to write your own custom cache store.

Grabbing the Code

If you want to follow along in the code, I recommend cloning the Rails repository. The cache related code I'm covering all live within activesupport/lib/cache.rb and activesupport/lib/cache folder. The corresponding tests live in activesupport/test/caching_test.rb.

Introducing the Abstract Store

All cache store implementations inherit from an abstract store named ActiveSupport::Cache::Store. This class defines the public interface that's used by Rails to do caching. The three basic operations are read, write, and delete. Here's a simple example you can run in irb:

require 'activesupport' cache = ActiveSupport::Cache.lookup_cache(:memory_store) cache.read('foo') => nil cache.write('foo', 'bar') cache.read('foo') => 'bar' cache.delete('foo') cache.read('foo') => nil 

After requiring activesupport, we ask for an instance of a MemoryStore (we'll cover the different store types later in this post). The interface for read, write, and delete are self explanatory. You can also customize the behavior of these actions.

Store Implementations

The concrete store implementations are well documented, so I'll introduce them briefly here and leave the details to the documentation.

  • MemoryStore - a cache store that stores data in a plain-old Ruby hash. As an added feature, it keeps track of cache access and cache size, and will prune the cache when it hits a customizable max size.

  • FileStore - a cache store that stores data on the filesystem. It also caches multiple read calls within the same block in memory to decrease I/O.

  • MemCacheStore - the big daddy of cache stores. Backed by memcached, this store allows you to specify multiple memcached servers and load balances between them.

  • NullStore - Interesting cache store that does nothing. That's right, if you look at its implementation, it's just a bunch of empty methods. It's perfect for use as a mock cache store for testing, or as a template for writing your own cache store.

Rails Initialization

By default, Rails 3 will initialize a FileStore that you can reference through Rails.cache. This cache is used internally by Rails to cache classes, pages, actions, and fragments. If you want to change which cache store is used, you can configure it in your application.rb

# use a MemoryStore cache with a max size of 512 megabytes config.cache_store = [:memory_store, {:size => 536870912}] 

In production mode, Rails will also insert Rack::Cache to the top of the middleware stack and use Rails.cache as its storage. Note that even though Rack::Cache's heap storage does not bound the size of its cache, if you use ActiveSupport's MemoryStore, the least recently used entries will be pruned from the cache when it hits your specified limit. So if you set correct cache headers, Rack::Cache will pick them and cache your responses.

Writing A Custom Cache Store

The default cache stores are a perfect fit for most situations, but if you do need to write a custom cache store, rest assured that it's easy to do.

The three main methods to override are:

# Read an entry from the cache implementation. Subclasses must implement this method. def read_entry(key, options) # :nodoc:   raise NotImplementedError.new end  # Write an entry to the cache implementation. Subclasses must implement this method. def write_entry(key, entry, options) # :nodoc:   raise NotImplementedError.new end  # Delete an entry from the cache implementation. Subclasses must implement this method. def delete_entry(key, options) # :nodoc:   raise NotImplementedError.new end 

These methods are then used by the public interface methods. There are a few methods you can optionally implement, but your cache will work with just the three listed above.

For a client project, I wrote a write-through cache store called CascadeCache that chains multiple cache stores together. For example, here's one possible configuration:

config.cache_store = [:cascade_store, {   :stores => [     [:memory_store, :size => 5.megabytes],     [:memcache_store, 'somehost:11211']   ] }] 

The behavior of this cache store is to return the first hit from the list of caches. This allows the app to have a small low-latency MemoryStore in front of a MemCacheStore. If something can't be found in the MemoryCache, then we fall back to MemCache. When writing to the cache, entries are written through to both underlying cache stores. The primary reason for doing this wasn't because MemCache store was slow, but as an extra backup cache in case MemCache became temporarily unavailable (actually happened in production).

I'm hoping CascadeCache makes it upstream into ActiveSupport, but in the meantime, I've packaged it up as a separate gem. For another example of a custom cache implementation, check out redis-store. It includes an ActiveSupport compatible cache.

Caching is a tricky beast. On top of deciding what to cache and when to expire, the underlying cache store can affect your app's performance. Choose the cache store that best fits your needs, use a hybrid CascadeCache, or write your own. Good luck and happy tuning!

Categories
Author
1
Subscribe to Activesupport