概要
Railsで実装している際LEFT OUTER JOINを色々試したので、まとめました。
(※2016/03/07 その6追加しました)
ただ、やりたい事自体は実現できていません。
有識者の方、コメントお願いします。
やりたいこと
Facebookのような投稿に対しユーザーが「いいね」を押すようなWEBアプリを作成しています。
テーブルとmodelは以下の3つで構成しています。
- Post id[PK], user_id, text
- User id[PK], name
- Good id[PK], post_id, user_id
Facebookみたいに、POSTの一覧ページにて、自分が「いいね」を押しているか判るようにしたいので、
以下のようなクエリを発行したい。
かつ、
eager_loadで実行することで、クエリ発行数を減らしたい。
という条件があります。
SELECT * FROM posts
LEFT OUTER JOIN goods
ON goods.post_id = posts.id AND goods.user_id = 3
※goods.user_id = 3 のuser_idの指定は任意の値を与えたい。
その1 .joins()を利用する。
user_id = 3
@posts = Post
.joins("LEFT OUTER JOIN goods ON posts.id = goods.post_id AND goods.user_id = #{user_id}")
一応、クエリ自体は発行できました。
ただ、キャッシュされていないので、N+1問題が残ります。
その2 .joins() + arel を利用する。
user_id = 3
good = Good.arel_table
good_condition = post
.join(good, Arel::Nodes::OuterJoin)
.on(post[:id].eq(good[:post_id]), good[:user_id].eq(user_id))
.join_sources
@posts = Post.joins(good_condition)
その1の改良版で、ArelというActiveRecordの基盤?を使っているので、DBの方言に起因する実行問題がなくなります。
ただ、こちらもキャッシュされないので、N+1問題が残ります。
その3 eager_load + where を利用する。
user_id = 3
@posts = Post.eager_load(:goods)
.where(good[:user_id].eq(user_id))
eager_loadを使っているので、クエリは1回だけです。
ただ発行されるクエリが
SELECT * FROM
posts
LEFT OUTER JOINgoods
ONgoods
.post_id
=posts
.id
WHEREgoods
.user_id
= 3
と、絞り込まれてINNER JOINしたみたいになってます。
その4 eager_load + active_record の アソシエーションでのラムダ式 を利用する。
今までは、単純なhas_manyやbelong_toしか利用してませんでしたが、
アソシエーションでラムダ式を利用してみました。
@posts = Post.eager_load(:goods)
has_many :goods, -> { where(user_id: 3) }
とりあえず、user_idは3でべた書き。
SELECT * FROM
posts
LEFT OUTER JOINgoods
ONgoods
.post_id
=posts
.id
ANDgoods
.user_id
= 3
期待通りのクエリが発行されて、かつeager_loadになっている。
ただし、user_idは固定なのでダメ
その5 eager_load + active_record の アソシエーションでのラムダ式 + 変数 を利用する。
基本的にやってる事はその4と一緒です。
@posts = Post.eager_load(:goods)
has_many :goods, ->(post_user_id) { where(user_id: post_user_id) }
で、これどうやって、post_user_idを渡すの?
やり方全然判らないんですけど…
ちなみに実行すると
SELECT * FROM
posts
LEFT OUTER JOINgoods
ONgoods
.post_id
=posts
.id
ANDgoods
.user_id
= NULL
上記のクエリが発行されます。
その6 eager_load + joinsのstring型を利用する。
@posts = Post.eager_load(:goods).joins("AND goods.user_id = #{user_id}")
このやり方だと、プレイスホルダは使えてませんが、一応要件は満たしてます。
ただ、やり方が裏技っぽい気がするので、将来的には少し心配です。
あと、joinsが1テーブルしか使えません。
以下のように複数テーブルに対して使うと、うまく動きません。
@posts = Post.eager_load(:goods).joins("AND goods.user_id = #{user_id}")
.eager_load(:goods2).joins("AND goods2.user_id = #{user_id}")
HELP募集中
というわけで、色々試しましたが、どれも要件を満たしません。
どなたか、良い解決方法がありましたら、ご教示お願いします。
preloadとかの方が良いのかなぁ。