Top View


Author kakudaisuke

1つの親テーブルに対して2つの子テーブルという構造からデータを一緒くたに取得するには?(Rails、left_joins)

2021/10/01

やりたいこと

やりたいことはタイトルの通りではあるのですが、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_notifsauthor_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_joinsLEFT 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

最後に

何かご指摘などございましたら、コメントいただけるとありがたいです!

kakudaisuke

kakudaisuke

Twitter X

IoT