attributes.rb 8.2 KB
Newer Older
1
module ActiveRecord
S
Sean Griffin 已提交
2
  # See ActiveRecord::Attributes::ClassMethods for documentation
S
Sean Griffin 已提交
3
  module Attributes
4 5
    extend ActiveSupport::Concern

S
Sean Griffin 已提交
6
    # :nodoc:
7
    Type = ActiveRecord::Type
8

9
    included do
10 11
      class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
      self.attributes_to_define_after_schema_loads = {}
12 13
    end

S
Sean Griffin 已提交
14 15 16 17 18
    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
S
Sean Griffin 已提交
19
      # ActiveRecord::QueryMethods#where. This will let you use
S
Sean Griffin 已提交
20 21 22 23 24
      # 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.
25 26 27 28 29
      #
      # +cast_type+ A type object that contains information about how to type cast the value.
      # See the examples section for more information.
      #
      # ==== Options
S
Sean Griffin 已提交
30 31 32 33 34
      # The following options are accepted:
      #
      # +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+.
35
      #
S
Sean Griffin 已提交
36
      # +array+ (PG only) specifies that the type should be an array (see the examples below).
S
Sean Griffin 已提交
37
      #
S
Sean Griffin 已提交
38
      # +range+ (PG only) specifies that the type should be a range (see the examples below).
39
      #
40 41
      # ==== Examples
      #
A
Anton Cherepanov 已提交
42
      # The type detected by Active Record can be overridden.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
      #
      #   # 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')
      #
      #   # before
      #   store_listing.price_in_cents # => BigDecimal.new(10.1)
      #
      #   class StoreListing < ActiveRecord::Base
59
      #     attribute :price_in_cents, :integer
60 61 62 63 64
      #   end
      #
      #   # after
      #   store_listing.price_in_cents # => 10
      #
S
Sean Griffin 已提交
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
      # 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
S
Sean Griffin 已提交
89 90 91 92 93 94
      # to the methods defined on the value type. The method
      # +type_cast_from_database+ or +type_cast_from_user+ 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
95 96
      #
      #   class MoneyType < ActiveRecord::Type::Integer
S
Sean Griffin 已提交
97
      #     def type_cast_from_user(value)
98 99
      #       if value.include?('$')
      #         price_in_dollars = value.gsub(/\$/, '').to_f
S
Sean Griffin 已提交
100
      #         super(price_in_dollars * 100)
101
      #       else
S
Sean Griffin 已提交
102
      #         super
103 104 105 106 107
      #       end
      #     end
      #   end
      #
      #   class StoreListing < ActiveRecord::Base
S
Sean Griffin 已提交
108
      #     attribute :price_in_cents, MoneyType.new
109 110 111 112
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
S
Sean Griffin 已提交
113 114
      #
      # For more details on creating custom types, see the documentation for
S
Sean Griffin 已提交
115
      # ActiveRecord::Type::Value
S
Sean Griffin 已提交
116 117 118
      #
      # ==== Querying
      #
S
Sean Griffin 已提交
119
      # When ActiveRecord::QueryMethods#where is called, it will
S
Sean Griffin 已提交
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
      # use the type defined by the model class to convert the value to SQL,
      # calling +type_cast_for_database+ 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 +type_cast_from_database+ or
      #     # +type_cast_from_user+. Assumed to be in instance of +Money+ in
      #     # this case.
      #     def type_cast_for_database(value)
      #       value_in_bitcoins = currency_converter.convert_to_bitcoins(value)
      #       value_in_bitcoins.amount
      #     end
      #   end
      #
      #   class Product < ActiveRecord::Base
      #     currency_converter = ConversionRatesFromTheInternet.new
      #     attribute :price_in_bitcoins, MoneyType.new(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?+
S
Sean Griffin 已提交
155 156
      # will be called from ActiveModel::Dirty. See the documentation for those
      # methods in ActiveRecord::Type::Value for more details.
157
      def attribute(name, cast_type, **options)
158
        name = name.to_s
159
        reload_schema_from_cache
160

161 162 163 164
        self.attributes_to_define_after_schema_loads =
          attributes_to_define_after_schema_loads.merge(
            name => [cast_type, options]
          )
165 166
      end

S
Sean Griffin 已提交
167 168 169
      # 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
S
Sean Griffin 已提交
170 171 172
      # 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.
S
Sean Griffin 已提交
173 174 175
      #
      # +name+ The name of the attribute being defined. Expected to be a +String+.
      #
S
Sean Griffin 已提交
176
      # +cast_type+ The type object to use for this attribute.
S
Sean Griffin 已提交
177 178 179 180 181 182
      #
      # +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
S
Sean Griffin 已提交
183
      # +type_cast_from_user+ or +type_cast_from_database+.
184 185 186 187 188 189 190 191
      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)
192 193
      end

S
Sean Griffin 已提交
194
      def load_schema! # :nodoc:
195
        super
196
        attributes_to_define_after_schema_loads.each do |name, (type, options)|
197 198 199 200 201
          if type.is_a?(Symbol)
            type = connection.type_for_attribute_options(type, **options.except(:default))
          end

          define_attribute(name, type, **options.slice(:default))
202
        end
203 204 205 206
      end

      private

207 208
      NO_DEFAULT_PROVIDED = Object.new # :nodoc:
      private_constant :NO_DEFAULT_PROVIDED
209

210 211 212 213 214 215 216
      def define_default_attribute(name, value, type, from_user:)
        if value == NO_DEFAULT_PROVIDED
          default_attribute = _default_attributes[name].with_type(type)
        elsif from_user
          default_attribute = Attribute.from_user(name, value, type)
        else
          default_attribute = Attribute.from_database(name, value, type)
217
        end
218
        _default_attributes[name] = default_attribute
219
      end
220 221 222
    end
  end
end