Find or create by and its usages in Rails 3

Oct 20, 2012 -

One method that I frequently use in Rails is the find_or_create_by_* method ( If you can call it one method since it comes in so many variations ). As it implies, this method will try to find a record from one or many attributes and create it if it does not find it. But like so many other functions, it has undergone some changes along the way.

Using one attribute

This will try to find a User by its name and create one if not found.

User.all
=> [#<User id: 1, name: "Jake", age: 30>]

User.find_or_create_by_name("Jake")
=> #<User id: 1, name: "Jake", age: 30>

User.find_or_create_by_name("Joe")
=> #<User id: 2, name: "Joe", age: nil>

User.all
=> [#<User id: 1, name: "Jake", age: 30>,
    #<User id: 2, name: "Joe", age: nil>]

Using Multiple attributes

User.all
=> [#<User id: 1, name: "Jake", age: 30>]

User.find_or_create_by_name_and_age("Jake", 30)
=> #<User id: 1, name: "Jake", age: 30>

User.find_or_create_by_name_and_age("Jake", 31)
=> #<User id: 2, name: "Jake", age: 31>

User.find_or_create_by_name_and_age("Joe", 40)
=> #<User id: 3, name: "Joe", age: 40>

User.all
=> [#<User id: 1, name: "Jake", age: 30>,
    #<User id: 2, name: "Jake", age: 31>,
    #<User id: 3, name: "Joe", age: 40>]

One recent addition to this set of methods is with an exclamation mark at the end, for example find_or_create_by_name!. What this means is that if there was no record that existed and there was an error while trying to create the new record (maybe because of validation) then an error will be raised. That does not happen without the exclamation mark.

The reason I like that addition is that it makes it offers a better way to use it within transations. Say for example the scenario of creating a Blog Post record and want to add Tags to that new record. Then a very simple way of doing it in rails is to handle the Tag records in an after_save callback. But if there is a problem in the callback that causes the associated record not to be saved/created, then you might not want the first Post to be saved either. So by raising an error upon failed save, then the entire transation towards the database will be rolled back. That way you can be certain that there are no half-saved records in the database.

Here is an example of how that could look:

class Post < ActiveRecord::Base
  has_many :post_tags, dependent: :destroy
  has_many :tags,      through: :post_tags

  attr_writer: tag_names

  after_save do
    self.tags = tag_names.split(' ').map { |tag_name| Tag.find_or_create_by_name!(tag_name) }
  end

  ...
end