Skip to content

Batch up your ActiveRecord "touch" operations for better performance.

License

Notifications You must be signed in to change notification settings

ProductPlan/activerecord-batch_touching

Repository files navigation

ActiveRecord::BatchTouching

Gem Version Build Status Maintainability Test Coverage

Batch up your ActiveRecord "touch" operations for better performance.

This gem is derivative of activerecord-delay_touching and @BMorearty's subsequent PR to merge this functionality into Rails with rails/rails#18824.

Why?

Doesn't ActiveRecord already consolidate touches?

Yes, and no! Let's dig in!

The examples below build upon the following setup:

class Person < ActiveRecord::Base
  has_many :pets
  accepts_nested_attributes_for :pets
end

class Pet < ActiveRecord::Base
   belongs_to :person, touch: true
end

One touch per object

Just like with the current ActiveModel functionality, batch_touching will prevent this simple update in the controller from calling@person.touch N times, where N is the number of pets that were updated via nested attributes. That's N-1 unnecessary round-trips to the database:

class PeopleController < ApplicationController
  def update
    ...
    @person.update(person_params)
    ...
  end
end

# SQL (0.1ms)  UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.137158' WHERE "people"."id" = 1
# SQL (0.1ms)  UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.138457' WHERE "people"."id" = 1
# SQL (0.1ms)  UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "people"."id" = 1

With batch_touching, @person is touched only once:

@person.update(person_params)
# SQL (0.1ms)  UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "people"."id" = 1

Nothing to see here! The next two sections are where this gem differentiates itself from the current ActiveRecord implementation.

Consolidate Touches Per Table

In the following example, a person gives his pet to another person. ActiveRecord automatically touches the old person and the new person. The current ActiveRecord implementation has a SQL UPDATE per individual record touched. With batch_touching, this will only make a single round-trip to the database, setting updated_at for all Person records in a single SQL UPDATE statement. Not a big deal when there are only two touches, but when you're updating records en masse and have a cascade of hundreds touches, it really is a big deal.

class Pet < ActiveRecord::Base
  belongs_to :person, touch: true

  def give(to_person)
    self.person = to_person
    save! # touches old person and new person in a single SQL UPDATE.
  end
end

Deadlock Prevention

batch_touching will sort the consolidated SQL updates by model name, and then commit touches in their own transaction. The separate transaction in a predictable order for updates should help mitigate potential database deadlocking.

For example, if two transactions happen to touch records in the following order, there is a potential for a deadlock:

Transaction 1:

ActiveRecord::Base.transaction do
  person1.touch
  pet1.touch
end
# SQL (0.1ms)  UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "people"."id" = 1
# SQL (0.1ms)  UPDATE "pets" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "pets"."id" = 1

Transaction 2:

ActiveRecord::Base.transaction do
  pet1.touch
  person1.touch
end
# SQL (0.1ms)  UPDATE "pets" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "pets"."id" = 1
# SQL (0.1ms)  UPDATE "people" SET "updated_at" = '2014-07-09 19:48:07.140088' WHERE "people"."id" = 1

batch_touching will have both transactions update in the same order, regardless of the order .touch was called.

It's dangerous to go alone! Take batch_touching.

Installation

Add this line to your application's Gemfile:

gem 'activerecord-batch_touching'

And then execute:

$ bundle

Or install it yourself:

$ gem install activerecord-batch_touching

Usage

Once installed, all transactions will automatically have batch_touching enabled.

Other tidbits

Some additional information or gotchas to be aware of!

Cascading Touches

When batch_touching runs through and touches everything, it captures additional touch calls that might be called as side-effects. (E.g., in after_touch handlers.) Then it makes a second pass, batching up those touches as well.

It keeps doing this until there are no more touches, or until the sun swallows up the earth. Whichever comes first.

Gotchas

  • after_touch callbacks are still fired for every instance, but not until the block is exited. As a result, the ordering of the callbacks may be different than the default ActiveRecord implementation.
  • If you call person1.touch and then person2.touch, and they are two separate instances with the same id, only person1's after_touch handler will be called.

Contributing

  1. Fork it ( https://github.com/ProductPlan/activerecord-batch_touching/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

Batch up your ActiveRecord "touch" operations for better performance.

Resources

License

Stars

Watchers

Forks

Languages