Rails5: ActiveRecord標準のattributes API(翻訳)

ActiveRecordに任意の属性を定義したり既存の属性を上書きしたりできるRails標準機能です。

概要

MITライセンスに基いて翻訳・公開いたします。

参考: Rails 5のActive Record attributes APIについて y-yagiさんの良記事です。

Rails5: ActiveRecord標準のattributes API(翻訳)

メソッド

定数

NO_DEFAULT_PROVIDED = Object.new

publicインスタンスメソッド

attribute(name, cast_type = Type::Value.new, **options)Link

型を持つ属性をこのモデルに定義します。必要な場合、既存の属性の型をオーバーライドします。これにより、モデルへの代入時に値がSQLと相互に変換される方法を制御できるようになります。また、ActiveRecord::Base.whereに渡される値の振る舞いも変更されます。これを使って、実装の詳細やモンキーパッチに依存せずに、ActiveRecordの多くに渡ってドメインオブジェクトを使えるようになります。

  • name” 属性メソッドの定義対象となるメソッド名、およびこれを適用するカラム。

  • cast_type: この属性で使われる:string:integerなどの型オブジェクト。利用例について詳しくは以下のカスタム型オブジェクトの情報をご覧ください。

オプション

以下のオプションを渡せます。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。

  • array:(PostgreSQLのみ)array型にならなければならないことを指定する(以下の例を参照)。

  • range:(PostgreSQLのみ)range型にならなければならないことを指定する(以下の例を参照)。

ActiveRecordで検出される型はオーバーライド可能です。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.decimal :price_in_cents
end
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end
store_listing = StoreListing.new(price_in_cents: '10.1')

# 変更前
store_listing.price_in_cents # => BigDecimal.new(10.1)

class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :integer
end

# 変更後
store_listing.price_in_cents # => 10

デフォルト値を指定することもできます。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.string :my_string, default: "original default"
end

StoreListing.new.my_string # => "original default"
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :my_string, :string, default: "new default"
end

StoreListing.new.my_string # => "new default"

class Product < ActiveRecord::Base
  attribute :my_default_proc, :datetime, default: -> { Time.now }
end

Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600

属性の背後にデータベースのカラムがなくても構いません。

# app/models/my_model.rb
class MyModel < ActiveRecord::Base
  attribute :my_string, :string
  attribute :my_int_array, :integer, array: true
  attribute :my_float_range, :float, range: true
end
model = MyModel.new(
  my_string: "string",
  my_int_array: ["1", "2", "3"],
  my_float_range: "[1,3.5]",
)
model.attributes
# =>
  {
    my_string: "string",
    my_int_array: [1, 2, 3],
    my_float_range: 1.0..3.5
  }

カスタム型の作成

値型で定義されるメソッドと対応していれば、独自の型を定義することもできます。この型オブジェクトでは、deserializeメソッドまたはcastメソッドが呼び出され、データベースやコントローラから受け取った生の入力を取ります。前提とされるAPIについてはActiveModel::Type::Valueをご覧ください。型オブジェクトは既存の型かActiveRecord::Type::Valueのいずれかを継承することが推奨されます。

class MoneyType < ActiveRecord::Type::Integer
  def cast(value)
    if !value.kind_of?(Numeric) && value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      super(price_in_dollars * 100)
    else
      super
    end
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :money
end
store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

カスタム型の作成について詳しくは、ActiveModel::Type::Valueのドキュメントをご覧ください。型をシンボルで参照できるように登録する方法については、ActiveRecord::Type.registerをご覧ください。シンボルの代わりに型オブジェクトを直接渡すこともできます。

クエリ

ActiveRecord::Base.whereが呼び出されると、そのモデルクラスで定義された型が使われ、型オブジェクトでserializeを呼んで値がSQLに変換されます。次の例をご覧ください。

class Money < Struct.new(:amount, :currency)
end
class MoneyType < Type::Value
  def initialize(currency_converter:)
    @currency_converter = currency_converter
  end

  # deserializeまたはcastの結果が値になる
  # ここではMoneyのインスタンスになることが前提
  def serialize(value)
    value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
    value_in_bitcoins.amount
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/product.rb
class Product < ActiveRecord::Base
  currency_converter = ConversionRatesFromTheInternet.new
  attribute :price_in_bitcoins, :money, currency_converter: currency_converter
end
Product.where(price_in_bitcoins: Money.new(5, "USD"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.02230

Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.03412

dirtyトラッキング

属性の型には、dirtyトラッキングの実行方法を変更する機会が与えられます。ActiveModel::Dirtychanged?changed_in_place?が呼び出されます。これらのメソッドについて詳しくはActiveModel::Type::Valueをご覧ください。

define_attribute( name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true )Link

これはattributeの背後にある低レベルAPIです。型オブジェクトのみを受け取り、スキーマの読み込みを待たずに即座に動作します。自動スキーマ検出と#attributeはどちらもこのメソッドを背後で呼び出します。このメソッドが提供されていることでプラグイン作者によって使われる可能性もありますが、おそらくアプリのコードで#attributeを使うべきです。

  • name: 定義される属性の名前。Stringで定義します。

  • cast_type: この属性で使う型オブジェクト。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。procを渡すことも可能であり、新しい値が必要になるたびにprocが1度ずつ呼び出される。

  • user_provided_default: デフォルト値がcastdeserializeでキャストされるべきかどうかを指定。

  • GitHubソース

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