こちらにあります https://github.com/sonots/ruby-sql-maker

細かい使い方はドキュメント書いている途中なので少々お待ちを> < 

動機

SQL を文字列として自力で組み立てているコードがあって、 そこを綺麗にしたかったんだけど、 Ruby でクエリビルダーというと Arel ぐらいしかなくて、 しかし、Arel で書くと非常につらぽよな感じだったので作った。

ActiveRecord とか Sequel とかの ORM に丸っと置き換えても良かったんだけど、 そのアプリにとってはちょっと重すぎるというのと、 クエリ生成コードを綺麗にしたい&セキュアにしたいだけだったので、 理想的には使いやすいクエリビルダがあれば良いのになぁという感じだった。 で、理想を求めた。

使い方 - Arel との比較

Arel がどんなかんじに辛いのかってのは、 Arelで色んなSQLを組み立ててみる - @ryopekoのなにか の記事を見てもらうと想像つくと思うんだけど、 一番シンプルなやつでもこんなかんじで、ちょっと「うっ」と来る。

books = Arel::Table.new :books
books.project(Arel.sql('*')).where(books[:id].eq(1)).to_sql
# => SELECT * FROM "books" WHERE "books"."id" = 1

SQL::Maker を使うとこうなる。非常に直感的。

builder = SQL::Maker::Select.new(:quote_char => '"', :auto_bind => true)
builder.add_select('*').add_from('books').add_where('books.id' => 1).as_sql
# => SELECT * FROM "books" WHERE "books"."id" = 1

ライセンスについて

SQL::Maker のライセンスは「perlと同じ」と書いてあるので、 GPL と Artistic License のデュアルライセンスになっているのだけど、 移植の場合にこれを果たして MIT ライセンスにしてもいいのだろうか、とか思ったので、 社内の有識者に質問したりしてた。

cf. perl のライブラリ(ライセンスはperlと同じ)をrubyに移植した場合、GPL汚染を受けるのか?

で、まぁ色々悩んだわけですが、「MIT ライセンス」にしましたので、 あとは @zigorou さんによろしくおねがいしたいと思います。╭( ・ㅂ・)و ̑̑

※ ちなみに SQL::QueryMaker は MIT ライセンスでOK

perl 版との違い

perl 版わかる人にしか伝わらないかもだけど、書いておく。

スカラーリファレンスの扱い

perl の SQL::Maker は内部的に文字列と、文字列リファレンスで分岐していて、

  1. 文字列ならクォートする
  2. 文字列リファレンスならクォートしない

というような扱いをしているんだけど、ruby だと全部リファレンスなので、 この区別ができない。なので、ruby 版では、クォートされたくない場合は、 sql_raw メソッド(SQL::QueryMaker由来)を使って、オブジェクトを渡してもらうことにした。

perl 版

builder->add_select(\'COUNT(*)')->add_from('books')->as_sql();
#=> SELECT COUNT(*) FROM "books"

ruby 版

include SQL::Maker::Helper # sql_raw などを生やす
builder.add_select(sql_raw('COUNT(*)')).add_from('books').as_sql
# => SELECT COUNT(*) FROM "books"
SQL::Maker::SelectSet の Export 関数

perl 版では、

use SQL::Maker::SelectSet qw(union union_all intersect intersect_all except except_all);

みたいに use すると、そのスコープに union や intersect 関数が生える。

ruby 版では、これを SQL::Maker::Helper というモジュールにして

include SQL::Maker::Helper

として使ってもらうようにした。 ただし、名前が sql_unionsql_intersect のように変更してある。

perl では qw(union) のようにして取り込む関数を絞ることができるからまだいいんだけど、 ruby の include だとそういうことができないので、重複の可能性が減る方向にもっていった。

SQL::QueryMaker の Export 関数

今回 SQL::Maker に kazuho さんの SQL::QueryMaker も取り込んだ。 SQL::QueryMaker 由来の関数も

include SQL::Maker::Helper

で生えるようにした。sql_raw とか sql_eq とか。

オートバインド

悲しいことに ruby の代表的な Mysql library である mysql2 には、 prepared statement の機能がない。

そこで、:auto_bind => true オプションを渡すと bind して SQL を返すようにした。

:auto_bind => false (デフォルト)

builder = SQL::Maker::Select.new(:quote_char => '"')
builder.add_select('*').add_from('books').add_where('books.id' => 1)
builder.as_sql
#=> SELECT * FROM "books" WHERE "books"."id" = ?
builder.bind
#=> [1]

:auto_bind => true

builder = SQL::Maker::Select.new(:quote_char => '"', :auto_bind => true)
builder.add_select('*').add_from('books').add_where('books.id' => 1).as_sql
#=> SELECT * FROM "books" WHERE "books"."id" = 1

内部的には escape して ? を置換している。ActiveRecord と全く同じことをやっている。

今後

DBI 由来の sql_type とか、プラグインとかまだ移植してないのでボチボチやっていく。 sql_type はどうしようかな〜

おわりに

Ruby には missing だった使いやすい SQL ビルダーができたので、 ORM 使うほどじゃないなぁって場合は、試してもらえるといいかも。

ActiveRecord を使っている場合でも、 1部直接 SQL を自力で組み立てている人とかけっこういると思うので、 そういうところで使っても便利かもしれないですね。Enjoy!