ActiveRecordモデルの拡張方法にはいくつか選択肢があるので、どういった方法を選ぶのが適切かよくよく考えなおしてみたいと思います
今回のストーリー
新規ユーザーは必ず無料のサンプルを受け取ることができるので、ユーザーが作成されたときに、無料サンプル用の注文を作成したい
ただし、受け取れる無料サンプルは一人一つまでで、無料サンプルを提供する販売元と無料サンプルはコードで指定したい
販売元コード: DEFAULT
商品コード: JOIN
2種類の拡張方法を試す
1.Scopeを使った拡張
Scopeを使った拡張ではクラス本体が拡張される
そのクラスを参照する全ての場合に利用できる
buildやcreateにも利用することが可能
ActiveRecord::Scoping::Named::ClassMethods
まずは特定条件でレコードがフィルタリングできることを確認します
実装
class Order < ApplicationRecord
belongs_to :customer
belongs_to :item
# 発送前の注文をフィルタするスコープを定義
scope :not_yet_shipped, -> { where("shipment_date < ?", Time.zone.now)}
end
検証
下記の2つの呼び出し方法で同じSQLが発行されていることを確認します
> Order.not_yet_shipped
Order Load (0.5ms) SELECT `orders`.* FROM `orders` WHERE (shipment_date < '2022-09-17 01:23:32.786304')
> Customer.last.orders.not_yet_shipped
Order Load (0.5ms) SELECT `orders`.* FROM `orders` WHERE (shipment_date < '2022-09-17 01:23:32.786304')
次に、スコープからレコードが作成できることも確認します
ただし、この場合には、「無料のサンプルは一人ひとつまで」という制約を入れたい場合に、scopeの中だけでは拡張できないので、create_join_bonus
をメソッドとして定義することになります
実装
class Order < ApplicationRecord
belongs_to :customer
belongs_to :item
# キャンペーンの注文をフィルタするスコープの定義
scope :campaign_order, -> {
where(item: Item.where({seller: Seller.where(code: 'DEFAULT').first, campaign_code: 'JOIN'}).last)
}
# 無料サンプル用のレコードを作成
def create_join_bonus
find_or_create_by!(item: find_join_bonus) do |order|
order.shipment_date = Time.zone.now + 3.days
end
end
# 無料サンプル用のレコードを検索
def find_join_bonus
seller = Seller.where(code: 'DEFAULT').first
Item.where({seller: seller, campaign_code: 'JOIN'}).last
end
end
検証
campaign_orderのスコープからだと何件も同じレコードを作成できてしまう抜け道があることを確認する
> Customer.last.orders.campaign_order.create
=> #<Order:0x00007fc84e190c30
id: 3,
customer_id: 3,
item_id: 1,
shipment_date: nil,
created_at: Sat, 17 Sep 2022 01:07:54 UTC +00:00,
updated_at: Sat, 17 Sep 2022 01:07:54 UTC +00:00>
> Customer.last.orders.campaign_order
=> [#<Order:0x00007fc84e23bf68
id: 2,
customer_id: 3,
item_id: 1,
shipment_date: nil,
created_at: Sat, 17 Sep 2022 01:07:52 UTC +00:00,
updated_at: Sat, 17 Sep 2022 01:07:52 UTC +00:00>,
#<Order:0x00007fc84e23be00
id: 3,
customer_id: 3,
item_id: 1,
shipment_date: nil,
created_at: Sat, 17 Sep 2022 01:07:54 UTC +00:00,
updated_at: Sat, 17 Sep 2022 01:07:54 UTC +00:00>]
2.AssociationExtentionを使ったhas_manyリレーションの拡張
Active Record の関連付け - Railsガイド
Orderモデルは一般的なECサイトなどでは、必然的に非常にたくさんの機能を抱えることになります
「無料のサンプルは一人ひとつまで」という業務要件は、Customerモデルの1行(=一人)に対して、紐づく独自要件なのでキャンペーンの要件としてひとくくりにしてて、モジュールに切り出したいと思います
そこで、has_manyリレーション内で、新たに作成されたCampaignExtension
を使って拡張したいと思います
この場合には、クラス本体は拡張されず、orders経由でレコードが作成される場合にのみ拡張されます
has_oneやbelongs_toのときには利用できないので注意しましょう
実装
class Customer < ApplicationRecord
has_many :orders, -> { extending CampaignExtension },
class_name: 'Order', inverse_of: :customer
end
app/models/concerns/campaign_extension.rb
を新たに作成します
create_join_bonusというメソッドに「無料のサンプルは一人ひとつまで」という制限をまとめて実装してしまいましょう
module CampaignExtension
def create_join_bonus
find_or_create_by!(item: find_join_bonus) do |order|
order.shipment_date = Time.zone.now + 3.days
end
end
def find_join_bonus
seller = Seller.where(code: 'DEFAULT').first
Item.where({seller: seller, campaign_code: 'JOIN'}).last
end
end
order
モデルからは、ロジックを削除して、このようにシンプルな形に戻しましょう
class Order < ApplicationRecord
belongs_to :customer
belongs_to :item
# 発送前の注文をフィルタするスコープを定義
scope :not_yet_shipped, -> { where("shipment_date < ?", Time.zone.now)}
検証検証
何度呼び出しても重複レコードが作成されないことを確認しましょう
> Customer.last.orders.create_join_bonus
=> #<Order:0x00007fd528524568
id: 1,
customer_id: 2,
item_id: 1,
shipment_date: Sun, 18 Sep 2022 15:09:10 UTC +00:00,
created_at: Thu, 15 Sep 2022 15:09:10 UTC +00:00,
updated_at: Thu, 15 Sep 2022 15:09:10 UTC +00:00>
> Customer.last.orders
=> [#<Order:0x00007fc84e23bf68
id: 2,
customer_id: 3,
item_id: 1,
shipment_date: nil,
created_at: Sat, 17 Sep 2022 01:07:52 UTC +00:00,
updated_at: Sat, 17 Sep 2022 01:07:52 UTC +00:00>,
#<Order:0x00007fc84e23be00
id: 3,
customer_id: 3,
item_id: 1,
shipment_date: nil,
created_at: Sat, 17 Sep 2022 01:07:54 UTC +00:00,
updated_at: Sat, 17 Sep 2022 01:07:54 UTC +00:00>]
「無料のサンプルは一人ひとつまで」という制限がActiveRecordの機能を通して実現できたというよりは、制限を置く場所を変えてあげると、あとからメソッドを利用する人が間違えにくくなるという、認識を持っていただけれと思います
また、ドメインとモデルの分離をしてFat Modelをダイエットさせるというときにも役に立つと思います