こんにちは!kossyです!
さて、今回はRailsのActiveRecordのmergeメソッドを使って、
ActiveRecord Queryを組み立てる方法について、ブログに残してみたいと思います。
環境
Ruby 2.6.3
Rails 6.0.3
MacOS Catalina
前提
テーブル定義は以下とします。
・tenantsテーブル
・branchesテーブル
・usersテーブル
・daily_reportsテーブル
リレーションは以下とします。
・tenant has_many branches
・tenant has_many users
・branch has_many users
・User has_many daily_reports
カラムとして、以下を定義します。
・usersテーブルにrole(権限)カラムを定義。取りうる値は、general、hr、admin の3つとします。
mergeメソッドの基本
まずは公式APIドキュメントを見てみましょう。
Merges in the conditions from other, if other is an ActiveRecord::Relation. Returns an array representing the intersection of the resulting records with other, if other is an array.
otherがActiveRecord :: Relationの場合、otherの条件にマージします。
otherが配列の場合、結果のレコードとotherの交差を表す配列を返します。
Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # Performs a single join query with both where conditions. # 両方のwhere条件で単一の結合クエリを実行します。 recent_posts = Post.order('created_at DESC').first(5) Post.where(published: true).merge(recent_posts) # Returns the intersection of all published posts with the 5 most recently created posts. # 公開されたすべての投稿と最近作成された5つの投稿の共通部分を返します。 # (This is just an example. You'd probably want to do this with a single query!) # これはほんの一例です。 あなたには、これを1つのクエリで実行することをお勧めします。
ふむふむ、「otherが配列の場合、結果のレコードとotherの交差を表す配列を返します。」が肝ですね。
「交差」なので、Rubyの条件演算子で書くと、A && B に合致するレコードが返り値になるということですね。いわゆる「積集合」です。
ちなみに「和集合」は「合併」と言われることが多いです。Ruby で表現するとしたら、A || B ですね。
TypeScriptでは「交差型(Intersection Types)」と「合併型(Union Types)」と言いますので、合併や交差という単語は一般的なんだと思われます。(間違ってたら教えてください)
前提で示したテーブル構成でmergeメソッドを役立たせるとするならば、
# Branchに所属するUserの中で、roleがhrのUserを持つBranchを取得する $ Branch.joins(:users).merge(User.where(role: :hr)).to_sql => "SELECT \"branches\".* FROM \"branches\" INNER JOIN \"users\" ON \"users\".\"branch_id\" = \"branches\".\"id\" WHERE \"users\".\"role\" = 1 "
みたいな感じでしょうか。
実践的な使い方
先ほど「otherが配列の場合、結果のレコードとotherの交差を表す配列を返します。」と説明しましたが、
ActiveRecordのor演算子と組み合わせることで、合併を表す配列を返すことも可能です。
# Branchに所属するUserの中で、roleがgeneralかhrのUserを持つBranchを取得する $ Branch.joins(:users).merge(User.where(role: :hr).or(User.where(role: :general))).distinct.to_sql => "SELECT DISTINCT \"branches\".* FROM \"branches\" INNER JOIN \"users\" ON \"users\".\"branch_id\" = \"branches\".\"id\" WHERE (\"users\".\"role\" = 1 OR \"users\".\"role\" = 2) "
上記のようにorメソッドを用いることで、合併の挙動を表現することができます。
以下にもう一つ例を記載します。
# 日報を持っていないUserがいるBranchを取得 $ Branch.joins(:users).merge(User.left_joins(:daily_reports).where(daily_reports: {user_id: nil})).distinct.to_sql => "SELECT DISTINCT \"branches\".* FROM \"branches\" INNER JOIN \"users\" ON \"users\".\"branch_id\" = \"branches\".\"id\" LEFT OUTER JOIN \"daily_reports\" ON \"daily_reports\".\"user_id\" = \"users\".\"id\" WHERE \"daily_reports\".\"user_id\" IS NULL "
上記のようなクエリを組み立てることで、例えば「日報を持っていないユーザーがいる支社の人事に対して、リマインドメールを送信する」みたいなことが出来ます。
mergeメソッド、うまく使えば絞り込みの挙動をより簡潔に表現できそうです。
勉強になりました。
参考にさせていただいた記事
ActiveRecord の or は merge とセットで使え - Qiita
ActiveRecord::SpawnMethods