やりたいこと
やりたいことはタイトルの通りではあるのですが、1つの親テーブルと2つ以上の子テーブルという構造のデータがあったとして、親テーブルを介して複数の子テーブルのレコードを一緒くたに取得してViewに表示したいです。
え、意味がわからない?
ですよねw
背景を説明したらもう少しわかるかも...↓
背景
メルマガシステムを作っているとします。
そこには、admins, authors, readersというテーブルがあって、お知らせ(notifications、略してnotifs)を通知する機能があります。
admins => authors
(運営から著者へのお知らせ)
admins => readers
(運営から読者へのお知らせ)
authors => readers(購読フォローしている著者から読者へのお知らせ)
テーブルの概念図はこのような感じ↓
// tables // columns
------------------------------------------------------
notifs // be_published_at, status
┣━ admin_to_author_notifs // title, body
┣━ admin_to_reader_notifs // title, body
┗━ author_to_reader_notifs // title, body
親のnotifsは公開日時(be_published_at)や公開/非公開(status)のデータを持ち、子テーブルたちをhas_oneしています。
子テーブルたちは件名(title)や本文(body)のデータを持ち、notifにbelongs_toしています。
読者(readers)へのお知らせが2種類あります。admin_to_reaer_notifs
とauthor_to_reader_notif
です。これらをnotifsテーブルを介して同時に取得して、読者に表示してあげたいです。色々な方法が考えられますが、今回は親テーブルを通して、一緒くたに取得してViewに渡したい、というわけです。
実装!
Controller
こんな感じで一行でデータを取得したいです。
# controllers/notif_controller.rb
def index
@notifs = Notifs.any_to_reader_notifs
end
3つあるnotifの子テーブルから読者であるreader
向けの2つだけ取得したいので、any_to_reader_notifs
というスコープを作ります。
# models/notif.rb
scope :any_to_reader_notifs, lambda { |author|
left_joins(:admin_to_reader_notif, :author_to_reader_notif)
.includes(:admin_to_reader_notif, :author_to_reader_notif)
.where.not("admin_to_reader_notifs.id": nil)
.or(where.not("author_to_reader_notifs.id": nil)
.where("author_to_reader_notifs.author_id": author.id))
}
left_joins
,includes
メソッドは引数を2つ取れる!→この記事の肝はここです。これを知りたかったのですが、特にleft_joinsは、ドキュメント見てもググっても分からずで...。(記憶が正しければ、つよい先輩も「こんなことできるんだ」とやや驚かれてました。)left_joins
はLEFT OUTER JOIN
という外部結合のSQLを発行します。このLEFT OUTER JOIN
はNULLになるレコードも取ってきます。内部結合では、authour_to_reader_notif
のレコードを取得してくれません。admin_to_reader_notif
がNULLだからです。includes
はN+1問題を避けるために関連するテーブルを同時に取ってくるメソッド。Pikawakaの解説がいつもながらわかりやすい!→【Rails】 N+1問題をincludesメソッドで解決しよう!or.()
で並列で別の条件を付与できます。
これが発行するSQLがこちら。
SELECT
"notifs".*
FROM
"notifs"
LEFT OUTER JOIN "admin_to_reader_notifs" ON "admin_to_reader_notifs"."notif_id" = "notifs"."id"
LEFT OUTER JOIN "author_to_reader_notifs" ON "author_to_reader_notifs"."notif_id" = "notifs"."id"
WHERE
(
"admin_to_reader_notifs"."id" IS NOT NULL
OR "author_to_reader_notifs"."id" IS NOT NULL
AND "author_to_reader_notifs"."author_id" = $ 1
)
View
レコードを一緒くたに取ってくると、Viewもなるべく簡潔に書くことができます。
<% @notifs.each do |notif| %>
<%= notif.decorate.title %>
<%= notif.decorate.published_by(@author) %>
<%= notif.publish_at.to_ymdhm %>
<%= notif.decorate.body %>
<% end %>
↑ご覧になってお分かりのとおり、decoratorのメソッドを使いたいです。(draper)
お知らせがadminからかauthorからかの判定をViewに書いてしまうとごちゃごちゃしてしまうからです。
下のようにdecoratorファイルを作ります。
class NotifDecorator < Draper::Decorator
delegate_all
def title
if admin_to_reader_notif.present?
admin_to_reader_notif.title
elsif school_to_reader_notf.present?
school_to_reader_notif.title
end
end
def body
if admin_to_reader_notif.present?
admin_to_reader_notif.body
elsif school_to_reader_notif.present?
school_to_reader_notif.body
end
end
def published_by(author)
if admin_to_reader_notif.present?
"運営より"
elsif author_to_reader_notif.present?
author.name
end
end
end
最後に
何かご指摘などございましたら、コメントいただけるとありがたいです!
Related Posts
Yuhei Okazaki
2021/12/16
kakudaisuke
2021/11/26
kta-m
2021/09/07