背景
ActiveRecordに定義されていたscopeでmergeを用いた記述方法があり、これは便利だなと思ったので記録しておきます
モデルケース
基本的なECサイトのモデルケースです
order.rb
class Order < ApplicationRecord
has_many :items
# 支払い済みの注文
scope :paid, -> { where(payment_status: 'paid') }
end
-
OrderはItemを複数保持できる -
Orderには、支払いステータスであるpayment_statusがあり、paidスコープが定義されている
item.rb
class Item < ApplicationRecord
belongs_to :order
# 在庫がある商品
scope :in_stock, -> { where('quantity > ?', 0) }
end
-
Itemモデルにはquantityという在庫を管理するカラムがある - 在庫がある
Itemを取得するためにin_stockというスコープが定義されている
mergeの使い方
例えば、支払い済みかつ在庫がある商品を取得したい時はこんな感じにかけます
order.rb
class Order < ApplicationRecord
has_many :items
scope :paid, -> { where(payment_status: 'paid') }
scope :ready_to_ship, -> { paid.joins(:item).merge(Item.in_stock) }
end
元々定義してあるpaidスコープでpaidステータスのデータを取りつつ、Itemの在庫があるデータです
なんでこの書き方が良いのか
当然、わざわざmerge使わなくていいじゃんとなります、例えば下記
order.rb
class Order < ApplicationRecord
has_many :items
scope :paid, -> { where(payment_status: 'paid') }
scope :ready_to_ship, -> { paid.joins(:item).where('quantity > ?', 0) }
end
itemをjoinして内部結合しているので可能です。
しかし、「在庫あり」という概念が変更になった場合はどうでしょうか?
例えば、「今現在の在庫はあるが、予約システムが導入されたので、予約済みの商品は配送できない」みたいなケース
item.rb
class Item < ApplicationRecord
belongs_to :order
# 在庫があり、予約されていない商品
scope :in_stock, -> { where('quantity > ?', 0).where(reserved: false) }
end
この場合、Orderに記述したscopeも変更をする必要があります。同じ処理を2箇所で利用しているので
しかしscope :ready_to_ship, -> { paid.joins(:item).merge(Item.in_stock) }このように定義しておけば、変更の手間が省けて保守性が高くなります。
感想
- 他モデルのスコープも使いまわせるの初めて知った
-
mergeの汎用性が高い
参考