attribute_methods.rb 14.3 KB
Newer Older
J
Jeremy Kemper 已提交
1
require 'active_support/core_ext/enumerable'
2
require 'mutex_m'
J
Jeremy Kemper 已提交
3

4
module ActiveRecord
5
  # = Active Record Attribute Methods
6
  module AttributeMethods
7
    extend ActiveSupport::Concern
8
    include ActiveModel::AttributeMethods
9

10
    included do
11
      initialize_generated_modules
12 13 14 15 16 17 18 19 20 21
      include Read
      include Write
      include BeforeTypeCast
      include Query
      include PrimaryKey
      include TimeZoneConversion
      include Dirty
      include Serialization
    end

22
    module ClassMethods
23 24 25 26 27 28
      def inherited(child_class) #:nodoc:
        child_class.initialize_generated_modules
        super
      end

      def initialize_generated_modules # :nodoc:
29 30 31 32
        @generated_attribute_methods = Module.new {
          extend Mutex_m

          const_set :AttrNames, Module.new {
33 34 35 36 37
            def self.set_name_cache(name, value)
              const_name = "ATTR_#{name}"
              unless const_defined? const_name
                const_set const_name, value
              end
38 39 40
            end
          }
        }
41
        @attribute_methods_generated = false
42 43 44
        include @generated_attribute_methods
      end

45 46
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
47
      def define_attribute_methods # :nodoc:
V
Vipul A M 已提交
48
        # Use a mutex; we don't want two thread simultaneously trying to define
J
Jon Leighton 已提交
49
        # attribute methods.
50
        generated_attribute_methods.synchronize do
51
          return false if @attribute_methods_generated
J
Jon Leighton 已提交
52 53 54 55
          superclass.define_attribute_methods unless self == base_class
          super(column_names)
          @attribute_methods_generated = true
        end
56
        true
57 58
      end

59
      def undefine_attribute_methods # :nodoc:
60 61 62 63
        generated_attribute_methods.synchronize do
          super if @attribute_methods_generated
          @attribute_methods_generated = false
        end
64 65
      end

66 67 68 69 70 71 72 73 74 75 76 77 78 79
      # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
      # \Active \Record method is defined in the model, otherwise +false+.
      #
      #   class Person < ActiveRecord::Base
      #     def save
      #       'already defined by Active Record'
      #     end
      #   end
      #
      #   Person.instance_method_already_implemented?(:save)
      #   # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
      #
      #   Person.instance_method_already_implemented?(:name)
      #   # => false
80
      def instance_method_already_implemented?(method_name)
81
        if dangerous_attribute_method?(method_name)
X
Xavier Noria 已提交
82
          raise DangerousAttributeError, "#{method_name} is defined by Active Record"
83 84
        end

J
Jon Leighton 已提交
85
        if superclass == Base
J
Jon Leighton 已提交
86 87
          super
        else
88 89 90
          # If B < A and A defines its own attribute method, then we don't want to overwrite that.
          defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
          defined && !ActiveRecord::Base.method_defined?(method_name) || super
J
Jon Leighton 已提交
91
        end
92
      end
93

94 95
      # A method name is 'dangerous' if it is already defined by Active Record, but
      # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
96
      def dangerous_attribute_method?(name) # :nodoc:
J
Jon Leighton 已提交
97 98
        method_defined_within?(name, Base)
      end
99

100
      def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
J
Jon Leighton 已提交
101 102 103 104 105 106 107 108 109
        if klass.method_defined?(name) || klass.private_method_defined?(name)
          if sup.method_defined?(name) || sup.private_method_defined?(name)
            klass.instance_method(name).owner != sup.instance_method(name).owner
          else
            true
          end
        else
          false
        end
110
      end
111

112 113 114 115 116 117 118 119 120
      # Returns +true+ if +attribute+ is an attribute method and table exists,
      # +false+ otherwise.
      #
      #   class Person < ActiveRecord::Base
      #   end
      #
      #   Person.attribute_method?('name')   # => true
      #   Person.attribute_method?(:age=)    # => true
      #   Person.attribute_method?(:nothing) # => false
121 122 123 124
      def attribute_method?(attribute)
        super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
      end

125 126 127 128 129 130 131 132
      # Returns an array of column names as strings if it's not an abstract class and
      # table exists. Otherwise it returns an empty array.
      #
      #   class Person < ActiveRecord::Base
      #   end
      #
      #   Person.attribute_names
      #   # => ["id", "created_at", "updated_at", "name", "age"]
