Railsの技: 関連先レコードがないデータをwhere.missingで検索する(翻訳)
否定は一般には立証できませんが(訳注: 消極的事実の証明)、データベースに否定のクエリを投げることについてはどうでしょうか。クエリはデータを検索するために書くのが普通ですが、逆にデータが「存在しない」ことを検出するためにクエリを書くこともあります。
生SQLで言うなら、LEFT OUTER JOIN
とNULL
チェックを組み合わせることで、特定の関連付けを持たないレコードを検出できます。
利用法
Railsでは、上のようなSQLの概念をActive Recordで直接適用できます。
以下のモデルがあるとしましょう。
class Account < ApplicationRecord
has_many :recovery_email_addresses
end
リカバリーメールのバックアップが設定されていないAccount
を探索したいのであれば、以下のようなクエリを書くだけで問題なくできます。
Account.left_joins(:recovery_email_addresses).where(recovery_email_addresses: { id: nil })
# SELECT "accounts".* FROM "accounts" LEFT OUTER JOIN "recovery_email_addresses" ON "recovery_email_addresses"."account_id" = "accounts"."id" WHERE "recovery_email_addresses"."id" IS NULL
しかしこれではコードが長くなります。Rails 6.1からは、同じクエリを以下のようにずっと簡潔に書けるようになりました。
Account.where.missing(:recovery_email_addresses)
# SELECT "accounts".* FROM "accounts" LEFT OUTER JOIN "recovery_email_addresses" ON "recovery_email_addresses"."account_id" = "accounts"."id" WHERE "recovery_email_addresses"."id" IS NULL
生成されるSQLは同一になりますし、コードもずっと読みやすくなります。以下のようにbelongs_to
リレーションシップでも同じことができます。
class Contract < ApplicationRecord
belongs_to :promiser, class_name: "User"
belongs_to :promisee, class_name: "User"
belongs_to :beneficiary, optional: true, class_name: "User"
end
Contract.where.missing(:promiser) # promiserがないcontact
Contract.where.missing(:promiser, :beneficiary) # promiserもbeneficiaryもないcontact
以下のようにmissing
を通常のActive Recordメソッドチェーンと組み合わせることもできます。
Contact.where("amount > ?", 1200).where.missing(:promiser)
Contact.where(signed: true).where.missing(:beneficiary)
参考資料
- Railsのマージ済みPR: Finding Orphan Records by tomrossi7 · Pull Request #34727 · rails/rails
-
Rails APIドキュメント:
WhereChain#missing
概要
原著者の許諾を得て翻訳・公開いたします。
#missing
はRails 6.1で追加された機能です。参考: 週刊Railsウォッチ(20190513)