Ruby Freaks Lounge

第43回Rails 3を支える名脇役たち その1 - Arel -

はじめに

Ruby on Railsの2年半ぶりのメジャーバージョンアップである3.0の正式リリースがいよいよ間近に迫ってきました。

Rails 3は、アプリケーション・レベルではRails 2.3との互換性をなるべく保ちながらも、メジャーバージョンアップだけあってフレームワーク自体は隅々にまで徹底的なリファクタリングが施されて更なる洗練を遂げています。結果として、Rails 3では融通の効かないフルスタック構造を捨ててすっきりとしたモジュール独立性が実現されているのですが、この際に、Merbとの合併の影響もあってか、いくつかの新たな外部ライブラリに依存する形になっているのも興味深いところです。

そこで本稿では、あえてRails 3そのものではなく、このRails 3の大改造の舞台裏を支える裏方さんにスポットライトを当ててみたいと思います。

Arelによってパラダイムが大きく変わったActiveRecord 3

今回のRailsのメジャーバージョンアップの中でも特にドラスティックに変身を遂げたのが、データベース層を司る、ご存知ActiveRecordです。大げさに言えば、ActiveRecord 3では、従来のO/Rマッパーとは一線を画した、O/Rマッピングの概念を一段高いステージに押し上げるような、全く新しい世界観が表現されています。では、新しいActiveRecordでは、一体どのような新機能が、一体どのようにして実現されているのでしょうか?

従来のActiveRecordでは、⁠世の中のほとんど全てのO/Rマッパーがそうであるように)文字列ベースで組み立てたSQLをfind()メソッドの呼び出しでドカッとまとめてDBに投げていたのですが、ActiveRecord 3では、RubyのDSLで書かれた条件を最終的にSQLの文字列に変換する処理は「Arel」という中間ライブラリで行われるようになっています。

どうやら、ActiveRecord 3の新しいクエリインターフェイスの鍵は、このArelが握っているようです。そこで、このArelというライブラリについて、少々深く掘り下げて調べてみることにしましょう。

すべてが「named_scope」!?

もともとこのArelというライブラリは、ActiveRecordのnamed_scope機能の作者である Nick Kallen氏[1]が着想して作り始めたものです。この時の設計の意図については、彼自身のブログで以下のように語られています。

After I wrote ⁠named_scope⁠⁠ I immediately asked myself ⁠but what if every query was a named_scope? What if named_scope were the rule and not the exception?⁠.

Nick Kallen氏のブログMagic Scaling Sprinklesより

