ActiveRecordを支える技術 – Arelとは何者なのか? (全5回) その1


2014年 05月 04日

Rails3, ActiveRecordからは、内部でArelと呼ばれるSQL生成用のライブラリが利用されています。
今回、Arelが内部でどのようにSQLを生成しているのかを調査したので、当ブログにて公開いたします。
ちょっと長くなってしまったため、全5回に分割しました。

以下、目次となります。

Arelについて調査しようと思った背景

ActiveRecordを使って複雑なクエリを組み立てていると、たまにおかしなSQL文が生成されていること、ありませんか?

ActiveRecordはとてもよく出来たライブラリだと思います。
Datamapper というORMをRailsで使っていた頃は、ちょっとした事でもすぐにクエリが壊れてしまうという事態に遭遇していましたが、ActiveRecordはちょっとやそっとのことでは、クエリが壊れたりはしません。

それでも業務系システムでありがちな複雑なクエリ(例えばテーブルを6個くらいjoinして検索、自己結合あり等)をActiveRecordで頑張って組み立てようとすると、変なSQL文が生成されることがあります。

以下、遭遇する問題の例をあげます。

  • 同じテーブルを何回もjoinするとwhere条件がおかしくなってしまった
  • mergeすると変な条件がくっついてしまった
  • includes, referencesすると別のデータがとれてしまう、でもincludesせずpreloadすると問題なくデータとれる

等など(上記問題はRails4系でも遭遇します)。

こういう複雑なクエリを組み立てる事になった場合は、素直にTwoWaySql等を使ったほうが良いような気もします。でも、一度TwoWaySql方式を採用したことがありますが、scope使い回せないのはやっぱり辛い。DRYじゃない。

「ActiveRecordのソース読んで、なんでこういうおかしなクエリが発行されるのか調査してやろう」

そう思ってソースを読み始めた訳ですが、実際読んでみるとよくわからない。
それもそのはず、ActiveRecordが内部で使っているArelを理解してないと、どういう風にSQLを生成しているのか理解できないわけです。

そういう訳で、今回Arelが内部でどのようにクエリを組み立てているのかについて調査しようと思いました。

Arelでのクエリ生成例

過去にArelで複雑なクエリを生成する方法についての記事を書きました。
Arelになじみのない方は、以下のブログ記事を読んでいただければと思います。

ActiveRecord4でこんなSQLクエリどう書くの? Arel編

今後利用するサンプルコード

調査するArelは 2014/04/22 時点での masterブランチ(last commit: de91633) のソースを利用します。

DB adapterはmysql2を利用します。
Arelにmysql2 engineを渡すためにActiveRecordを使ってます。
pry, pry-byebugはデバッグ用に利用します。

よって、以下のGemfileを用意します。

source "https://rubygems.org/"

gem 'arel', path: '~/Programming/arel/' # path以下はgit clone したディレクトリを指定してください
gem 'mysql2'
gem 'activerecord'
gem 'pry'
gem 'pry-byebug'

サンプルコードは、以下の通りです。

require 'rubygems'
require 'bundler'
Bundler.require

require 'arel'
require 'active_record'
require 'pry'

db_name = :arel_research

# Arelでto_sqlしたときに組み立てるSQL文をmysqlのものにするため、
# mysqlに対してestablish_connectionする
# ActiveRecordはestablish_connectionのためにのみ利用
ActiveRecord::Base.establish_connection adapter: 'mysql2', database: db_name, host: 'localhost'
Arel::Table.engine = ActiveRecord::Base

# ここから先はActiveRecordの事は忘れてください
# where等でてきますが、ActiveRecordのwhereではありません。

# --- ここにサンプルコードをかきます #1
product = Arel::Table.new(:products)
product.project('id').project('name').to_sql
product.project('*').to_sql
# --- サンプルコードここまで

上記「#1」以下に、今後調査に利用するArelのサンプルコードを書いていきます。