Rails(ActiveRecord)のJOINのイディオム
旧世代と罵られそうですが、O/Rマッパー(ORM)に微妙に慣れません。基本的に、こうあるべき、というSQL(SELECT文)を思い浮かべて、それと同程度のSQLを吐き出すようにコードを書いています。実に倒錯的です。
なぜそんなことをするかと言うと、N+1問題が恐いからです。N+1問題は、以下のActiveRecordの解説で説明しています(現象的には1+N問題と呼ぶ方が適切な気もしてきました)。
あまり考えずにORMでコードを書くと、SELECTの結果セットがN行の時、追加のSELECT文がN回走る危険があります。これが恐くて、コードを書いては、いちいち発行されるSQLを確認しないと眠れません。偏執狂的ですが、Nが1でも許せません。JOIN一回で済むところを2回もSELECTが発行されるのが許せないのです。
そんなわけで、変態的ですが、SQLからActiveRecordのコードという逆引きチートシートです。形式的に引けるように、テーブル名をアルファベット一文字にしています。外部キーはRailsの流儀です(特に説明していません)。 has_manyの部分は、has_oneにしてもだいたい等価です(Rubyのコードで配列になるかどうかの違い)。
SELECT * FROM A LEFT OUTER JOIN B ON B.a_id=A.id; class A has_many :B end A.find(:all, :include=>:B)
SELECT * FROM A LEFT OUTER JOIN B ON B.a_id=A.id LEFT OUTER JOIN C ON C.a_id=A.id class A has_many :B has_many :C end A.find(:all, :include=>[:B,:C])
SELECT * FROM A LEFT OUTER JOIN B ON B.a_id=A.id LEFT OUTER JOIN D ON D.id=B.d_id class A has_many :B has_many :D, :through=>:B end class B belongs_to :D end A.find(:all, :include=>:D) または A.find(:all, :include=>{:B=>:D}) # この書き方の場合、:through=>:B の行がなくても動きます
SELECT * FROM A LEFT OUTER JOIN B ON B.a_id=A.id LEFT OUTER JOIN D ON D.id=B.d_id LEFT OUTER JOIN E ON E.d_id=D.id; class A has_many :B end class D has_many :E end A.find(:all, :include=>{:B=>{:D=>:E}})
最後に対して具体例を示します。
テーブルは3つです。説明に必要なカラムだけ取り出した図を示します。例によって、外部キーは暗黙にRails流儀を仮定しています。
articles |id|descripton| comments |id|article_id|user_id|description| users |id|name| ext_user_maps |id|user_id|
articlesとcommentsテーブルの関係は、ありがちな関係なので説明を省略します。usersテーブルも省略します。ext_user_mapsは少し説明が必要です。このシステムが持っているユーザIDと、なんらかの外部システムのユーザIDと結びつけるテーブルだと考えてください。このテーブルを通して得られるidを外部システムユーザIDと呼ぶことにします。
ここで、やりたいことが、articlesの結果セットのコメント全体と、それぞれのコメントの作者の外部システムユーザIDだとします。分かりづらいので、具体的な数値をいれた例で示します。説明に影響は無いので、articlesテーブルのレコード数はひとつにします。
articles |id|descripton| |1 |'x' | comments |id|article_id|user_id|description| |1 |1 |1 |'a' | |2 |1 |1 |'b' | |2 |1 |2 |'c' | users |id|name| |1 |'A' | |2 |'B' | ext_user_maps |id|user_id| |10|1 | |11|2 |
SELECT文と欲しい結果セットは次のようになります。
SELECT articles.id aid, comments.id cid, ext_user_maps.id ext_id FROM articles LEFT OUTER JOIN comments ON articles.id = comments.article_id LEFT OUTER JOIN users ON users.id = comments.user_id LEFT OUTER JOIN ext_user_maps ON ext_user_maps.user_id=users.id (必要ならarticlesへのWHERE句); |aid|cid|ext_id| |1 |1 |10 | |1 |2 |10 | |1 |2 |11 |
これを実現するRails(ActiveRecord)のコードは次のようになります(チートシート参照)。
class Article has_many :comments end class User has_one :ext_user_map end Article.find(:all, :include=>{:comments=>{:user=>:ext_user_map}})
ここまで苦労して報われるとしたら、上のfind()の戻り値をarticlesで受けた時、articles.comments[0].user.ext_user_map.idとして、コメントごとの外部システムユーザIDを簡単に得られることです(苦労したおかげで、この時にSQLは発行されません)。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/activerecord-for-join/tbping
Re:Rails(ActiveRecord)のJOINのイディオム
結論から言うと、ORMは使った方がいいです。SQL(SELECT文)を書くのが例え簡単でも、結果セットからオブジェクトを生成するコードを書くのは不毛だからです。SELECT文と同等のfind呼び出しを考えるのも不毛に感じますが、これは結構楽しいのです。一方、結果セットからオブジェクトを生成するコードを書くのは、不毛なだけでなく、つまらないのです。この違いは決定的です。
Re:Rails(ActiveRecord)のJOINのイディオム
勉強になります。ありがとうございます。m(_ _)m