Nickは、named_scopeをさらに汎用化させて関係代数における「関係(=Relation⁠⁠」という概念をそのままオブジェクト化することによって、すべてのクエリをこの「関係」「関係」で表した純粋で美しい世界が構築できるのではないかと考えたのです。この「関係」オブジェクトは、数学的には「クロージャ」であり、実装のアイデアは関数型言語に強くインスパイアされたものでした。

しかし、ActiveRecord全体をnamed_scopeっぽいものに変えるためには、ActiveRecordの根幹からガラッと作り替える大手術を行う必要があったため、いきなり複雑に肥大化したActiveRecordの手術に挑むよりは、まずは特定のO/Rマッパーに依存しないような汎用的なクエリ言語の設計から始めた方が良いのではないか、という判断に至りました。こうして作り始められたのが汎用オブジェクト指向関係代数クエリ言語「Arel」です。その後Nickがなんとわずか半年程度でArelをざっくりと作り終えると、このNickの試みはBrian Helmkamp, Emilio Tagua, Pratik Naikといった協力者たちによってActiveRecordへと組み込まれ、僕らのもとへと届けられることになりました。

のちほどActiveRecord 3で実際にクエリっぽいものを動的に組み立てて最後に遅延評価で実行させるコードをご紹介しますが、これは、⁠すべてがscope」というビジョンに基づいて再設計が行われ、それが本当に実現されてしまったからこそ可能になっているわけです。

Arelについて

Arelとは、⁠Relational Algebra」または「Active Relation」の略で、その名前から想像がつくとおり、ActiveRecordから派生した、⁠関係代数」をRubyのオブジェクトで取り扱うためのライブラリです。

ドキュメントによれば、Arelは、

  1. 面倒なSQLの生成を簡単にしてくれて、
  2. さまざまなデータベースシステムに対応している、

「フレームワークのフレームワーク」を目指して作られています。

つまり、Arelを使えば、DBの互換性やSQL文字列の生成などに惑わされることなく、大事な設計やモデリングに注力してO/Rマッピング処理を実装することができるようになっています。

この仕組みは、WEB層でいうなら、ちょうどRackと同じ位置づけに当たります。RackがWebサーバーとWebアプリケーションの間を仲介してくれているのと同様に、ArelはデータベースとO/Rマッパーの間のごちゃごちゃとした部分を抽象化してすっきり整理してくれています。

また、⁠さまざまなデータベースシステムに対応」というのは、単に複数のRDBMS製品の方言を吸収、というだけにとどまらず、SQLを使わないデータベース、インメモリのキャッシュ的なもの、さらにはYAML等のファイルへの永続化までを考慮して設計されています。ひと昔前までは、⁠データベース操作」と言えば「リレーショナル・データベース・システム」に対して「SQL」という規格化された問い合わせ言語を用いて実装されることが多かったのですが、最近は「Key Value Store⁠⁠、⁠クラウド⁠⁠、はたまた「NoSQL」といった形で、永続化や問い合わせの手法がどんどん多様化してきています。バックエンドの多様性を吸収して抽象化してくれることによってアプリケーションコードの汎用性を高めてくれるArelという存在は、まさにこんな時代の流れを的確に捉えたライブラリと言えるでしょう。

作者について

なお、Arelの作者であるNick Kallen氏は、QCon Tokyo 2010というイベントで来日を果たしており筆者もArelについて直接質問をぶつける機会があったのですが、意外なことに、本人はArel自体の開発から手を引くと同時に既にArelというプロダクト自体に対してもさほど興味は残っていないようでした。本人も認めているとおり、彼はオープンソース活動を通じて自身の素晴らしいアイデアを世界にぶちまけることにやりがいを感じているのですが、その後のメンテナンスやら細かい作り込みにはそれほど熱心になれないようです(ある意味Rubyistらしいとは言えるかもしれませんね)。いずれにしろ、彼のような天才の仕事が、本人の手を離れてもコミュニティによって引き継がれて人類の共有財産になっていっている、というあたりは、オープンソースの世界の懐の広さを感じさせる現象ではないでしょうか。

写真1 筆者(左)とNick Kallen(右⁠⁠。QCon Tokyoのパーティにて
図1 筆者(左)とNick Kallen(右)。QConTokyoのパーティにて
撮影:角谷信太郎さん

その後も彼はまた全く別の分野で新たなプロダクトを作り始めているとのことなので、今後とも彼の活躍には期待したいところです。

Arelを使ってみよう

さて、それでは、さっそく生のArelを実際に使ってみましょう。

例えば、以下のようなデータベースがあったとします(例はSqlite3を使用⁠⁠。

books.sqlite3
create table authors(id integer primary key autoincrement not null, name varchar);
create table books(id integer primary key autoincrement not null, title varchar, price integer, published_date date, author_id integer);

執筆時現在の最新バージョン(0.4.0)では、Arelからデータベースに接続するためにはActiveRecord::Baseのconnectionを利用しています。そこで、ちょっと横着ですが、例えば以下のようにすれば手っ取り早くArelでこのDBに接続することができます。

require 'rubygems'
require 'arel'
require 'sqlite3'
require 'active_record'

ActiveRecord::Base.configurations = {'development' => {:adapter => 'sqlite3', :database => 'books.sqlite3'}}
ActiveRecord::Base.establish_connection('development')

Arel::Table.engine = Arel::Sql::Engine.new(ActiveRecord::Base)

books = Arel::Table.new :books
puts books.to_sql

これを実行すると、以下のようなSQL文が出力されるはずです。

出力されるSQL
SELECT     "books"."id", "books"."title", "books"."price", "books"."published_date", "books"."author_id"
FROM       "books"

それでは、このbooksテーブルに対する問い合わせ条件を追加してみましょう。

books = Arel::Table.new :books
books = books.where(books[:title].eq('Head First Rails'))
puts books.to_sql
出力されるSQL
SELECT     "books"."id", "books"."title", "books"."price", "books"."published_date", "books"."author_id"
FROM       "books" WHERE     "books"."title" = 'Head First Rails'

以下のように、さらに複雑な条件を指定したり他テーブルと結合したりすることもできます。

books = Arel::Table.new :books
authors = Arel::Table.new(:authors).where(authors[:name].eq('david'))
books = books.join(authors).on(books[:author_id].eq(authors[:id]))
books = books.where(books[:price].gt(3000))
books = books.order(books[:published_date])
books = books.take(10)
puts books.to_sql
出力されるSQL
SELECT     "books"."id", "books"."title", "books"."price", "books"."published_date", "books"."author_id", "authors"."id", "authors"."name"
FROM       "books" INNER JOIN "authors" ON "books"."author_id" = "authors"."id"
WHERE     "books"."price" > 3000 AND "authors"."name" = 'david'
ORDER BY  "books"."published_date" ASC
LIMIT 10

このように、Arelによるクエリは、小さな条件を表す一つ一つのメソッド呼び出しをどんどんメソッドチェインしながら組み立てていく形で作られており、最後にcallされた時点で全ての条件を重ね合わせたものを評価するような仕組みになっています。上の例だけだと、昔のO/Rマッパーでよくあった "Criteria" クラス的なものとどこが違うんだ?と疑問に思われるかもしれませんが、ArelではTable.newやwhere等のメソッドの戻り値がそれぞれRelation(のサブクラス)のオブジェクトになっていて、それぞれのRelationが条件そのものを表すと同時に何重にもチェインすることができ、そしてそれ自体が実行可能でもある、というところが特徴的なところです。

このRelationはあくまで「関係」を表す数学的な概念のインスタンスであって、実際にcallされた時点で初めてコンテキストに応じて遅延評価されて適切な形式でクエリに変換されます。

このおかげで、プログラマーから見ると、Arelによるクエリは最小単位の Relation をどんどんメソッドチェインしながら組み立てていくようになっていて、最後に評価された時点ですべての条件を重ね合わせた結果のみが実体化されるという仕組みになっていることが理解いただけたかと思います。

これこそまさに、中学校あたりで図1のような「ベン図」を書きながら習った、あの「集合演算」のイメージそのものですよね。

図1 ⁠ベン図」で書かれた集合演算のイメージ
図1 「ベン図」で書かれた集合演算のイメージ

Arelはこのようにして、関数型由来のダイナミズムというRubyの持つ特性をうまく生かしたアプローチで、複雑な概念をシンプルなインターフェイスに落とし込むことに成功しているのです。

Arelで生まれ変わったActiveRecord 3

最後に、話をActiveRecord 3に戻して、Arelが実際にどう活用されているかを見てみましょう。前述のとおり、ActiveRecord 3の正体はArelをラップして作られた高機能O/Rマッパーなのですが、この際に、Railsらしく生のArelよりはだいぶユーザーに優しいDSLが提供されています。

従来のActiveRecordは、モデルのクラスのfind()メソッドの引数にRubyのHashをキーワード引数っぽく使った以下のようなスタイルでクエリを発行していました[2]⁠。

ActiveRecord 2.3の普通のクエリ
books = Book.all(:joins => :author, :conditions => {:price => 3000, :authors => {:name => 'david'}}, :order => :published_date, :limit => 10)
このとき発行されるSQL
SELECT "books".* FROM "books" INNER JOIN "authors" ON "authors"."id" = "books"."author_id"
WHERE ("books"."price" = 3000) AND ("authors"."name" = 'david') ORDER BY published_date LIMIT 10

しかし、RubyのHashで表現できないような条件になると、とたんに「SQL文を文字列で組み立てる」感じの大昔からお馴染みの手法に逆戻りせざるを得ないという、いささか残念な状況に陥ってしまうのでした。

ActiveRecord 2.3で、条件に不等号が1つ入った場合のクエリ
books = Book.all(:joins => :author,
:conditions => ['price > ? and authors.name = ?', 3000, 'david'], :order => :published_date, :limit => 10)
このとき発行されるSQL
SELECT "books".* FROM "books" INNER JOIN "authors" ON "authors"."id" = "books"."author_id"
WHERE (price > 3000 and authors.name like '%david%') ORDER BY published_date LIMIT 10

また、例えばユーザーの入力に応じて動的に条件を組み立てるような機能を実装しようとすると、引数のHashや文字列を事前にごにょごにょとmergeしたりする羽目になってしまい、⁠Rubyだから頑張ればたいがい1~2行で記述できるとは言っても)決して美しいとは言い難いコードになってしまっていました。

一方、ActiveRecord 3では、これと同じ内容の問い合わせは以下のように記述することになります。

Rails 3の普通のクエリ
books = Book.joins(:author).where('price > ?', 3000).order(:published_date).limit(10) & Author.where(:name => 'david')
このとき発行されるSQL
*何も発行されない*

さらに、この一連の条件の塊に対して、後から条件を動的に追加することができるようになっています。

Rails 3でクエリに動的に条件を追加する
books = books.where('published_date > ?', 3.years.ago.to_date)
books.each do |user|  # SQLはこのeachの呼び出しの時点で初めて発行される
  puts books.title
end
このとき発行されるSQL
# SELECT "books".* FROM "books" INNER JOIN "authors" ON "authors"."id" = "books"."author_id"
WHERE (price > 3000) AND ("authors"."name" = 'david') AND (published_date > '2007-06-14') ORDER BY published_date LIMIT 10

つまり、ActiveRecord 3は、⁠文字列ベースで組み立てたSQLを何かのメソッド呼び出しでドカッと発行する」というスタイルを遠く離れて、Rubyで記述された複数の小さな論理的な条件を重ね合わせているうちにいつの間にか適切な集合演算が行われてしまっている、というような感覚に変わっています。

そう、まるで:conditionsも:orderも:joinsも、⁠すべて動的なnamed_scopeになってしまった」かのようです。

さて、ここまでお読みいただいた皆さんには、どうやってこんな魔法のようなDSLが実現できてしまっているのか、もうご理解いただけていますよね?

まとめ

以上、駆け足でご紹介しましたが、Arelにはまだまだ紹介しきれていない機能もいろいろありますし、まだ若いプロダクトなので、これから更なる発展を遂げることが期待されます。Rackの登場に後押しされてWebサーバーやWEBアプリケーション・フレームワークのプロダクトが数多く世に登場したのと同じく、今後はArelの肩に乗っかってRubyで作られた新世代O/Rマッパーやデータ永続化の仕組みがこれからどんどん作られていく流れになっていくかもしれません。楽しみですね!

おすすめ記事

記事・ニュース一覧