arel 9.0.0の一部を読んだ

activerecord への理解を深めるために、arel 9.0.0 のコードを読んだので、arel が SQL で表現された文字列を生成する流れについてここにまとめておきます。

rails/arel
arel - A Relational Algebragithub.com

サンプルコード

arel の README では、簡単なサンプルコードとして、以下のコードが紹介されています。この記事では、このコードを見ながら説明を進めていきます。

users = Arel::Table.new(:users)
query = users.project(Arel.sql("*"))
query.to_sql

抽象構文木の構築、文字列への還元

上記のコードは、SELECT * FROM “users” に相当する SQL 表現を文字列として生成するためのコードです。まずは全体像を把握するために、このコードの全体の流れについて説明していきます。それぞれの部分の具体的な実装については後述します。

arel の API は、オブジェクトに対してメソッドを呼び出していくことで、抽象構文木 (AST) を大きく育てていくような設計になっています。まず起点となる葉ノードを用意し、そのノードに対してメソッドを呼び出すことで、その親ノードを定義していくような形です。リレーショナルデータベースの観点で見ると、ある単純な集合を表す AST を起点に、別の集合を表す AST を生成していくような形です。

Arel::Table はテーブルを表すノードで、ここでは users という名前のテーブルを表しています。users.project は、射影を表しています。Arel.sql(“*”) は SQL の * にあたる部分のノードを簡単に生成するためのメソッドで、Arel::Nodes::SqlLiteral.new(“*”) と同様の意味です。users テーブルから全ての列を取り出すような射影を行う、という操作を表しているわけです。

これら一連のコードによって、内部的にはオブジェクト同士の参照によって AST として扱えるデータ構造が構築され、その根ノードにあたるオブジェクトへの参照が得られます。上記のコードでは、これをローカル変数 query に格納しています。

query.to_sql を呼び出すと、内部では AST を文字列に還元するための処理が実行されます。AST の構造と SQL 表現への変換アルゴリズムとを分離するために、この還元処理は Visitor パターンを用いて実装されています。

Arel::Table.new

Arel::Table.new で行われている処理について、詳しく見ていきます。

とはいえ、ここではほとんど特筆すべき処理は行われていません。Arel::Table のインスタンスを生成し、コンストラクタの引数として渡された値を、そのインスタンス変数に格納しているだけです。

Arel::Table#project

users.project で行われている処理について、詳しく見ていきます。

このメソッドは、Arel::SelectManager のインスタンスを生成し、このインスタンスに処理を委譲します。Arel::SelectManager は、SELECT を利用した問い合わせ文を内包するためのクラスです。内部では Arel::Nodes::SelectStatement というノードを用意し、users テーブルを表す Arel::Table のインスタンスを問い合わせのデータソースとして与え、それから project の引数を射影のための情報として与えています。

結果的に、テーブルに対して射影を行おうとすると、SELECT クエリを表現するためのオブジェクトがあれこれと用意され、オブジェクト間の参照によってこれが木構造に組み合わされます。簡略化すると、以下のような Arel::SelectManager を根ノードとした木構造が出来上がります。

Arel::SelectManager
|
`-Arel::Nodes::SelectStatement
|
`-Arel::Nodes::SelectCore
|
|-Arel::Nodes::SqlLiteral
|
`-Arel::Nodes::JoinSource
|
`-Arel::Table

説明していませんでしたが、SelectStatement は、ORDER BY や LIMIT などにあたる情報は直接参照する一方で、FROM や WHERE などにあたる情報は SelectCore というノードに内包して参照します。SelectCore では更に、FROM にあたる部分の情報を JoinSource というノードに内包して参照します。

users.project の呼び出しが AST を構築する様子について説明しました。

Arel::TreeManager#to_sql

query.sql で行われている処理について、詳しく見ていきます。

このメソッドは、Visitor パターンを利用しながら、これまでに生成した AST を根ノードから探索し、SQL で表現された文字列として還元していきます。

AST をどのような SQL に変換するべきかというロジックは、対象となるリレーショナルデータベースエンジンによって微妙に異なります。そのため arel は、適切なコネクションアダプタを利用者に要求し、コネクションアダプタから Visitor を提供してもらうことで、この差異に対処しています。実際には Visitor クラスの実装自体は arel によって提供されており、各コネクションアダプタはこの実装を利用して Visitor を提供することになるのですが、この Visitor の実装では、クォーティング処理や型変換処理のロジックをコネクションアダプタから提供してもらうことになっています。相互に参照し合っている複雑な関係ではありますが、このことによって、 Visitor がデータベースエンジンごとに異なる振る舞いを持てるようになっています。

Visitor の実装には、Arel::Visitors::ToSql クラスが使われます。これはArel::Visitors::Visitor を継承したクラスで、Arel::Visitors::Visitor クラスではノードを受理するときにどのメソッドを呼ぶかというロジックが定義され、Arel::Table に対しては #visit_Arel_Table というように、クラス名に対応するメソッドが呼ばれる仕組みが実装されています。そして Arel::Visitors::ToSql クラスでは、これらの各ノードに対応するメソッドがそれぞれ定義されています。今回のサンプルコードであれば、以下のメソッドが利用されることになります。

  • Arel::Visitors::ToSql#visit_Arel_Nodes_SelectStatement
  • Arel::Visitors::ToSql#visit_Arel_Nodes_SelectCore
  • Arel::Visitors::ToSql#visit_Arel_Nodes_SqlLiteral
  • Arel::Visitors::ToSql#visit_Arel_Nodes_JoinSource
  • Arel::Visitors::ToSql#visit_Arel_Table

Arel::Visitors::ToSql#visit_Arel_Nodes_SelectStatement

今回の探索処理の起点となるメソッドです。1つの SELECT 文を生成するためのメソッドと言えます。ほとんどの部分を SelectCore で生成した後、ORDER BY、LIMIT、OFFSET などに関する部分を付け加えています。

Arel::Visitors::ToSql#visit_Arel_Nodes_SelectCore

SELECT 文の先頭から WHERE 句までの部分を担当するメソッドです。Arel::Table#project の引数として与えた情報をもとに、SELECT に続く文字列を構築します。また、FROM に続く文字列を JoinSource に構築させます。

Arel::Visitors::ToSql#visit_Arel_Nodes_SqlLiteral

SQL でのリテラルを担当するメソッドですが、今回は * を出力するために利用されます。妥当なリテラルを表現する文字列で構成されていることが分かりきっているため、特にエスケープ処理やクォーティング処理などは加えられません。

Arel::Visitors::ToSql#visit_Arel_Nodes_JoinSource

FROM に続く文字列を担当するメソッドです。今回 JOIN は関与していないため、単純にテーブル名の部分を出力だけの処理になり、これは Table に任されます。

Arel::Visitors::ToSql#visit_Arel_Table

テーブル名にあたる文字列を担当するメソッドです。FROM users u のように、SQL でのテーブル名には別名を割り当てることができますが、今回はこの処理は関与しません。テーブル名の部分は、各データベースエンジンに適した形式でクォーティングされます。

おわり

簡単なサンプルコードをベースに、arel が SQL で表現された文字列を生成するまでの流れについてまとめました。