観察から知る Rails3 ActiveRecord + Arel の特徴と使い方

Rails 3 になって Active Record に Arel という機構が適用された。これを利用することで、Active Record のサブクラスに対してメソッドチェインのようにSQLクエリを構成し、発行することができる。*1

例えば以下のようにクエリを構成できる。

ruby-1.9.2-p0 > Person.where('age > ?', 10)
 => [#<Person id:1, name:"taro", age:24>, #<Person id:2, name:"jiro", age:13>]

そしてメソッドチェインによって検索条件を追加。

ruby-1.9.2-p0 > Person.where('age > ?', 10).where('age < ?', 20)
 => [#<Person id:2, name:"jiro", age:13>]

to_sql によって利用されるSQLが何かを確認することもできる。

ruby-1.9.2-p0 > Person.where('age > ?', 10).where('age < ?', 20).to_sql
 => "SELECT \"people\".* FROM \"people\" WHERE (age > 10) AND (age < 20)"

途中まで SQL を組み立てたオブジェクトを持ち運び、後になって SQL を追加することも可能だ。

ruby-1.9.2-p0 > on_the_way = Person.where('age > ?', 10)
 => [#<Person id:1, name:"taro", age:24>, #<Person id:2, name:"jiro", age:13>]
ruby-1.9.2-p0 > on_the_way.where('age < ?', 20)
 => [#<Person id:2, name:"jiro", age:13>]

以前でもできたが、 Association を持ち運び、更に SQL を追加することが可能になっている。以前と比べて強力なのはやはりメソッドチェインによって条件を追加していけることだ。

ruby-1.9.2-p0 > kids = children.people
 => [#<Person id:2, name:"jiro", age:13>, #<Person id:3, name:"saburo", age:8>]
ruby-1.9.2-p0 > kids_o = kids.where("name like ?", '%o')
 => [#<Person id:2, name:"jiro", age:13>, #<Person id:3, name:"saburo", age:8>]
ruby-1.9.2-p0 > kids_o.where('age > ?', 10)
 => [#<Person id:2, name:"jiro", age:13>]
ruby-1.9.2-p0 > kids_o.where('age > ?', 10).to_sql
 => "SELECT \"people\".* FROM \"people\" WHERE (\"people\".group_id = 2) AND (name like '%o') AND (age > 10)"

何故こんな事が実現できているか

判りやすいところから見ていく。先ず where 等は class を問いあわせると ActiveRecord::Relation を返している。

ruby-1.9.2-p0 > Person.where(name:'taro').class
 => ActiveRecord::Relation
ruby-1.9.2-p0 > Person.where(name:'taro').order('name asc').class
 => ActiveRecord::Relation

でも rails console 上でこれ単体を評価すると Array が帰ってくる。判ってしまえば簡単な話なんだけど、rails console (或いは irb) で式を評価した結果の表示は、inspect を噛ましている。従って、inspect が呼ばれた段階で SQL を発行紙、その結果を返している。このあたりは ActiveRecord::Relation#inspect のソースを参照のこと。

ruby-1.9.2-p0 > puts Person.where(name:'taro').inspect
[#<Person id:1, name:"taro", age:24>]
 => nil

inspect は内部的に to_a に対して inspect を行っていて、to_a が呼ばれた時点でオブジェクト群を見つけるための SQL を発行しているようだ*2

その結果、配列が返されている。each メソッドなども実装されていて、これは to_a に対して delegate されており結果的には to_a の戻り値に対して each している。

    delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a

こうして実際にSQLの発行が必要な時まで構文の組み立てのみを行い、inspect や each 等が呼ばれた場合に、必要になったと判断して SQL を発行している。

ActiveRecord::Relation は Enumerable や Range と類似したインターフェースを持つ、関係性を表すためのクラスと言うことだ。あくまで類似なのでイコールではない。ただ不便になるようなシーンは僕のようなライトユーザにとっては、今のところないw

色々調査できる点はあるのだけれど、今回はこのライブラリの利用者の視点に立って、ActiveRecord + Arel の利用法を明らかにすることが目的だったのでここでひとまず筆を置く。

遅延評価を実装したことによって ActiveRecord は以下のことが可能になった。

  • SQL を断続的に組み立てること
  • その途中経過を to_sql によってチェックすることが可能
  • 組み立てている最中のオブジェクトを持ち運ぶこと

利用したテストデータ

先ずは migration で User, Group を作成

$ rails g model Person group_id:integer name:string age:integer
      invoke  active_record
      create    db/migrate/20110303105426_create_people.rb
      create    app/models/person.rb
      invoke    test_unit
      create      test/unit/person_test.rb
      create      test/fixtures/people.yml
$ rails g model Group name:string
      invoke  active_record
      create    db/migrate/20110303105433_create_groups.rb
      create    app/models/group.rb
      invoke    test_unit
      create      test/unit/group_test.rb
      create      test/fixtures/groups.yml
$ rake db:migrate
==  CreatePeople: migrating ===================================================
-- create_table(:people)
   -> 0.0026s
==  CreatePeople: migrated (0.0033s) ==========================================

==  CreateGroups: migrating ===================================================
-- create_table(:groups)
   -> 0.0018s
==  CreateGroups: migrated (0.0018s) ==========================================

次に migration を追加して関連性を持たせる。あと見やすさのために inspect も定義しておく。

# app/models/person.rb
class Person < ActiveRecord::Base
  belongs_to :group
  def inspect
    "#<Person id:#{id.inspect}, name:#{name.inspect}, age:#{age.inspect}>"
  end
end

# app/models/group.rb
class Group < ActiveRecord::Base
  has_many :people
  def inspect
    "#<Group id:#{id.inspect}, name:#{name.inspect}>"
  end
end

サンプルデータ投入

$ rails console
Loading development environment (Rails 3.0.4)
ruby-1.9.2-p0 > taro = Person.create name:'taro', age:24
 => #<Person id:1, name:"taro", age:24>
ruby-1.9.2-p0 > jiro = Person.create name:'jiro', age:13
 => #<Person id:2, name:"jiro", age:13>
ruby-1.9.2-p0 > saburo = Person.create name:'saburo', age:8
 => #<Person id:3, name:"saburo", age:8>
ruby-1.9.2-p0 > adults = Group.new(name:"adult")
 => #<Group id:nil, name:"adult">
ruby-1.9.2-p0 > adults.people << taro
 => [#<Person id:1, name:"taro", age:24>]
ruby-1.9.2-p0 > adults.save
 => true
ruby-1.9.2-p0 > children = Group.new(name:"children")
 => #<Group id:nil, name:"children">
ruby-1.9.2-p0 > children.people << jiro
 => [#<Person id:2, name:"jiro", age:13>]
ruby-1.9.2-p0 > children.people << saburo
 => [#<Person id:2, name:"jiro", age:13>, #<Person id:3, name:"saburo", age:8>]
ruby-1.9.2-p0 > children.save
 => true

*1:検証した環境は Ruby 1.9.2, Rails 3

*2:他にもあるかもしれないが、今回は調査の対象外