観察から知る 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 は以下のことが可能になった。
利用したテストデータ
先ずは 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