RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

Dirty Checking of Callbacks in Rails

Summary: Dirty checking is a way of tracking changes by checking a variable’s value against what that variable’s value was. The Ruby on Rails ActiveModel::Dirty module enables dirty checking. This is useful for running ActiveRecord callbacks only when an attribute of an active model has changed.



I first heard the phrase “dirty checking” only recently, as I began exploring front end frameworks. Dirty checking is inherent to frameworks like Angular JS, in which a listener asynchronously checks its value against its previous value (thus knowing if something has changed and needs to be re-rendered).

Little did I know that dirty checking is possible (and valuable) in Ruby as well.

Background

(To skip the background context and jump straight to the technical explanation, click here).
In a recent pairing session, I was asked to implement a feature for a Q&A polling application. Imagine the application depicted below:

Now imagine yourself on the flip side, not as a user answering a poll, but as an admin at the portal implementing a poll, creating new questions, and populating prospective choices. This was the context for my challenge.

The challenge broke down into two parts:

  1. Front end: Add a couple options, in addition to populating the required choices with text.
    • A ‘randomize’ checkbox, which would mix up the order in which choices displayed.
    • A ‘special choices’ option, which could either be ‘Other’ or ‘None of the Above’.
  2. Back end: Reflect any changes by persisting to the database.

The front end feature was straightforward enough: add a checkbox option for randomization, and a radio button. Something as simple as the above right.

Code Example

Imagine the class below. In creating an answer as an administrator, we create an answer object with multiple choices (take my word for it that there was a lot more going on than this):

(answer_choices.rb) download
1
2
3
4
5
6
7
8
class Answer
  attr_accessor :choices

  include Mongoid::Document

  # ...

end

We need to add an attribute for :special_answer—easy enough. But how do we ensure that our new special_answer becomes an element in the array of our answer object’s choices? A simple append method. And how do we ensure things are saved as necessary? A simple callback: before_save :add_special_answer.

(answer_choices_2.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Answer
  attr_accessor :choices
  attr_accessor :special_answer

  include Mongoid::Document
  embeds_many :special_answers

  before_save :add_special_answer_to_choices

  def add_special_answer_to_choices
    choices << special_answer
  end

  # ...

end

One problem:each time we change the special answer, we’ll get an additional answer.

There should only ever be one special answer, so we need the new special answer to replace the previous one rather than continuing to add additional ones. And that’s a job that dirty checking can handle.

(answer_choices_3.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Answer
  attr_accessor :choices
  attr_accessor :special_answer

  include ActiveModel::Dirty

  include Mongoid::Document
  embeds_many :special_answers

  before_save :add_special_answer_to_choices, if: special_answer_changed?

  def add_special_answer_to_choices
    choices << special_answer
  end

  # ...

end

The difference here? That simple if: special_answer_changed? following the callback, as well as include ActiveModel::Dirty. The ActiveModel::Dirty module gives us a few key methods, most notable changed?. As the code above illustrates, we can make the callback conditional by dirty checking whether the special_answer attribute has changed.

ActiveModel::Dirty

You can view the module’s documentation in its entirety here. By its own definition, it “Provides a way to track changes in your object in the same way as Active Record does.” This way of tracking changes is helpful to us in this case—performing a callback only if the attribute has changed—and likely has applications in many other situations.

Resources