- Ruby / Rails関連
Rails API: ActiveRecord::Attributes::ClassMethodsの#attributeと#define_attribute(翻訳)
概要
MITライセンスに基づいて翻訳します。
- 原文: ActiveRecord::Attributes::ClassMethods
- ファイル: activerecord/lib/active_record/attributes.rb
- Railsバージョン: 5.2.1
- ライセンス: MIT
y-yagiさんの以下の記事も参考になります。
ActiveRecord::Attributes::ClassMethods
のインスタンスpublicメソッド
attribute(name, cast_type = Type::Value.new, **options)
attribute
メソッドは、型を1つ持つ属性(attribute)をこのモデルに定義し、必要に応じて既存の属性の型を上書きします。このメソッドを用いて、モデルに代入される値をSQLと相互変換する方法を制御できます。また、ActiveRecord::Base.where
に渡される値の振る舞いも変更されます。これにより、実装の詳細やモンキーパッチに頼らずに、自分のドメインオブジェクトを多くのActive Recordで使えるようになります。
name
: 属性メソッドの定義に用いる名前や、この属性の永続化先となるカラムを定義します。-
cast_type
::string
や:integer
などのシンボルか、この属性で使われる型オブジェクトを指定します。独自の型オブジェクトを提供する方法についてはこの後のコード例をご覧ください。
オプション
**options
では以下のオプションを利用できます。
default
: 値が渡されない場合のデフォルト値です。このオプションが指定されていない場合、直前のデフォルト値があればそれがデフォルト値になり、ない場合はnil
がデフォルト値になります。-
array
: (PostgreSQL限定)arrayを型とすべきであることを指定します。 -
range
:(PostgreSQL限定)rangeを型とすべきであることを指定します(以下のコード例を参照)。
コード例
Active Recordが検出する型をオーバーライドできます。
# 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(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
はあなたのオブジェクトで呼び出され、データベースやコントローラからのraw入力を取ります。ここで期待されるAPIについてはActiveModel::Type::Value
を参照してください。利用する型オブジェクトは、既存の型かActiveModel::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
が呼び出されると、このモデルクラスで定義された型を用いて値をSQLに変換し、利用する型オブジェクト上でserialize
を呼び出します。次の例をご覧ください。
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トラッキングが行われると変更される可能性があります。changed?
メソッドやchanged_in_place?
メソッドはActiveModel::Dirty
から呼び出されます。これらのメソッドについて詳しくはActiveModel::Type::Value
のドキュメントをご覧ください。
- ソースコード
# File activerecord/lib/active_record/attributes.rb, line 195
def attribute(name, cast_type = Type::Value.new, **options)
name = name.to_s
reload_schema_from_cache
self.attributes_to_define_after_schema_loads =
attributes_to_define_after_schema_loads.merge(
name => [cast_type, options]
)
end
define_attribute( name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true )
define_attribute
メソッドは、attribute
メソッドを支える低レベルAPIであり、型オブジェクトのみを受け取って、スキーマが読み込まれるのを待たずにただちに動作します。自動スキーマ検出や#attribute
は、どちらも背後でdefine_attribute
を呼び出します。define_attribute
メソッドが提供されていることでプラグイン作者もこのメソッドを利用できますが、アプリのコードではdefine_attribute
メソッドではなくattribute
メソッドを使うべきです。
name
: 定義される属性名です。String
が期待されます。-
cast_type
: この属性で用いる型オブジェクト -
default
: 値が渡されない場合のデフォルト値です。このオプションが指定されていない場合、直前のデフォルト値があればそれがデフォルト値になり、ない場合はnil
がデフォルト値になります。procを渡すことも可能であり、その場合新しい値が必要になるたびにprocが1回呼び出されます。 -
user_provided_default
: デフォルト値をcast
やdeserialize
でキャストすべきかどうかを指定します。
- ソースコード
# File activerecord/lib/active_record/attributes.rb, line 223
def define_attribute(
name,
cast_type,
default: NO_DEFAULT_PROVIDED,
user_provided_default: true
)
attribute_types[name] = cast_type
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end
* GitHub