Docunext


Advanced ActiveRecord Queries with Rails 3.1 Arel joins and merge

August 18th, 2011

I've been exploring Rails 3.1 for a couple weeks now and last night I focused on advanced queries with Active Record, utilizing some new scoping features thanks to Arel.

Note: I'm not fully versed in Arel as of yet, but as far as I understand, it's related to what I figured out last night.

I've written about scopes in Rails 3 before, but nowadays I'm keener on class methods, like this one:

1 class Entry < ActiveRecord::Base
...
 45   class << self
 46     def before_date(date)
 47       where(["date <= ?", date])
 48     end
 49   end
...
 92 end

In the above example, the before_date method will not be called until it is used, and it will change the SQL generated for the database query.

How about a more advanced example? Here's a sweet thing:

  1 class Transaction < Record
  2
  3   validates :amount_in_cents,
  4             :presence => true,
  5             :numericality => true
  6
  7   class << self
  8     def before_date(date)
  9       joins(:entry).merge(Entry.before_date(date))
 10     end
 11   end
 12
 13   def amount_in_cents=(amount_in_cents)
 14     write_attribute(:amount_in_cents, amount_in_cents.to_f * 100)
 15   end
 16
 17   def amount
 18     amount_in_cents * 0.01
 19   end
 20 end
~

In this case, it results in some awesome SQL:

>> @expense.entries.before_date(Date.today + 1.day).to_sql
=> "SELECT \"entries\".* FROM \"entries\" INNER JOIN \"transactions\" ON \"entries\".\"id\" = \"transactions\".\"entry_id\" WHERE \"transactions\".\"account_id\" = 1 AND (date <= '2011-08-19')"

Nice, huh?

Supposedly, this syntax:

joins(:entry).merge(Entry.before_date(date))

is equivalent to:

joins(:entry) & Entry.before_date(date)

But I have found that to be entirely false and not true. For the above referenced scenario, I found the latter syntax to result in two SQL queries instead of a join, like so:

>> @expense.transactions.joins(:entry) & (Entry.before_date(Date.tomorrow))
  Transaction Load (0.6ms)  SELECT "transactions".* FROM "transactions" INNER JOIN "entries" ON "entries"."id" = "transactions"."entry_id" WHERE "transactions"."type" IN ('Transaction') AND "transactions"."account_id" = 16
  Entry Load (0.5ms)  SELECT "entries".* FROM "entries" WHERE (date <= '2011-08-21')

Thanks:

Yearly Indexes: 2003 2004 2006 2007 2008 2009 2010 2011 2012 2013 2015 2019 2020 2022