133 134 135 136 137 138 139
      def attribute_names
        @attribute_names ||= if !abstract_class? && table_exists?
            column_names
          else
            []
          end
      end
140
    end
J
Joshua Peek 已提交
141

142 143
    # If we haven't generated any methods yet, generate them, then
    # see if we've created the method we're looking for.
144
    def method_missing(method, *args, &block) # :nodoc:
145
      if self.class.define_attribute_methods
146 147 148 149 150
        if respond_to_without_attributes?(method)
          send(method, *args, &block)
        else
          super
        end
151 152
      else
        super
153 154 155
      end
    end

156
    def attribute_missing(match, *args, &block) # :nodoc:
157 158 159 160 161 162 163 164 165 166 167 168 169
      if self.class.columns_hash[match.attr_name]
        ActiveSupport::Deprecation.warn(
          "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
          "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
          "is a column of the table. If this error has happened through normal usage of Active " \
          "Record (rather than through your own code or external libraries), please report it as " \
          "a bug."
        )
      end

      super
    end

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
    # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
    # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
    # which will all return +true+. It also define the attribute methods if they have
    # not been generated.
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.respond_to(:name)    # => true
    #   person.respond_to(:name=)   # => true
    #   person.respond_to(:name?)   # => true
    #   person.respond_to('age')    # => true
    #   person.respond_to('age=')   # => true
    #   person.respond_to('age?')   # => true
    #   person.respond_to(:nothing) # => false
186
    def respond_to?(name, include_private = false)
187
      name = name.to_s
188
      self.class.define_attribute_methods
189 190
      result = super

191
      # If the result is false the answer is false.
192 193 194 195
      return false unless result

      # If the result is true then check for the select case.
      # For queries selecting a subset of columns, return false for unselected columns.
196 197
      # We check defined?(@attributes) not to issue warnings if called on objects that
      # have been allocated but not yet initialized.
198
      if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
199 200 201 202
        return has_attribute?(name)
      end

      return true
203
    end
204

205 206 207 208 209 210 211 212 213
    # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.has_attribute?(:name)    # => true
    #   person.has_attribute?('age')    # => true
    #   person.has_attribute?(:nothing) # => false
214 215 216 217 218
    def has_attribute?(attr_name)
      @attributes.has_key?(attr_name.to_s)
    end

    # Returns an array of names for the attributes available on this object.
219 220 221 222 223 224 225
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.attribute_names
    #   # => ["id", "created_at", "updated_at", "name", "age"]
226 227 228 229 230
    def attribute_names
      @attributes.keys
    end

    # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
231 232 233 234 235 236 237
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.create(name: 'Francesco', age: 22)
    #   person.attributes
    #   # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
238
    def attributes
239 240 241
      attribute_names.each_with_object({}) { |name, attrs|
        attrs[name] = read_attribute(name)
      }
242 243 244 245 246 247 248 249
    end

    # Returns an <tt>#inspect</tt>-like string for the value of the
    # attribute +attr_name+. String attributes are truncated upto 50
    # characters, and Date and Time attributes are returned in the
    # <tt>:db</tt> format. Other attributes return the value of
    # <tt>#inspect</tt> without modification.
    #
250
    #   person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
251 252
    #
    #   person.attribute_for_inspect(:name)
253
    #   # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
254 255
    #
    #   person.attribute_for_inspect(:created_at)
256
    #   # => "\"2012-10-22 00:15:07\""
257 258 259 260 261 262 263 264 265 266 267 268
    def attribute_for_inspect(attr_name)
      value = read_attribute(attr_name)

      if value.is_a?(String) && value.length > 50
        "#{value[0..50]}...".inspect
      elsif value.is_a?(Date) || value.is_a?(Time)
        %("#{value.to_s(:db)}")
      else
        value.inspect
      end
    end

269 270 271
    # Returns +true+ if the specified +attribute+ has been set by the user or by a
    # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
    # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
272
    # Note that it always returns +true+ with boolean attributes.
273
    #
274
    #   class Task < ActiveRecord::Base
275 276
    #   end
    #
277 278 279
    #   person = Task.new(title: '', is_done: false)
    #   person.attribute_present?(:title)   # => false
    #   person.attribute_present?(:is_done) # => true
280
    #   person.name = 'Francesco'
281 282 283
    #   person.is_done = true
    #   person.attribute_present?(:title)   # => true
    #   person.attribute_present?(:is_done) # => true
