attributes.rb 8.7 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
      # +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.
29 30
      #
      # ==== Options
S
Sean Griffin 已提交
31 32 33 34 35
      # 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+.
36
      #
S
Sean Griffin 已提交
37
      # +array+ (PG only) specifies that the type should be an array (see the examples below).
S
Sean Griffin 已提交
38
      #
S
Sean Griffin 已提交
39
      # +range+ (PG only) specifies that the type should be a range (see the examples below).
40
      #
41 42
      # ==== Examples
      #
A
Anton Cherepanov 已提交
43
      # The type detected by Active Record can be overridden.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
      #
      #   # 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
60
      #     attribute :price_in_cents, :integer
61 62 63 64 65
      #   end
      #
      #   # after
      #   store_listing.price_in_cents # => 10
      #
66 67 68 69 70 71 72 73 74 75 76 77 78 79
      # 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"
      #
S
Sean Griffin 已提交
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
      # 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 已提交
104 105 106 107 108 109
      # 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
110 111
      #
      #   class MoneyType < ActiveRecord::Type::Integer
S
Sean Griffin 已提交
112
      #     def type_cast_from_user(value)
113 114
      #       if value.include?('$')
      #         price_in_dollars = value.gsub(/\$/, '').to_f
S
Sean Griffin 已提交
115
      #         super(price_in_dollars * 100)
116
      #       else
S
Sean Griffin 已提交
117
      #         super
118 119 120 121 122
      #       end
      #     end
      #   end
      #
      #   class StoreListing < ActiveRecord::Base
S
Sean Griffin 已提交
123
      #     attribute :price_in_cents, MoneyType.new
124 125 126 127
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
S
Sean Griffin 已提交
128 129
      #
      # For more details on creating custom types, see the documentation for
130
      # ActiveRecord::Type::Value.
S
Sean Griffin 已提交
131 132 133
      #
      # ==== Querying
      #
S
Sean Griffin 已提交
134
      # When ActiveRecord::QueryMethods#where is called, it will
S
Sean Griffin 已提交
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
      # 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 已提交
170 171
      # will be called from ActiveModel::Dirty. See the documentation for those
      # methods in ActiveRecord::Type::Value for more details.
172
      def attribute(name, cast_type, **options)
173
        name = name.to_s
174
        reload_schema_from_cache
175

176 177 178 179
        self.attributes_to_define_after_schema_loads =
          attributes_to_define_after_schema_loads.merge(
            name => [cast_type, options]
          )
180 181
      end

S
Sean Griffin 已提交
182 183 184
      # 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 已提交
185 186 187
      # 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 已提交
188 189 190
      #
      # +name+ The name of the attribute being defined. Expected to be a +String+.
      #
S
Sean Griffin 已提交
191
      # +cast_type+ The type object to use for this attribute.
S
Sean Griffin 已提交
192 193 194 195 196 197
      #
      # +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 已提交
198
      # +type_cast_from_user+ or +type_cast_from_database+.
199 200 201 202 203 204 205 206
      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)
207 208
      end

S
Sean Griffin 已提交
209
      def load_schema! # :nodoc:
210
        super
211
        attributes_to_define_after_schema_loads.each do |name, (type, options)|
212 213 214 215 216
          if type.is_a?(Symbol)
            type = connection.type_for_attribute_options(type, **options.except(:default))
          end

          define_attribute(name, type, **options.slice(:default))
217
        end
218 219 220 221
      end

      private

222 223
      NO_DEFAULT_PROVIDED = Object.new # :nodoc:
      private_constant :NO_DEFAULT_PROVIDED
224

225 226 227 228 229 230 231
      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)
232
        end
233
        _default_attributes[name] = default_attribute
234
      end
235 236 237
    end
  end
end