Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsの技: Attributes APIでPOROの属性を自動的にキャストする(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

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,
]

以下のようにすることで、castserializeを実装したカスタム型も登録できます。

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: ActiveRecord標準のattributes APIドキュメント(翻訳)


  1. ActiveModel::Attributes# :nodoc:と記載されていることを指していると思われます(メソッドの可視性 -- API ドキュメント作成ガイドライン)。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。