Personal tools
You are here: Home ブログ 井上 Rails(ActiveRecord)のJOINのイディオム
« August 2008 »
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 comments
Re:Software Design 2008年2月号「Emacsマスターへの道」の原稿を公開 elim 2008-07-25
Re:Rails(ActiveRecord)のJOINのイディオム inoue 2008-06-20
Re:「ピアレビュー」を読みました Anonymous User 2008-05-12
Re:「ピアレビュー」を読みました inoue 2008-05-10
Re:「ピアレビュー」を読みました Anonymous User 2008-05-09
Re:「ピープルウェア」再読 inoue 2008-04-20
Re:僅か30分で3つのバグ - Rubyの落し穴 - inoue 2008-04-20
Re:僅か30分で3つのバグ - Rubyの落し穴 - rubikitch 2008-04-19
Re:ソフトウェアインスペクションの試行 horii 2008-03-31
Re:「ピープルウェア」再読 anaka 2008-03-31
Re:WEB+DB Press Vol.43の記事への指摘 yanagisawa 2008-03-25
Re:マルスケと月刊I/O あなか 2008-03-23
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呼び出しを考えるのも不毛に感じますが、これは結構楽しいのです。一方、結果セットからオブジェクトを生成するコードを書くのは、不毛なだけでなく、つまらないのです。この違いは決定的です。
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.