RailsのActiveRecordのmergeメソッドを使ってみる

こんにちは!kossyです!





さて、今回はRailsActiveRecordの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