気づけば随分と間が空いてしまい、ブログを書くのも久々です。ということで今日は小ネタ。 ActiveRecord の一部カラムのみを別オブジェクトとして切り出したいと思ったこと、ありませんか?
例えばこんなコード。
class Item < ApplicationRecord
# t.string :title
# t.text :description
# t.integer :amount
# t.string :currency
end
class Subscription < ApplicationRecord
# t.belongs_to :user
# t.integer :amount
# t.string :currency
belongs_to :user
end
item = Item.new(amount: 100, currency: 'JPY')
item.price # => { amount: 100, currency: 'JPY' }
subscription = Subscription.new(amount: 1000, currency: 'USD')
subscription.price # => { amount: 1000, currency: 'USD' }
別テーブルに切り出して正規化するには値の振れ幅が大きいし、何より無意味に has_one なテーブル作りまくってもしょうがなくね?というのが率直な感想で如何ともしがたい。みたいなとき。
もっと言うと今回の Price の例なら僕は validation も書けるようになってほしい。
ということで作りました。こちらです。
partialize.rb
は app/models/concerns
以下に、 partial_validator.rb
は app/validators
の下にでも置いて下さい。これを include すると、こんな書き方が出来ます。
class Price < Partializable::Model
AVAILABLE_CURRENCY = %w[JPY USD]
attr_accessor :amount, :currency
validates :amount, numericality: { greater_than: 0 }
validates :currency, presence: true, inclusion: { in: AVAILABLE_CURRENCY }
def currency
self[:currency]&.upcase
end
end
class Item < ApplicationRecord
partialize :price, mapping: { amount: :amount } # 同名のカラムは省略可能
# partialize :price, class_name: 'Price' # クラス名を与えられる
# partialize :price, prefix: :price # price_amount, price_currency など、プレフィックスが揃ってるならマッピングはこうも書けます
validates :price, partial: { if: :persisted? }
end
price = Price.new(amount: 100, currency: 'JPY')
item = Item.new(price: price)
item.amount # => 100
item.currency # => 'JPY'
item.price.amount = 200
item.amount # => 200
ちゃんとオブジェクト経由で書き込んでも元レコードに書き込みされます。安心。別レコードの部分オブジェクトを別レコードの部分オブジェクトとして代入しても、値はコピーされるだけで、参照はちゃんと切れるので、そこも安心です。
こんな感じで使えると便利だなーって感じで作ったんですが、いかがでしょう。評判良さそうなら gem 化したいな。名前が微妙すぎるから、違う名前つけてあげたいけど……