< Home

Why I love update_attributes

This week I am working on a project for mint digital and I came across something that inspired me to write my first of many blog posts. So without further a do lets go ahead and dive straight into some code.

To begin with I had the following models:

class Author < ActiveRecord::Base
  has_many :books
end

class Book < ActiveRecord::Base
  belongs_to :author
  # state => ['live', 'hold', 'sold']
end

Currently the author model does not have a ‘state’, I was in a situation where I wanted to be able to find out if an author was on 'sale’. By that I mean, if the author has any associated books with the state of 'live’. My first thought was, it would either involve a resource heavy loop (this is espcially true if the author has thousands of books), or it would involve a hard to implement and difficult to reuse SQL statement. Both of which did not seem particualaly apealing.

I decided the best thing to do is add a boolean field to the Author model named listed_for_sale, this field would then be updated each time a book was created, updated or deleted. Doing it this way would make it easy to find out if an author is on sale or not by simply calling listed_for_sale? which would return true/false.

So I added the field listed_for_sale to the author model:

rails g migration AddListedForSaleColumnToAuthor listed_for_sale:boolean
# I then added a default value by appending the below to the add_column call
default: false
rake db:migrate

Then I add the following to the models:

# app/models/author.rb

def update_listed_for_sale!
  state = books.any? { |book| book.state == 'live' }
  self.listed_for_sale = state
  self.save
end

# app/models/book.rb

after_commit :update_book_author

def update_book_author
  author.update_listed_for_sale!
end

The problem with the above code is that it will update the field regardless of whether the field has changed in value or not. So I needed to add a check to the update_listed_for_sale! method so that it is only updated when really necessary. I altered the method to look like the below:

# app/models/author

def update_listed_for_sale!
  state = books.any? { |book| book.state == 'live' }
  self.listed_for_sale = state
  self.save if self.listed_for_sale_changed?
end

I wrote a quick test and ran the test suite, and it worked successfully. However something didn’t feel right. I looked at the method on author, although it does exactly what it is meant to do, it just felt clunky or un ruby like. So I did some exploration and played around with the console. That is when I found update_attributes, I had used this previously to update the attributes of a model without having to explicitly call save. However there seems to be an awesome bit of sugar lying the the depths of the method which I don’t feel is documented well enough. It does the exact same check I am after. I.e. It will only update the field if the field has changed in value. That meant I was able to go ahead and reduce the method to one line:

# app/models/author.rb

def update_listed_for_sale!
  update_attributes(:listed_for_sale, books.any? { |book| book.state == 'live' })
end

Pretty neat right? So there you go, go ahead and use update_attributes whenever you can, it’s just one of many places where rails has got your back!