Computers, Open-source, Ruby, Software, Web

Rails ActiveRecord Callbacks (Hooks)

Rails has some neat tricks up its sleeve when it comes to its ORM – ActiveRecord. One of the many things it does well is provide the ability to customize what happens at certain stages of the ActiveRecord transaction lifecycle. This means that you can have pre and post events that fire off when you create, save, or destroy records.

Here are the methods that are available inside ActiveRecord::Base derived models:

after_create, after_destroy, after_save, after_update, after_validation, after_validation_on_create, after_validation_on_update, before_create, before_destroy, before_save, before_update, before_validation, before_validation_on_create, before_validation_on_update

So when would you use this?:

Glad you asked! I recently had a need to implement such a thing when I was writing a new website.  In this website, a user can create, move, and delete tabs from their interface. The position of the tab was saved in a database table. When I would render the tabs to the user, I would just make my association with “:order => ‘position'”. Whatever position the tabs were ordered in, would show in the interface.

Adding a tab sounds easy (at first glance). You can just append the new tab to the end of the user’s tab listing. Something like this may be a good first run:

before_create :order_tab

def order_tab
  self.position = self.user.tab_layouts.count
end

Remember not to save your record in the callback methods, as this will cause an infinite loop, as it is saved, triggering the before_save event again, and so on. Before you know it, your CPU is hot enough to fry an egg.

However, we want to be flexible enough to allow the user to rearrange tabs as they please. If we keep this as is, no matter what is rearranged, when the save method is called, it will just override the new position again with the last position because of our code above. You may be thinking of using the before_create callback to get around this, however I wanted a more generic answer in creating / updating tab positions.

After toying around for a while, I came up with this solution:

before_create :order_tabs_on_create

def order_tabs_on_create
  self.position = self.user.tab_layouts.count if self.position.nil?

  ActiveRecord::Base.connection.execute("UPDATE tab_layouts
     SET POSITION = POSITION + 1
   WHERE user_id = #{self.user_id} AND POSITION >= #{self.position}")
end

Lets look at this line by line. First, we need to determine if a position has been set prior to saving. If it does not, then lets just throw it on the end (or wherever – it really won’t matter soon).

If a position has been specified, then we want to honor that location, and increment the positions of other tabs by one to allow room to insert the new tab. After we increment these positions (leaving a gap), our new tab will fill this in when the method ends, and is saved.

Just a side note, I could have used the “increment!” method instead of executing raw SQL to save the record. There is an “n+1” performance problem to consider. Simply, if a user inserts a new tab at the beginning of 100 other tabs, then you will have one insert statement, and 100 update statements to accomplish this callback magic.

So this will allow you to insert a tab at any location, and increment the positions of the tabs behind it so they are out of the way. Now, what happens when you destroy a tab? If the tab is on the end of the list, everything is fine, because the positions are still sequential. However, if we destroy a tab in the middle, then we have a gap in our positions. Lets take care of this with a new callback:

before_destroy :order_tabs_on_destroy

def order_tabs_on_destroy
  ActiveRecord::Base.connection.execute("UPDATE tab_layouts
       SET POSITION = POSITION - 1
     WHERE user_id = #{self.user_id} AND POSITION > #{self.position}")
end

This method will fire whenever a record is destroyed. Note the significant difference between calling tab.delete, and tab.destroy. If you just call “delete” it is gone, without ever invoking all of this callback magic. The ActiveRecord authors provide both methods to address performance concerns. Destroy is slower because it does run through all of these callbacks prior to deletion, creation, saving, etc.

Now, when we delete a tab, all tabs with positions higher than the current tab are destroyed as well.

Update:

After I wrote the original version of this code this morning, I realized that I didn’t take into account reordering tabs that were already added to a user’s layout. The next piece of code below handles this as well. Additionally, I have a sneaking suspicion that there is a more compact way to do this, but I just couldn’t get my head around all of that logic:

before_update :order_tabs_on_update

def order_tabs_on_update
  old_position = TabLayout.find(self.id).position
  if old_position  #{old_position} AND POSITION <= #{self.position}")
  else
    ActiveRecord::Base.connection.execute("UPDATE tab_layouts
     SET POSITION = POSITION + 1
   WHERE user_id = #{self.user_id} AND POSITION = #{self.position}")
  end
end
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s