aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/attributes.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/attributes.rb')
-rw-r--r--activerecord/lib/active_record/attributes.rb178
1 files changed, 153 insertions, 25 deletions
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 7cb6b075a0..c8979a60d7 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,7 +1,9 @@
module ActiveRecord
- module Attributes # :nodoc:
+ # See ActiveRecord::Attributes::ClassMethods for documentation
+ module Attributes
extend ActiveSupport::Concern
+ # :nodoc:
Type = ActiveRecord::Type
included do
@@ -9,21 +11,32 @@ module ActiveRecord
self.attributes_to_define_after_schema_loads = {}
end
- module ClassMethods # :nodoc:
- # Defines or overrides an attribute on this model. This allows customization of
- # Active Record's type casting behavior, as well as adding support for user defined
- # types.
+ module ClassMethods
+ # Defines an attribute with a type on this model. It will override the
+ # type of existing attributes if needed. This allows control over how
+ # values are converted to and from SQL when assigned to a model. It also
+ # changes the behavior of values passed to
+ # ActiveRecord::QueryMethods#where. This will let you use
+ # your domain objects across much of Active Record, without having to
+ # rely on implementation details or monkey patching.
+ #
+ # +name+ The name of the methods to define attribute methods for, and the
+ # column which this will persist to.
+ #
+ # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
+ # to be used for this attribute. See the examples below for more
+ # information about providing custom type objects.
#
- # +name+ The name of the methods to define attribute methods for, and the column which
- # this will persist to.
+ # ==== Options
+ # The following options are accepted:
#
- # +cast_type+ A type object that contains information about how to type cast the value.
- # See the examples section for more information.
+ # +default+ The default value to use when no value is provided. If this option
+ # is not passed, the previous default value (if any) will be used.
+ # Otherwise, the default will be +nil+.
#
- # ==== Options
- # The options hash accepts the following options:
+ # +array+ (PG only) specifies that the type should be an array (see the examples below).
#
- # +default+ is the default value that the column should use on a new record.
+ # +range+ (PG only) specifies that the type should be a range (see the examples below).
#
# ==== Examples
#
@@ -44,42 +57,153 @@ module ActiveRecord
# store_listing.price_in_cents # => BigDecimal.new(10.1)
#
# class StoreListing < ActiveRecord::Base
- # attribute :price_in_cents, Type::Integer.new
+ # attribute :price_in_cents, :integer
# end
#
# # after
# store_listing.price_in_cents # => 10
#
- # Users may also define their own custom types, as long as they respond to the methods
- # defined on the value type. The +type_cast+ method on your type object will be called
- # with values both from the database, and from your controllers. See
- # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your
- # type objects inherit from an existing type, or the base value type.
+ # A default can also be provided.
+ #
+ # create_table :store_listings, force: true do |t|
+ # t.string :my_string, default: "original default"
+ # end
+ #
+ # StoreListing.new.my_string # => "original default"
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :my_string, :string, default: "new default"
+ # end
+ #
+ # StoreListing.new.my_string # => "new default"
+ #
+ # Attributes do not need to be backed by a database column.
+ #
+ # 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
+ # }
+ #
+ # ==== Creating Custom Types
+ #
+ # Users may also define their own custom types, as long as they respond
+ # to the methods defined on the value type. The method
+ # +deserialize+ or +cast+ will be called on
+ # your type object, with raw input from the database or from your
+ # controllers. See ActiveRecord::Type::Value for the expected API. It is
+ # recommended that your type objects inherit from an existing type, or
+ # from ActiveRecord::Type::Value
#
# class MoneyType < ActiveRecord::Type::Integer
- # def type_cast(value)
+ # def cast(value)
# if value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
- # price_in_dollars * 100
+ # super(price_in_dollars * 100)
# else
- # value.to_i
+ # 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, MoneyType.new
+ # attribute :price_in_cents, :money
# end
#
# store_listing = StoreListing.new(price_in_cents: '$10.00')
# store_listing.price_in_cents # => 1000
+ #
+ # For more details on creating custom types, see the documentation for
+ # ActiveRecord::Type::Value. For more details on registering your types
+ # to be referenced by a symbol, see ActiveRecord::Type.register. You can
+ # also pass a type object directly, in place of a symbol.
+ #
+ # ==== Querying
+ #
+ # When ActiveRecord::QueryMethods#where is called, it will
+ # use the type defined by the model class to convert the value to SQL,
+ # calling +serialize+ on your type object. For example:
+ #
+ # class Money < Struct.new(:amount, :currency)
+ # end
+ #
+ # class MoneyType < Type::Value
+ # def initialize(currency_converter)
+ # @currency_converter = currency_converter
+ # end
+ #
+ # # value will be the result of +deserialize+ or
+ # # +cast+. Assumed to be in instance of +Money+ in
+ # # this case.
+ # def serialize(value)
+ # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
+ # value_in_bitcoins.amount
+ # end
+ # end
+ #
+ # ActiveRecord::Type.register(:money, MoneyType)
+ #
+ # class Product < ActiveRecord::Base
+ # currency_converter = ConversionRatesFromTheInternet.new
+ # attribute :price_in_bitcoins, :money, 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 Tracking
+ #
+ # The type of an attribute is given the opportunity to change how dirty
+ # tracking is performed. The methods +changed?+ and +changed_in_place?+
+ # will be called from ActiveModel::Dirty. See the documentation for those
+ # methods in ActiveRecord::Type::Value for more details.
def attribute(name, cast_type, **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])
+ self.attributes_to_define_after_schema_loads =
+ attributes_to_define_after_schema_loads.merge(
+ name => [cast_type, options]
+ )
end
+ # This is the low level API which sits beneath +attribute+. It only
+ # accepts type objects, and will do its work immediately instead of
+ # waiting for the schema to load. Automatic schema detection and
+ # ClassMethods#attribute both call this under the hood. While this method
+ # is provided so it can be used by plugin authors, application code
+ # should probably use ClassMethods#attribute.
+ #
+ # +name+ The name of the attribute being defined. Expected to be a +String+.
+ #
+ # +cast_type+ The type object to use for this attribute.
+ #
+ # +default+ The default value to use when no value is provided. If this option
+ # is not passed, the previous default value (if any) will be used.
+ # Otherwise, the default will be +nil+.
+ #
+ # +user_provided_default+ Whether the default value should be cast using
+ # +cast+ or +deserialize+.
def define_attribute(
name,
cast_type,
@@ -90,10 +214,14 @@ module ActiveRecord
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end
- def load_schema!
+ def load_schema! # :nodoc:
super
attributes_to_define_after_schema_loads.each do |name, (type, options)|
- define_attribute(name, type, **options)
+ if type.is_a?(Symbol)
+ type = ActiveRecord::Type.lookup(type, **options.except(:default))
+ end
+
+ define_attribute(name, type, **options.slice(:default))
end
end