diff options
Diffstat (limited to 'activerecord/lib/active_record/attribute_methods')
8 files changed, 184 insertions, 175 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 fd61febd57..1db6776688 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -2,7 +2,7 @@ module ActiveRecord module AttributeMethods # = Active Record Attribute Methods Before Type Cast # - # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to + # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to # read the value of the attributes before typecasting and deserialization. # # class Task < ActiveRecord::Base @@ -28,6 +28,7 @@ module ActiveRecord included do attribute_method_suffix "_before_type_cast" + attribute_method_suffix "_came_from_user?" end # Returns the value of the attribute identified by +attr_name+ before @@ -66,6 +67,10 @@ module ActiveRecord def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end + + def attribute_came_from_user?(attribute_name) + @attributes[attribute_name].came_from_user? + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index b58295a106..0bcfa5f00d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/module/attribute_accessors' +require 'active_record/attribute_mutation_tracker' module ActiveRecord module AttributeMethods @@ -34,84 +35,84 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - clear_changes_information + @mutation_tracker = nil + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new end end def initialize_dup(other) # :nodoc: super - calculate_changes_from_defaults + @attributes = self.class._default_attributes.map do |attr| + attr.with_value_from_user(@attributes.fetch_value(attr.name)) + end + @mutation_tracker = nil end - def changed? - super || changed_in_place.any? + def changes_applied + @previous_mutation_tracker = mutation_tracker + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes end - def changed - super | changed_in_place + def clear_changes_information + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes end - def attribute_changed?(attr_name, options = {}) + def raw_write_attribute(attr_name, *) result = super - # We can't change "from" something in place. Only setters can define - # "from" and "to" - result ||= changed_in_place?(attr_name) unless options.key?(:from) + clear_attribute_change(attr_name) result end - def changes_applied + def clear_attribute_changes(attr_names) super - store_original_raw_attributes + attr_names.each do |attr_name| + clear_attribute_change(attr_name) + end end - def clear_changes_information - super - original_raw_attributes.clear + 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(mutation_tracker.changed_values).freeze + end end - private - - def calculate_changes_from_defaults - @changed_attributes = nil - self.class.column_defaults.each do |attr, orig_value| - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) + def changes + cache_changed_attributes do + super end end - # Wrap write_attribute to remember original attribute value. - def write_attribute(attr, value) - attr = attr.to_s - - old_value = old_attribute_value(attr) + def previous_changes + previous_mutation_tracker.changes + end - result = super - store_original_raw_attribute(attr) - save_changed_attribute(attr, old_value) - result + def attribute_changed_in_place?(attr_name) + mutation_tracker.changed_in_place?(attr_name) end - def raw_write_attribute(attr, value) - attr = attr.to_s + private - result = super - original_raw_attributes[attr] = value - result + def mutation_tracker + unless defined?(@mutation_tracker) + @mutation_tracker = nil + end + @mutation_tracker ||= AttributeMutationTracker.new(@attributes) end - def save_changed_attribute(attr, old_value) - if attribute_changed?(attr) - changed_attributes.delete(attr) unless _field_changed?(attr, old_value) - else - changed_attributes[attr] = old_value if _field_changed?(attr, old_value) - end + def changes_include?(attr_name) + super || mutation_tracker.changed?(attr_name) end - def old_attribute_value(attr) - if attribute_changed?(attr) - changed_attributes[attr] - else - clone_attribute_value(:read_attribute, attr) - end + def clear_attribute_change(attr_name) + mutation_tracker.forget_change(attr_name) end def _update_record(*) @@ -122,45 +123,28 @@ module ActiveRecord 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 - end - - def _field_changed?(attr, old_value) - @attributes[attr].changed_from?(old_value) - end - - def changed_in_place - self.class.attribute_names.select do |attr_name| - changed_in_place?(attr_name) - end + changed & self.class.column_names end - def changed_in_place?(attr_name) - old_value = original_raw_attribute(attr_name) - @attributes[attr_name].changed_in_place_from?(old_value) + def store_original_attributes + @attributes = @attributes.map(&:forgetting_assignment) + @mutation_tracker = nil end - def original_raw_attribute(attr_name) - original_raw_attributes.fetch(attr_name) do - read_attribute_before_type_cast(attr_name) - end - end - - def original_raw_attributes - @original_raw_attributes ||= {} + def previous_mutation_tracker + @previous_mutation_tracker ||= NullMutationTracker.instance end - def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + clear_changed_attributes_cache end - def store_original_raw_attributes - attribute_names.each do |attr| - store_original_raw_attribute(attr) - end + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@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 cadad60ddd..0d5cb8b37c 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,7 +5,7 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array if one is + # Returns this record's primary key value wrapped in an array if one is # available. def to_key sync_with_transaction_state @@ -17,7 +17,7 @@ module ActiveRecord def id if pk = self.class.primary_key sync_with_transaction_state - read_attribute(pk) + _read_attribute(pk) end end @@ -39,6 +39,12 @@ module ActiveRecord 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) @@ -54,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) @@ -102,7 +108,7 @@ module ActiveRecord # self.primary_key = 'sysid' # end # - # You can also define the +primary_key+ method yourself: + # You can also define the #primary_key method yourself: # # class Project < ActiveRecord::Base # def self.primary_key @@ -114,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..10498f4322 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 @@ -19,10 +19,10 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) !value.blank? end - elsif column.number? + elsif value.respond_to?(:zero?) !value.zero? else !value.blank? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 10869dfc1e..5197e21fa4 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/method_transplanting' - module ActiveRecord module AttributeMethods module Read @@ -27,7 +25,7 @@ module ActiveRecord <<-EOMETHOD def #{method_name} name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - read_attribute(name) { |n| missing_attribute(n, caller) } + _read_attribute(name) { |n| missing_attribute(n, caller) } end EOMETHOD end @@ -36,44 +34,24 @@ module ActiveRecord extend ActiveSupport::Concern 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(<<-MESSAGE.strip_heredoc) - Calling `#{method_name}` is no longer necessary. All attributes are cached. - MESSAGE - 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 + def define_method_attribute(name) + safe_name = name.unpack('h*'.freeze).first + temp_method = "__temp__#{safe_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 + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + 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 @@ -83,15 +61,27 @@ module ActiveRecord # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) name = attr_name.to_s - name = self.class.primary_key if name == 'id' - @attributes.fetch_value(name, &block) + name = self.class.primary_key if name == 'id'.freeze + _read_attribute(name, &block) end - private - - def attribute(attribute_name) - read_attribute(attribute_name) + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the read_attribute API + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def _read_attribute(attr_name, &block) # :nodoc + @attributes.fetch_value(attr_name.to_s, &block) + end + else + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + end end + + alias :attribute :_read_attribute + private :attribute + end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 264ce2bdfa..65978aea2a 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -8,11 +8,20 @@ module ActiveRecord # 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 SerializationTypeMismatch will be raised. # - # A notable side effect of serialized attributes is that the model will - # be updated on every save, even if it is not dirty. + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of + # +Array+, will always be persisted as null. + # + # Keep in mind that database adapters handle certain serialization tasks + # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be + # converted between JSON object/array syntax and Ruby +Hash+ or +Array+ + # objects transparently. There is no need to use #serialize in this + # case. + # + # For more complex cases, such as conversion to or from your application + # domain objects, consider using the ActiveRecord::Attributes API. # # ==== Parameters # @@ -52,18 +61,6 @@ module ActiveRecord Type::Serialized.new(type, coder) end end - - def serialized_attributes - ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) - `serialized_attributes` is deprecated without replacement, and will - be removed in Rails 5.0. - WARNING - @serialized_attributes ||= Hash[ - columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| - [c.name, c.cast_type.coder] - } - ] - end 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 f439bd1ffe..9e693b6aee 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,19 +1,27 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class TimeZoneConverter < SimpleDelegator # :nodoc: - def type_cast_from_database(value) + class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: + def deserialize(value) convert_time_to_time_zone(super) end - def type_cast_from_user(value) + def cast(value) if value.is_a?(Array) - value.map { |v| type_cast_from_user(v) } + value.map { |v| cast(v) } + elsif value.is_a?(Hash) + set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) - value.in_time_zone + begin + super(user_input_in_time_zone(value)) || super + rescue ArgumentError + nil + end end end + private + def convert_time_to_time_zone(value) if value.is_a?(Array) value.map { |v| convert_time_to_time_zone(v) } @@ -23,6 +31,10 @@ module ActiveRecord value end end + + def set_time_zone_without_conversion(value) + ::Time.zone.local_to_utc(value).in_time_zone + end end extend ActiveSupport::Concern @@ -33,6 +45,9 @@ module ActiveRecord class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false self.skip_time_zone_conversion_for_attributes = [] + + class_attribute :time_zone_aware_types, instance_writer: false + self.time_zone_aware_types = [:datetime, :not_explicitly_configured] end module ClassMethods @@ -53,9 +68,31 @@ module ActiveRecord end 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 == cast_type.type) + enabled_for_column = time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) + result = enabled_for_column && + time_zone_aware_types.include?(cast_type.type) + + if enabled_for_column && + !result && + cast_type.type == :time && + time_zone_aware_types.include?(:not_explicitly_configured) + ActiveSupport::Deprecation.warn(<<-MESSAGE) + Time columns will become time zone aware in Rails 5.1. This + still causes `String`s to be parsed as if they were in `Time.zone`, + and `Time`s to be converted to `Time.zone`. + + To keep the old behavior, you must add the following to your initializer: + + config.active_record.time_zone_aware_types = [:datetime] + + To silence this deprecation warning, add the following: + + config.active_record.time_zone_aware_types << :time + MESSAGE + end + + result end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index b3c8209a74..bbf2a51a0e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/method_transplanting' - module ActiveRecord module AttributeMethods module Write @@ -25,27 +23,18 @@ module ActiveRecord module ClassMethods protected - 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 + def define_method_attribute=(name) + safe_name = name.unpack('h*'.freeze).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 + 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 @@ -56,7 +45,7 @@ module ActiveRecord write_attribute_with_type_cast(attr_name, value, true) end - def raw_write_attribute(attr_name, value) + def raw_write_attribute(attr_name, value) # :nodoc: write_attribute_with_type_cast(attr_name, value, false) end @@ -73,7 +62,7 @@ module ActiveRecord if should_type_cast @attributes.write_from_user(attr_name, value) else - @attributes.write_from_database(attr_name, value) + @attributes.write_cast_value(attr_name, value) end value |