TL;DR Use the activerecord-delay_touching gem to speed up writes in your rails app by consolidating
touch calls into a single
UPDATE round-trip to the database.
Did you know that a single call to
ActiveRecord#save can result in hundreds of database round-trips?
In this post I’ll show you how to fix it without using
no_touching is fine for when you don’t care about tracking when your records were updated, but frequently you do need to track that. For example, when you are using fragment caching or Russian doll caching, you need
updated_at to be correct.
In the middle of last year, when I was consulting on a Spree project, I found that ActiveRecord’s
touch: true can result in an enormous number of redundant
UPDATE calls to the database. In my case there was potential to update hundreds of child objects in a single action, which resulted in hundreds of
touch calls to a single parent object—and this caused hundreds more cascading
touch calls to the parent’s parent. There was a noticeable effect on performance.
Although I encountered this problem in Spree, it is not Spree-specific. It is a general problem any time you use
touch: true and your controller can update many child objects in a single action.
A good example of this is
accepts_nested_attributes_for, which lets you mass-update attributes of child records in a
has_many relationship by making a single call to
ParentObject.save. But if the child class has
touch: true on the
belongs_to statement (e.g. for fragment caching), Rails calls
touch on the parent object N times—where N is the number of child objects. Another example is when giving the user a bunch of checkboxes so they can indicate which objects are related. (See HABTM Checkboxes and HABTM Checkboxes (revised)).
In all these cases, the parent object only needs to be touched once. Touching it lots of times slows down response time (yes I measured it) and puts unnecessary load on the database.
To solve it, I created a small gem called activerecord-delay_touching. Its API is modeled after the
no_touching method that was introduced in Rails 4.1. Except that instead of not touching, it batches up the touches into the smallest possible number of
ActiveRecord::Base.delay_touching do # ...code that results in `touch` being called... end
When the end of the block is reached, all the batched-up touches are reduced and executed. They occur synchronously. It does not run in a background processor. This is by design—running in the background would screw up subsequent requests that depend on
updated_at, such as in cache keys.
In addition, the gem consolidates touches across multiple rows in each table. For example, if
touch was called on two
Person objects with ids 1 and 2, it will make a single call to update updated_at where people.id in (1,2).
And, finally, it captures and reduces cascading touches. In other words if, while running through all the
after_touch calls, more touch calls occur, it batches them up and runs them together in a second pass.
It keeps doing this until there are no more touches, or until the sun swallows up the earth. Whichever comes first.
There are a couple of things to be aware of when using the gem. Read the “Gotchas” section of the README.
The gem is well-tested. It has thorough specs, and it has been used in production for six months.
If you’d like to use it or contribute, please do. Here’s the source. Check out the README for more examples.