attribute_methods.rb 8.4 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 23 24 25 26 27 28 29 30 31
    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).
      alias [] read_attribute

      # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
      # (Alias for the protected write_attribute method).
      alias []= write_attribute

      public :[], :[]=
    end

32
    module ClassMethods
33 34
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
35
      def define_attribute_methods
36
        return if attribute_methods_generated?
J
Jon Leighton 已提交
37 38 39
        superclass.define_attribute_methods unless self == base_class
        super(column_names)
        @attribute_methods_generated = true
40 41 42
      end

      def attribute_methods_generated?
J
Jon Leighton 已提交
43
        @attribute_methods_generated ||= false
44 45
      end

J
Jon Leighton 已提交
46 47 48 49
      # 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).
50
      def generated_external_attribute_methods
J
Jon Leighton 已提交
51
        @generated_external_attribute_methods ||= Module.new { extend self }
52 53
      end

J
Jon Leighton 已提交
54
      def undefine_attribute_methods
55
        super if attribute_methods_generated?
J
Jon Leighton 已提交
56
        @attribute_methods_generated = false
57 58
      end

59
      def instance_method_already_implemented?(method_name)
60 61 62 63
        if dangerous_attribute_method?(method_name)
          raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
        end

J
Jon Leighton 已提交
64
        if active_record_super == Base
J
Jon Leighton 已提交
65 66
          super
        else
67 68 69
          # 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 已提交
70
        end
71
      end
72

73 74
      # 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 已提交
75 76 77
      def dangerous_attribute_method?(name)
        method_defined_within?(name, Base)
      end
78

J
Jon Leighton 已提交
79 80 81 82 83 84 85 86 87 88
      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
89
      end
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

      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
105
    end
J
Joshua Peek 已提交
106

107 108 109 110
    # 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?
111
        self.class.define_attribute_methods
112 113 114 115 116 117

        if respond_to_without_attributes?(method)
          send(method, *args, &block)
        else
          super
        end
118 119
      else
        super
120 121 122
      end
    end

123 124 125 126 127 128 129 130 131 132 133 134 135 136
    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

137
    def respond_to?(name, include_private = false)
138
      self.class.define_attribute_methods unless self.class.attribute_methods_generated?
139 140
      super
    end
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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
    # 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)
      self.class.columns_hash[name.to_s]
    end

194
    protected
195 196 197 198 199 200 201 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

    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
234
      end
235 236 237 238 239 240 241

      attrs
    end

    def attribute_method?(attr_name)
      attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
    end
242 243
  end
end