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

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

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

      # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
      # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
      # (Alias for the protected read_attribute method).
23 24 25
      def [](attr_name)
        read_attribute(attr_name)
      end
26 27 28

      # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
      # (Alias for the protected write_attribute method).
29 30 31
      def []=(attr_name, value)
        write_attribute(attr_name, value)
      end
32 33
    end

34
    module ClassMethods
35 36
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
37
      def define_attribute_methods
J
Jon Leighton 已提交
38 39 40 41 42 43 44 45
        # Use a mutex; we don't want two thread simaltaneously trying to define
        # attribute methods.
        @attribute_methods_mutex.synchronize do
          return if attribute_methods_generated?
          superclass.define_attribute_methods unless self == base_class
          super(column_names)
          @attribute_methods_generated = true
        end
46 47 48
      end

      def attribute_methods_generated?
J
Jon Leighton 已提交
49
        @attribute_methods_generated ||= false
50 51
      end

J
Jon Leighton 已提交
52 53 54 55
      # We will define the methods as instance methods, but will call them as singleton
      # methods. This allows us to use method_defined? to check if the method exists,
      # which is fast and won't give any false positives from the ancestors (because
      # there are no ancestors).
56
      def generated_external_attribute_methods
J
Jon Leighton 已提交
57
        @generated_external_attribute_methods ||= Module.new { extend self }
58 59
      end

J
Jon Leighton 已提交
60
      def undefine_attribute_methods
61
        super if attribute_methods_generated?
J
Jon Leighton 已提交
62
        @attribute_methods_generated = false
63 64
      end

65
      def instance_method_already_implemented?(method_name)
66 67 68 69
        if dangerous_attribute_method?(method_name)
          raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
        end

70
        if [Base, Model].include?(active_record_super)
J
Jon Leighton 已提交
71 72
          super
        else
73 74 75
          # 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 已提交
76
        end
77
      end
78

79 80
      # 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.)
J
Jon Leighton 已提交
81 82 83
      def dangerous_attribute_method?(name)
        method_defined_within?(name, Base)
      end
84

J
Jon Leighton 已提交
85 86 87 88 89 90 91 92 93 94
      def method_defined_within?(name, klass, sup = klass.superclass)
        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
95
      end
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110

      def attribute_method?(attribute)
        super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
      end

      # Returns an array of column names as strings if it's not
      # an abstract class and table exists.
      # Otherwise it returns an empty array.
      def attribute_names
        @attribute_names ||= if !abstract_class? && table_exists?
            column_names
          else
            []
          end
      end
111
    end
J
Joshua Peek 已提交
112

113 114 115 116
    # If we haven't generated any methods yet, generate them, then
    # see if we've created the method we're looking for.
    def method_missing(method, *args, &block)
      unless self.class.attribute_methods_generated?
117
        self.class.define_attribute_methods
118 119 120 121 122 123

        if respond_to_without_attributes?(method)
          send(method, *args, &block)
        else
          super
        end
124 125
      else
        super
126 127 128
      end
    end

129 130 131 132 133 134 135 136 137 138 139 140 141 142
    def attribute_missing(match, *args, &block)
      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

143
    def respond_to?(name, include_private = false)
144
      self.class.define_attribute_methods unless self.class.attribute_methods_generated?
145 146
      super
    end
147

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    # Returns true if the given attribute is in the attributes hash
    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.
    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.
    def attributes
      Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
    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.
    #
    #   person = Person.create!(:name => "David Heinemeier Hansson " * 3)
    #
    #   person.attribute_for_inspect(:name)
    #   # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
    #
    #   person.attribute_for_inspect(:created_at)
    #   # => '"2009-01-12 04:48:57"'
    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

    # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
    # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
    def attribute_present?(attribute)
      value = read_attribute(attribute)
      !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
    end

    # Returns the column object for the named attribute.
    def column_for_attribute(name)
A
Aaron Patterson 已提交
197
      # FIXME: should this return a null object for columns that don't exist?
198 199 200
      self.class.columns_hash[name.to_s]
    end

201
    protected
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240

    def clone_attributes(reader_method = :read_attribute, attributes = {})
      attribute_names.each do |name|
        attributes[name] = clone_attribute_value(reader_method, name)
      end
      attributes
    end

    def clone_attribute_value(reader_method, attribute_name)
      value = send(reader_method, attribute_name)
      value.duplicable? ? value.clone : value
    rescue TypeError, NoMethodError
      value
    end

    # Returns a copy of the attributes hash where all the values have been safely quoted for use in
    # an Arel insert/update method.
    def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
      attrs      = {}
      klass      = self.class
      arel_table = klass.arel_table

      attribute_names.each do |name|
        if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)

          if include_readonly_attributes || !self.class.readonly_attributes.include?(name)

            value = if klass.serialized_attributes.include?(name)
                      @attributes[name].serialized_value
                    else
                      # FIXME: we need @attributes to be used consistently.
                      # If the values stored in @attributes were already type
                      # casted, this code could be simplified
                      read_attribute(name)
                    end

            attrs[arel_table[name]] = value
          end
        end
241
      end
242 243 244 245 246

      attrs
    end

    def attribute_method?(attr_name)
247
      defined?(@attributes) && @attributes.include?(attr_name)
248
    end
249 250
  end
end