Personal tools
You are here: Home ブログ 井上 Rails(ActiveRecord)のJOINのイディオム
« December 2010 »
Su Mo Tu We Th Fr Sa
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Recent entries
Apache2.4のリリース予定は来年(2011年)初め(あくまで予定) inoue 2010-12-23
Herokuの発音 inoue 2010-12-20
雑誌記事「ソフトウェア・テストPRESS Vol.9」の原稿公開 inoue 2010-12-18
IPA未踏のニュース inoue 2010-12-15
労基法とチキンゲーム inoue 2010-12-06
フロントエンドエンジニア inoue 2010-12-03
ASCII.technologies誌にMapReduceの記事を書きました inoue 2010-11-25
技術評論社パーフェクトシリーズ絶賛発売中 inoue 2010-11-24
雑誌連載「Emacsのトラノマキ」の原稿(part8)公開 inoue 2010-11-22
RESTの当惑 inoue 2010-11-22
「プログラマのためのUXチートシート」を作りました inoue 2010-11-19
「ビューティフルコード」を読みました inoue 2010-11-16
Categories
カテゴリなし
 
Document Actions

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は発行されません)。

The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/inoue/activerecord-for-join/tbping

Re:Rails(ActiveRecord)のJOINのイディオム

Posted by inoue at 2008-06-20 01:06
記事を読んだ人から、「ORMって使わない方がいいんですか?」と聞かれてしまいました。ちょっと煽りすぎました。

結論から言うと、ORMは使った方がいいです。SQL(SELECT文)を書くのが例え簡単でも、結果セットからオブジェクトを生成するコードを書くのは不毛だからです。SELECT文と同等のfind呼び出しを考えるのも不毛に感じますが、これは結構楽しいのです。一方、結果セットからオブジェクトを生成するコードを書くのは、不毛なだけでなく、つまらないのです。この違いは決定的です。

Re:Rails(ActiveRecord)のJOINのイディオム

Posted by babydaemons at 2009-12-14 17:26
この逆引きチートシートは必携ですね!
勉強になります。ありがとうございます。m(_ _)m
Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.