284 285
    def attribute_present?(attribute)
      value = read_attribute(attribute)
286
      !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
287 288
    end

289 290 291 292 293 294 295 296 297 298 299 300
    # Returns the column object for the named attribute. Returns +nil+ if the
    # named attribute not exists.
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
    #   # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
    #
    #   person.column_for_attribute(:nothing)
    #   # => nil
301
    def column_for_attribute(name)
A
Aaron Patterson 已提交
302
      # FIXME: should this return a null object for columns that don't exist?
303 304 305
      self.class.columns_hash[name.to_s]
    end

306
    # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
307 308 309 310
    # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
    # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
    #
    # Alias for the <tt>read_attribute</tt> method.
311 312
    #
    #   class Person < ActiveRecord::Base
313
    #     belongs_to :organization
314 315
    #   end
    #
316
    #   person = Person.new(name: 'Francesco', age: '22')
317 318
    #   person[:name] # => "Francesco"
    #   person[:age]  # => 22
319 320 321 322
    #
    #   person = Person.select('id').first
    #   person[:name]            # => ActiveModel::MissingAttributeError: missing attribute: name
    #   person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
323
    def [](attr_name)
324
      read_attribute(attr_name) { |n| missing_attribute(n, caller) }
325 326 327
    end

    # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
328 329 330 331 332 333 334 335 336
    # (Alias for the protected <tt>write_attribute</tt> method).
    #
    #   class Person < ActiveRecord::Base
    #   end
    #
    #   person = Person.new
    #   person[:age] = '22'
    #   person[:age] # => 22
    #   person[:age] # => Fixnum
337 338 339 340
    def []=(attr_name, value)
      write_attribute(attr_name, value)
    end

341
    protected
342

343
    def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
344 345 346 347 348 349
      attribute_names.each do |name|
        attributes[name] = clone_attribute_value(reader_method, name)
      end
      attributes
    end

350
    def clone_attribute_value(reader_method, attribute_name) # :nodoc:
351 352 353 354 355 356
      value = send(reader_method, attribute_name)
      value.duplicable? ? value.clone : value
    rescue TypeError, NoMethodError
      value
    end

357
    def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
J
Jon Leighton 已提交
358
      arel_attributes_with_values(attributes_for_create(attribute_names))
R
Robin Roestenburg 已提交
359
    end
360

361
    def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
R
Robin Roestenburg 已提交
362
      arel_attributes_with_values(attributes_for_update(attribute_names))
363 364
    end

365
    def attribute_method?(attr_name) # :nodoc:
366
      # We check defined? because Syck calls respond_to? before actually calling initialize.
367
      defined?(@attributes) && @attributes.include?(attr_name)
368 369
    end

R
Robin Roestenburg 已提交
370
    private
371

R
Robin Roestenburg 已提交
372
    # Returns a Hash of the Arel::Attributes and attribute values that have been
373
    # typecasted for use in an Arel insert/update method.
R
Robin Roestenburg 已提交
374 375 376
    def arel_attributes_with_values(attribute_names)
      attrs = {}
      arel_table = self.class.arel_table
377

R
Robin Roestenburg 已提交
378 379 380
      attribute_names.each do |name|
        attrs[arel_table[name]] = typecasted_attribute_value(name)
      end
381 382 383
      attrs
    end

R
Robin Roestenburg 已提交
384 385 386
    # Filters the primary keys and readonly attributes from the attribute names.
    def attributes_for_update(attribute_names)
      attribute_names.select do |name|
387
        column_for_attribute(name) && !readonly_attribute?(name)
R
Robin Roestenburg 已提交
388 389 390 391 392
      end
    end

    # Filters out the primary keys, from the attribute names, when the primary
    # key is to be generated (e.g. the id attribute has no value).
J
Jon Leighton 已提交
393 394 395
    def attributes_for_create(attribute_names)
      attribute_names.select do |name|
        column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
R
Robin Roestenburg 已提交
396
      end
397 398 399 400 401 402
    end

    def readonly_attribute?(name)
      self.class.readonly_attributes.include?(name)
    end

R
Robin Roestenburg 已提交
403 404
    def pk_attribute?(name)
      column_for_attribute(name).primary
405 406 407
    end

    def typecasted_attribute_value(name)
408 409 410 411
      # FIXME: we need @attributes to be used consistently.
      # If the values stored in @attributes were already typecasted, this code
      # could be simplified
      read_attribute(name)
412
    end
413 414
  end
end