Rails tips: #with_options

by-rails, ruby

Another handy Rails method: #with_options

So let's say you have the following (contrived) model:

class User
  has_many :tasks, class_name: "UserTask", dependent: :destroy
  has_many :addresses, dependent: :destroy
  has_many :todos, as: :creator
  has_one  :subscription, dependent: :destroy
end

So when we delete a User, we also delete their associated items. However, there's a bug in our code - we've missed a dependent: :destroy for the todos associations, so we're going to be left with zombie todos floating around our database, potentially raising exceptions if a user tries to access them.

DRY for Hashes

So it'd be nice to have some way to DRY up our declaration that we want associated models destroyed when we destroy a User, and avoid more bugs from forgetting to add the necessary options. Enter #with_options:

Including in the methods ActiveSupport adds to Object, #with_options takes a hash and a block. Within the block, you can make further method calls that take hashes, and #with_options will merge these hashes with the original hash.

With that in mind, we can now refactor our User class:

class User
  with_options dependent: :destroy do |options|
    options.has_many :tasks, class_name: "UserTask"
    options.has_many :addresses
    options.has_many :todos, as: :creator
    options.has_one  :subscription
  end
end

Since #with_options is defined on Object, you can use it pretty much anywhere in your Rails codebase.

It blows up when I use a lambda!

There's an unfortunate bug if you use #with_options with a has_many association that has an additional scope:

class User < ActiveRecord::Base
  with_options dependent: :destroy do |options|
    options.has_many :addresses, -> { where(active: true) }
    # ...further associations...
  end
end

Rails will throw an error because it tries to call Hash methods on the lambda. Fortunately, the issue has been reported and was fixed in Rails v4.1.2. If you're using an older version, simply pass an empty hash as your last option to has_many and all will be well:

class User < ActiveRecord::Base
  with_options dependent: :destroy do |options|
    options.has_many :addresses, -> { where(active: true) }, {}
    # ...further associations...
  end
end

Photo by Daniel Petzold Photography on Flickr

Thanks for reading. To continue the discussion contact me: or

Related posts