2012年9月5日水曜日

find_by_sql のうまい使い方(1)

rails3からARELが導入され、ActiveRecordのクエリインターフェースはとても洗練されたものになりました。
scopeを組み合わせることで複雑なクエリをとてもシンプルかつ自然な形で表現できるようになっています。実際のところ、ARELの表現力はよく現れるSQLのほとんどは表現可能だと思います。

たとえば

 User.activated.without_email.includes(:emails, :tels).order("name").limit(10)

のようにしてメールアドレスを登録していないユーザを名前順に10件取り出す、ということが実現できます。ARELがすごいのはこのようなシンプルな例ではなく、もっと多数のテーブルが絡んだ複雑なクエリであっても、scope を適切に定義することですっきりとしたクエリが記述できることにあります。

しかしそんなARにも弱点はあります。パフォーマンスの問題です。ARはとにかく遅い。結果が1000行単位になると数秒待たされることもざらです。何に時間がかかるかというと

  • AR のオブジェクトの組み立て
  • オブジェクトツリーの構築
です。sql の実行がいかに早くても、オブジェクトの件数が多いとそれだけでもっさりしてしまいます。

性能がさほど問われないページではそれでもARELのメンテナンス性を重視して多少の速度には目を瞑るのですが、検索結果などの速度が必要なページでは、残念ながら生のARELは許容できません。

このようなパフォーマンス上の問題を改善するためにどういう方法がとれるかというと、基本的な方針としては find_by_sql に切り替えることになります。これはなぜかというと、

  • 不要なカラムを取得しないことでモデルオブジェクトの作成にかかるコストを減らす。
  • 細かいテーブル(ユーザに紐づくメアドや、市区町村名などの各種マスタ)をいちいち ActiveRecord のモデルとして作成せずに join してプロパティとしてアクセスできるようにする。たとえば user.email.address などのメソッドチェーンを発生させるのではなく、user.email_address などとしてワンステップでアクセスできるようにする。これによって余計なモデルの作成とオブジェクトツリーの組み立てが省略できる。
  • ActiveRecord の eager/lazy load がおきないようになる。ひとつのsqlですべてのデータを取得する。

ということを実現するためです。

このために従来は find_by_sql を使っていたのですが、これだとせっかく scope で定義したクエリの内容を重複して sql に書かないといけません。この sql の作成に scope を使いたい、と考えるのが人情です。

この目的のため?に、ActiveRecord は to_sql というメソッドを持っています。名前のとおり、scope などで定義された AREL をそれが表現する sql に変換するためのメソッドです。


sql = User.activated.select("users.id, users.name, emails.address, tels.tel").order(:name).limit(10).to_sql
User.find_by_sql(sql)

ポイントは

  • includes の代わりに joins を使う。
  • select で必要なカラムだけを取得する。
  • 結果として、association が発生しないように必要なカラムはすべて取得しておく。
ということになります。こうすることでパフォーマンスが必要な場合でも scope を使ってクエリを記述することができます。もともとの、sql を使わない版からの変更も比較的容易だと思います。


この方式がなぜうまくいくかを別の視点から説明しておきますと、
  • データの絞り込み条件として定義されたscopeは通常ビジネスロジック上の重要な概念を表しており(activated など)、ページごとに意味が変わることがなく、再利用しやすい。またscopeの形で字面上ひとつの単語(メソッド名)になっていても頻出の概念であるため理解が容易。
  • 一方でページごとにころころ変わるのはどのカラムを取得するか、だが、これは select でその都度切り替えることができる。逆にこれをscopeにしたところで適切な名前をつけることは難しく、結局中身を見ないとわからないということになりがちです。
ということで、変化しにくい部分をうまいことコードの下のほう(モデルの定義)に追いやって、変化しやすい部分をコードの上のほう(よりcontrollerに近いほう)にすくいあげていることになります。このようにコードがあるべき場所に配置されているためメンテナンスしやすいといえます。

0 件のコメント:

コメントを投稿