The 10 Most Underused ActiveRecord::Relation Methods

Knee-deep in ActiveRecord::Relation code yesterday, I was reminded of some interesting nuggets that I’ve seen used far too rarely. Here, I’ve gathered my top ten most underused relation methods from that list for your reading delight.

Note: This article was very popular in 2012 (~400 shares and tons of reads) and still has a lot of google juice. I’ve kept it up to keep from breaking links. However, this article is not up to date with recent changes in Rails 4 and 5. A great alternative resource for learning about Active Record is the official Rails Guide.

10. first_or_create with a block

first_or_create is very familiar:

Book.where(:title => 'Tale of Two Cities').first_or_create

and does exactly what it says. Often, though, you want to find a record with certain attributes, or create one with those and additional attributes. To do this succinctly, you can supply a block to first_or_create:

Book.where(:title => 'Tale of Two Cities').first_or_create do |book|
  book.author = 'Charles Dickens'
  book.published_year = 1859
end

9. first_or_initialize

If you don’t want to save the record yet, you can use first_or_initialize:

Book.where(:title => 'Tale of Two Cities').first_or_initialize

8. scoped

Sometimes you want an ActiveRecord::Relation representing all the records of a class. You can easily generate one using the scoped method:

def search(query)
  if query.blank?
    scoped
  else
    q = "%#{query}%"
    where("title like ? or author like ?", q, q)
  end
end

7. none (rails 4 only)

Likewise, sometimes you want an ActiveRecord::Relation that contains no objects. Returning an empty array is usually not a great idea if the consumer of your API is expecting a relation object. Instead, you can use none.

def filter(filter_name)
  case filter_name
  when :all
    scoped
  when :published
    where(:published => true)
  when :unpublished
    where(:published => false)
  else
    none
  end
end

Note: You have to be seriously living on the edge to use none right now. It will be available in rails 4, but not 3. It is easy to write your own in the meantime, though, checkout this stack overflow thread.

6. find_each

If you want to iterate over thousands of records, you probably don’t want to use each. It will execute a single query to get all the records, and then instantiate them all into memory. If you have enough memory to spare, go for it. Otherwise, this is a nice way to freeze up your Rails app! find_each instead finds a batch of records at a time (1000 by default) and yields those one at a time, so that you don’t have them all in memory at the same time.

Book.where(:published => true).find_each do |book|
  puts "Do something with #{book.title} here!"
end

Note that you can’t specify the order of records yielded by find_each. If you specify one on your relation, it will simply be ignored.

5. to_sql and explain

ActiveRecord is great, but it doesn’t always generate the queries you think it will. Jump in the console and run these commands on the relation you’re building, to make sure it maps to a smart query, or that it’s using the indices you lovingly crafted:

Library.joins(:book).to_sql
# => SQL query for you database.
Libray.joins(:book).explain
# => Database explain for the query.

4. find_by (rails 4 only)

Rails code tends to be littered with lines like:

Book.where(:title => 'Three Day Road', :author => 'Joseph Boyden').first

Instead, you can use the shortcut method find_by:

Book.find_by(:title => 'Three Day Road', :author => 'Joseph Boyden')

which does exactly the same thing.

Note: You have to be seriously living on the edge to use find_by right now. It will be available in rails 4, but not 3.

3. scoping

You can “scope” methods on a class to a particular relation. Consider the following example from the Rails documentation:

Comment.where(:post_id => 1).scoping do
  Comment.first # SELECT * FROM comments WHERE post_id = 1
end

This is all kinds of useful.

2. pluck

Want an array of column values for certain records? I’ve seen this too many times:

published_book_titles = Book.published.select(:title).map(&:title)

Or, even worse:

published_book_titles = Book.published.map(&:title)

Instead, use pluck:

published_book_titles = Book.published.pluck(:title)

1. merge

I couldn’t live without this jewel, but it’s strangely un-documented in the source, and not mentioned in any guide or book I’ve seen. Among other uses, it allows you to do a join, and filter by a named scope on the joined model:

class Account < ActiveRecord::Base
  # ...

  # Returns all the accounts that have unread messages.
  def self.with_unread_messages
    joins(:messages).merge( Message.unread )
  end
end