Railsの技: Attributes APIでPOROの属性を自動的にキャストする(翻訳)
Railsアプリでは、ロジックを抽出してPORO(Plain-Old Ruby Object)にすることがよく行われます。しかしコントローラでparams
から直接これらのオブジェクトにデータを渡していることも多く、その場合はデータが文字列になってしまいます。
class SalesReport
attr_accessor :start_date, :end_date, :min_items
def initialize(params = {})
@start_date = params[:start_date]
@end_date = params[:end_date]
@min_items = params[:min_items]
end
def run!
# クールな何かを実行する
end
end
report = SalesReport.new(start_date: "2020-01-01", end_date: "2020-03-01", min_items: "10")
# しかしデータがstringとして保存されてしまう☹️
report.start_date
# => "2020-01-01"
report.min_items
# => "10"
普通ならstart_date
はDateで欲しいでしょうし、min_items
はIntegerで欲しいでしょう。以下のように初歩的な型キャストをコンストラクタに追加する方法も考えられます。
class SalesReport
attr_accessor :start_date, :end_date, :min_items
def initialize(params)
@start_date = Date.parse(params[:start_date])
@end_date = Date.parse(params[:end_date])
@min_items = params[:min_items].to_i
end
def run!
# クールな何かを実行する
end
end
これも悪くありませんが、RailsのAttributes APIを利用してキャストを自動化すればさらによくなります。
利用法
RailsのAttributes APIは、ActiveRecord
モデルの舞台裏で属性の型キャストに使われています。たとえば、データベースにdatetime
カラムがあるモデルにクエリをかけて取り出したRubyオブジェクトにはDateTime
フィールドがあります。これはAttributes APIの働きによるものです。
以下のようにActiveModel::Model
モジュールとActiveModel::Attributes
モジュールをinclude
すると、SalesReportモデルをいい感じに引き締められます。
class SalesReport
include ActiveModel::Model
include ActiveModel::Attributes
attribute :start_date, :date
attribute :end_date, :date
attribute :min_items, :integer
def run!
# クールな何かを実行する
end
end
report = SalesReport.new(start_date: "2020-01-01", end_date: "2020-03-01", min_items: "10")
# 属性がネイティブ型になってくれた!
report.start_date
# => Wed, 01 Jan 2020
report.min_items
# => 10
このパターンは、RailsアプリのForm ObjectやReport Objectはもちろん、その他モデル系のRubyクラスにありがちなコードを減らすのに最適です。型キャストを手作りで再実装せずに、フレームワークにお任せできます。
オプション
原注
このモジュールはRails 6.1の時点ではprivate APIです1。ご利用は自己責任でお願いします。
Attributes APIは、ほとんどのプリミティブな型キャストを自動的に処理します。基本的な型はすべて扱えます。
attribute :start_date, :date
attribute :max_size, :integer
attribute :enabled, :boolean
attribute :score, :float
すぐ使える型の全リストはactivemodel/lib/active_model/typeで参照できます。
この機能で最も素晴らしい点は、これらの型で入力を受け付ける方法がきわめて堅牢にできていることです。たとえば、boolean型の属性は、false
に相当する以下のどの値にも対応しています。
FALSE_VALUES = [
false, 0,
"0", :"0",
"f", :f,
"F", :F,
"false", :false,
"FALSE", :FALSE,
"off", :off,
"OFF", :OFF,
]
以下のようにすることで、cast
やserialize
を実装したカスタム型も登録できます。
ActiveRecord::Type.register(:zip_code, ZipCodeType)
class ZipCodeType < ActiveRecord::Type::Value
def cast(value)
ZipCode.new(value) # 扱いが特殊なZipCodeクラスにキャストする
end
def serialize(value)
value.to_s
end
end
さらに、Attributes APIを用いてデフォルト値も設定できます。
attribute :start_date, :date, default: 30.days.ago
attribute :max_size, :integer, default: 15
attribute :enabled, :boolean, default: true
attribute :score, :float, default: 9.75
参考資料
- Rails APIドキュメント: Attributes API
-
ブログ記事: Rails' hidden type system
関連記事
-
ActiveModel::Attributes
に# :nodoc:
と記載されていることを指していると思われます(メソッドの可視性 -- API ドキュメント作成ガイドライン)。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。