RailsのActiveRecordのmergeメソッドで綺麗にクエリを書いてみた

こんにちは!kossyです!





さて、今回はRailsActiveRecordのmergeメソッドを使って、
SQLを組み立てる方法について、ブログに残してみたいと思います。



環境
Ruby 2.6.3
Rails 6.0.3
MacOS Catalina




前提

テーブル定義は以下とします。

・tenantsテーブル
・branchesテーブル
・usersテーブル
・diary_reportsテーブル

リレーションは以下とします。
・tenant has_many branches
・tenant has_many users
・branch has_many users
・User has_many diary_reports

カラムとして、以下を定義します。
・usersテーブルにrole(権限)カラムを定義。取りうる値は、general、hr、admin



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メソッドを役立たせるとするならば、

# Tenantが持つBranchに所属するUserのなかで、roleがhrのUserを持つBranchを取得する
Tenant.first.branches.joins(:users).merge(User.where(role: :hr))

みたいな感じでしょうか。



実践的な使い方

先ほど「otherが配列の場合、結果のレコードとotherの交差を表す配列を返します。」と説明しましたが、
ActiveRecordのor演算子と組み合わせることで、合併を表す配列を返すことも可能です。

# Tenantが持つBranchに所属するUserのなかで、roleがgeneralかhrのUserを持つBranchを取得する
Tenant.first.branches.joins(:users).merge(User.where(role: :hr).or(role: :general))

上記のようにorメソッドを用いることで、合併の挙動を表現することができます。



以下にもう一つ例を記載します。

# daily_reportsを持っていないUserがいるBranchを取得
Branch.joins(:users).merge(User.left_joins(:daliy_reports).where(daliy_reports: {user_id: nil}))

上記のようなクエリを組み立てることで、例えば「日報を持っていないユーザーがいる支社の人事に対して、リマインドメールを送信する」みたいなことが出来ます。


mergeメソッド、うまく使えば絞り込みの挙動をより簡潔に表現できそうです。





勉強になりました。




参考にさせていただいた記事
ActiveRecord の or は merge とセットで使え - Qiita
ActiveRecord::SpawnMethods