diff options
Diffstat (limited to 'activerecord/lib/active_record/attribute_methods')
8 files changed, 334 insertions, 320 deletions
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index a23baeaced..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -41,8 +41,9 @@ module ActiveRecord # task.read_attribute_before_type_cast('id') # => '1' # task.read_attribute('completed_on') # => Sun, 21 Oct 2012 # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" + # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @attributes[attr_name] + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -56,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 0333605eac..d5702accaf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/deprecation' module ActiveRecord module AttributeMethods @@ -15,24 +14,12 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false self.partial_writes = true - - def self.partial_updates=(v); self.partial_writes = v; end - def self.partial_updates?; partial_writes?; end - def self.partial_updates; partial_writes; end - - ActiveSupport::Deprecation.deprecate_methods( - singleton_class, - :partial_updates= => :partial_writes=, - :partial_updates? => :partial_writes?, - :partial_updates => :partial_writes - ) end # Attempts to +save+ the record and clears changed attributes if successful. def save(*) if status = super - @previously_changed = changes - @changed_attributes.clear + changes_applied end status end @@ -40,75 +27,158 @@ module ActiveRecord # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. def save!(*) super.tap do - @previously_changed = changes - @changed_attributes.clear + changes_applied end end # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @previously_changed.clear - @changed_attributes.clear + clear_changes_information + end + end + + def initialize_dup(other) # :nodoc: + super + calculate_changes_from_defaults + end + + def changes_applied + super + store_original_raw_attributes + end + + def clear_changes_information + super + original_raw_attributes.clear + end + + def changed_attributes + # This should only be set by methods which will call changed_attributes + # multiple times when it is known that the computed value cannot change. + if defined?(@cached_changed_attributes) + @cached_changed_attributes + else + super.reverse_merge(attributes_changed_in_place).freeze + end + end + + def changes + cache_changed_attributes do + super + end + end + + def attribute_changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) + end + + private + + def changes_include?(attr_name) + super || attribute_changed_in_place?(attr_name) + end + + def calculate_changes_from_defaults + @changed_attributes = nil + self.class.column_defaults.each do |attr, orig_value| + set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) end end - private # Wrap write_attribute to remember original attribute value. def write_attribute(attr, value) attr = attr.to_s - # The attribute already has an unsaved change. + old_value = old_attribute_value(attr) + + result = super + store_original_raw_attribute(attr) + save_changed_attribute(attr, old_value) + result + end + + def raw_write_attribute(attr, value) + attr = attr.to_s + + result = super + original_raw_attributes[attr] = value + result + end + + def save_changed_attribute(attr, old_value) if attribute_changed?(attr) - old = @changed_attributes[attr] - @changed_attributes.delete(attr) unless _field_changed?(attr, old, value) + clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else - old = clone_attribute_value(:read_attribute, attr) - @changed_attributes[attr] = old if _field_changed?(attr, old, value) + set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) end + end - # Carry on. - super(attr, value) + def old_attribute_value(attr) + if attribute_changed?(attr) + changed_attributes[attr] + else + clone_attribute_value(:_read_attribute, attr) + end end - def update(*) + def _update_record(*) partial_writes? ? super(keys_for_partial_write) : super end - def create(*) + def _create_record(*) partial_writes? ? super(keys_for_partial_write) : super end # Serialized attributes should always be written in case they've been # changed in place. def keys_for_partial_write - changed | (attributes.keys & self.class.serialized_attributes.keys) + changed end - def _field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || - changes_from_zero_to_string?(old, value)) - value = nil - else - value = column.type_cast(value) - end + def _field_changed?(attr, old_value) + @attributes[attr].changed_from?(old_value) + end + + def attributes_changed_in_place + changed_in_place.each_with_object({}) do |attr_name, h| + orig = @attributes[attr_name].original_value + h[attr_name] = orig + end + end + + def changed_in_place + self.class.attribute_names.select do |attr_name| + attribute_changed_in_place?(attr_name) end + end - old != value + def original_raw_attribute(attr_name) + original_raw_attributes.fetch(attr_name) do + read_attribute_before_type_cast(attr_name) + end end - def changes_from_nil_to_empty_string?(column, old, value) - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - column.null && (old.nil? || old == 0) && value.blank? + def original_raw_attributes + @original_raw_attributes ||= {} + end + + def store_original_raw_attribute(attr_name) + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + end + + def store_original_raw_attributes + attribute_names.each do |attr| + store_original_raw_attribute(attr) + end end - def changes_from_zero_to_string?(old, value) - # For columns with old 0 and value non-empty string - old == 0 && value.is_a?(String) && value.present? && value != '0' + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + remove_instance_variable(:@cached_changed_attributes) end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 0857b02c8e..c28374e4ab 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,30 +8,43 @@ module ActiveRecord # Returns this record's primary key value wrapped in an Array if one is # available. def to_key + sync_with_transaction_state key = self.id [key] if key end # Returns the primary key value. def id - read_attribute(self.class.primary_key) + if pk = self.class.primary_key + sync_with_transaction_state + _read_attribute(pk) + end end # Sets the primary key value. def id=(value) + sync_with_transaction_state write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value. def id? + sync_with_transaction_state query_attribute(self.class.primary_key) end # Returns the primary key value before type cast. def id_before_type_cast + sync_with_transaction_state read_attribute_before_type_cast(self.class.primary_key) end + # Returns the primary key previous value. + def id_was + sync_with_transaction_state + attribute_was(self.class.primary_key) + end + protected def attribute_method?(attr_name) @@ -47,7 +60,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) @@ -76,16 +89,13 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' unless base_name && !base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys[table_name] + connection.schema_cache.primary_keys(table_name) else 'id' end @@ -110,6 +120,7 @@ module ActiveRecord def primary_key=(value) @primary_key = value && value.to_s @quoted_primary_key = nil + @attributes_builder = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 0f9723febb..dc689f399a 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -8,7 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - value = read_attribute(attr_name) { |n| missing_attribute(n, caller) } + value = self[attr_name] case value when true then true diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 3c03cce838..20f0936e52 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,40 +1,16 @@ +require 'active_support/core_ext/module/method_transplanting' + module ActiveRecord module AttributeMethods module Read - extend ActiveSupport::Concern - - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] - - included do - class_attribute :attribute_types_cached_by_default, instance_writer: false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - end - - module ClassMethods - # +cache_attributes+ allows you to declare which converted attribute - # values should be cached. Usually caching only pays off for attributes - # with expensive conversion methods, like time related columns (e.g. - # +created_at+, +updated_at+). - def cache_attributes(*attribute_names) - cached_attributes.merge attribute_names.map { |attr| attr.to_s } - end - - # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. - def cached_attributes - @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set - end - - # Returns +true+ if the provided attribute is being cached. - def cache_attribute?(attr_name) - cached_attributes.include?(attr_name) - end - - protected - + ReaderMethodCache = Class.new(AttributeMethodCache) { + private # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch and - # uses more memory (because it creates a closure). + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. # # But sometimes the database might return columns with # characters that are not allowed in normal method names (like @@ -46,61 +22,81 @@ module ActiveRecord # the attribute name. Using a constant means that we do not have # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to - # key the @attributes_cache in read_attribute. - def define_method_attribute(name) - safe_name = name.unpack('h*').first - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name} - read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) } - end - alias_method #{name.inspect}, :__temp__#{safe_name} - undef_method :__temp__#{safe_name} - STR + # key the @attributes in read_attribute. + def method_body(method_name, const_name) + <<-EOMETHOD + def #{method_name} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + EOMETHOD end + }.new - private + extend ActiveSupport::Concern - def cacheable_column?(column) - if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - ! serialized_attributes.include? column.name - else - attribute_types_cached_by_default.include?(column.type) + module ClassMethods + [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| + define_method method_name do |*| + cached_attributes_deprecation_warning(method_name) + true + end + end + + protected + + def cached_attributes_deprecation_warning(method_name) + ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached." + end + + if Module.methods_transplantable? + def define_method_attribute(name) + method = ReaderMethodCache[name] + generated_attribute_methods.module_eval { define_method name, method } + end + else + def define_method_attribute(name) + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" + + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + STR + + generated_attribute_methods.module_eval do + alias_method name, temp_method + undef_method temp_method + end end end end + ID = 'id'.freeze + # 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 + # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) - # If it's cached, just return it - # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829. + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes_cache[name] || @attributes_cache.fetch(name) { - column = @columns_hash.fetch(name) { - return @attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name - read_attribute(self.class.primary_key) - end - } - } - - value = @attributes.fetch(name) { - return block_given? ? yield(name) : nil - } + name = self.class.primary_key if name == ID + _read_attribute(name, &block) + end - if self.class.cache_attribute?(name) - @attributes_cache[name] = column.type_cast(value) - else - column.type_cast value - end - } + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the read_attribute API + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } end private def attribute(attribute_name) - read_attribute(attribute_name) + _read_attribute(attribute_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 25d62fdb85..e5ec5ddca5 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -1,27 +1,23 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module AttributeMethods module Serialization extend ActiveSupport::Concern - included do - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - class_attribute :serialized_attributes, instance_accessor: false - self.serialized_attributes = {} - end - module ClassMethods # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The # serialization is done through YAML. If +class_name+ is specified, the - # serialized object must be of that class on retrieval or - # <tt>SerializationTypeMismatch</tt> will be raised. + # serialized object must be of that class on assignment and retrieval. + # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. + # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # or a class name that the object type should be equal to. # # ==== Example # @@ -29,121 +25,44 @@ module ActiveRecord # class User < ActiveRecord::Base # serialize :preferences # end - def serialize(attr_name, class_name = Object) - include Behavior - - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name + # + # # Serialize preferences using JSON as coder. + # class User < ActiveRecord::Base + # serialize :preferences, JSON + # end + # + # # Serialize preferences as Hash using YAML coder. + # class User < ActiveRecord::Base + # serialize :preferences, Hash + # end + def serialize(attr_name, class_name_or_coder = Object) + # When ::JSON is used, force it to go through the Active Support JSON encoder + # to ensure special objects (e.g. Active Record models) are dumped correctly + # using the #as_json hook. + coder = if class_name_or_coder == ::JSON + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + class_name_or_coder else - Coders::YAMLColumn.new(class_name) + Coders::YAMLColumn.new(class_name_or_coder) end - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - end - - def serialized_attributes - message = "Instance level serialized_attributes method is deprecated, please use class level method." - ActiveSupport::Deprecation.warn message - defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes - end - - class Type # :nodoc: - def initialize(column) - @column = column - end - - def type_cast(value) - value.unserialized_value - end - - def type - @column.type - end - end - - class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value - state == :serialized ? unserialize : value - end - - def serialized_value - state == :unserialized ? serialize : value - end - - def unserialize - self.state = :unserialized - self.value = coder.load(value) - end - - def serialize - self.state = :serialized - self.value = coder.dump(value) - end - end - - # This is only added to the model when serialize is called, which - # ensures we do not make things slower when serialization is not used. - module Behavior #:nodoc: - extend ActiveSupport::Concern - - module ClassMethods - def initialize_attributes(attributes, options = {}) - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) - - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) - end - end - - attributes + decorate_attribute_type(attr_name, :serialize) do |type| + Type::Serialized.new(type, coder) end end - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - - def _field_changed?(attr, old, value) - if self.class.serialized_attributes.include?(attr) - old != value - else - super - end - end - - def read_attribute_before_type_cast(attr_name) - if self.class.serialized_attributes.include?(attr_name) - super.unserialized_value - else - super - end - end - - def attributes_before_type_cast - super.dup.tap do |attributes| - self.class.serialized_attributes.each_key do |key| - if attributes.key?(key) - attributes[key] = attributes[key].unserialized_value - end - end - end - end - - def typecasted_attribute_value(name) - if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - super - end + def serialized_attributes + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `serialized_attributes` is deprecated without replacement, and will + be removed in Rails 5.0. + MSG + + @serialized_attributes ||= Hash[ + columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| + [c.name, c.cast_type.coder] + } + ] end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 47a8b576c0..777f7ab4d7 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,18 +1,33 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type # :nodoc: - def initialize(column) - @column = column + class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: + include Type::Decorator + + def type_cast_from_database(value) + convert_time_to_time_zone(super) end - def type_cast(value) - value = @column.type_cast(value) - value.acts_like?(:time) ? value.in_time_zone : value + def type_cast_from_user(value) + if value.is_a?(Array) + value.map { |v| type_cast_from_user(v) } + elsif value.respond_to?(:in_time_zone) + begin + value.in_time_zone || super + rescue ArgumentError + nil + end + end end - def type - @column.type + def convert_time_to_time_zone(value) + if value.is_a?(Array) + value.map { |v| convert_time_to_time_zone(v) } + elsif value.acts_like?(:time) + value.in_time_zone + else + value + end end end @@ -27,46 +42,28 @@ module ActiveRecord end module ClassMethods - protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(original_time) - original_time = nil if original_time.blank? - time = original_time - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - zoned_time = time && time.in_time_zone rescue nil - rounded_time = round_usec(zoned_time) - rounded_value = round_usec(read_attribute("#{attr_name}")) - if (rounded_value != rounded_time) || (!rounded_value && original_time) - write_attribute("#{attr_name}", original_time) - #{attr_name}_will_change! - @attributes_cache["#{attr_name}"] = zoned_time - end - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super + private + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end end + super end - private - def create_time_zone_conversion_attribute?(name, column) + def create_time_zone_conversion_attribute?(name, cast_type) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - [:datetime, :timestamp].include?(column.type) + (:datetime == cast_type.type) end end - - private - def round_usec(value) - value.change(usec: 0) if value - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index cd33494cc3..16804f86bf 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,6 +1,21 @@ +require 'active_support/core_ext/module/method_transplanting' + module ActiveRecord module AttributeMethods module Write + WriterMethodCache = Class.new(AttributeMethodCache) { + private + + def method_body(method_name, const_name) + <<-EOMETHOD + def #{method_name}(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} + write_attribute(name, value) + end + EOMETHOD + end + }.new + extend ActiveSupport::Concern included do @@ -10,17 +25,27 @@ module ActiveRecord module ClassMethods protected - # See define_method_attribute in read.rb for an explanation of - # this code. - def define_method_attribute=(name) - safe_name = name.unpack('h*').first - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - write_attribute(AttrNames::ATTR_#{safe_name}, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + if Module.methods_transplantable? + def define_method_attribute=(name) + method = WriterMethodCache[name] + generated_attribute_methods.module_eval { + define_method "#{name}=", method + } + end + else + def define_method_attribute=(name) + safe_name = name.unpack('h*').first + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR + end end end @@ -28,24 +53,12 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name) - column = column_for_attribute(attr_name) - - # If we're dealing with a binary column, write the data to the cache - # so we don't attempt to typecast multiple times. - if column && column.binary? - @attributes_cache[attr_name] = value - end + write_attribute_with_type_cast(attr_name, value, true) + end - if column || @attributes.has_key?(attr_name) - @attributes[attr_name] = type_cast_attribute_for_write(column, value) - else - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end + def raw_write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, false) end - alias_method :raw_write_attribute, :write_attribute private # Handle *= for method_missing. @@ -53,10 +66,17 @@ module ActiveRecord write_attribute(attribute_name, value) end - def type_cast_attribute_for_write(column, value) - return value unless column + def write_attribute_with_type_cast(attr_name, value, should_type_cast) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key + + if should_type_cast + @attributes.write_from_user(attr_name, value) + else + @attributes.write_cast_value(attr_name, value) + end - column.type_cast_for_write value + value end end end |