diff options
author | Rafael França <rafaelmfranca@gmail.com> | 2016-12-29 02:18:38 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-29 02:18:38 -0500 |
commit | eb6a6141a66e052deb58580448d7c0fa6aa675ba (patch) | |
tree | 0017a57044ec8e466c97cc6b7879d39caa6c1ffa /activerecord/lib/active_record/attribute_methods | |
parent | 447e1a48811fa053e8bef954376d1ad47bdb5cef (diff) | |
parent | fcec126eaa4f835c837bc75efa78008667b2ec5b (diff) | |
download | rails-eb6a6141a66e052deb58580448d7c0fa6aa675ba.tar.gz rails-eb6a6141a66e052deb58580448d7c0fa6aa675ba.tar.bz2 rails-eb6a6141a66e052deb58580448d7c0fa6aa675ba.zip |
Merge branch 'master' into clear_all_environments_log_by_default
Diffstat (limited to 'activerecord/lib/active_record/attribute_methods')
6 files changed, 198 insertions, 15 deletions
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index c9638bf70b..e20b65e43c 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "active_support/core_ext/module/attribute_accessors" require "active_record/attribute_mutation_tracker" @@ -15,6 +16,18 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false self.partial_writes = true + + after_create { changes_internally_applied } + after_update { changes_internally_applied } + + # Attribute methods for "changed in last call to save?" + attribute_method_affix(prefix: "saved_change_to_", suffix: "?") + attribute_method_prefix("saved_change_to_") + attribute_method_suffix("_before_last_save") + + # Attribute methods for "will change if I call save?" + attribute_method_affix(prefix: "will_save_change_to_", suffix: "?") + attribute_method_suffix("_change_to_be_saved", "_in_database") end # Attempts to +save+ the record and clears changed attributes if successful. @@ -35,8 +48,8 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @mutation_tracker = nil @previous_mutation_tracker = nil + clear_mutation_trackers @changed_attributes = HashWithIndifferentAccess.new end end @@ -46,19 +59,26 @@ module ActiveRecord @attributes = self.class._default_attributes.map do |attr| attr.with_value_from_user(@attributes.fetch_value(attr.name)) end - @mutation_tracker = nil + clear_mutation_trackers + end + + def changes_internally_applied # :nodoc: + @mutations_before_last_save = mutation_tracker + forget_attribute_assignments + @mutations_from_database = AttributeMutationTracker.new(@attributes) end def changes_applied @previous_mutation_tracker = mutation_tracker @changed_attributes = HashWithIndifferentAccess.new - store_original_attributes + clear_mutation_trackers end def clear_changes_information @previous_mutation_tracker = nil @changed_attributes = HashWithIndifferentAccess.new - store_original_attributes + forget_attribute_assignments + clear_mutation_trackers end def raw_write_attribute(attr_name, *) @@ -80,17 +100,27 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else + emit_warning_if_needed("changed_attributes", "saved_changes.transform_values(&:first)") super.reverse_merge(mutation_tracker.changed_values).freeze end end def changes cache_changed_attributes do + emit_warning_if_needed("changes", "saved_changes") super end end def previous_changes + unless previous_mutation_tracker.equal?(mutations_before_last_save) + ActiveSupport::Deprecation.warn(<<-EOW.strip_heredoc) + The behavior of `previous_changes` inside of after callbacks is + deprecated without replacement. In the next release of Rails, + this method inside of `after_save` will return the changes that + were just saved. + EOW + end previous_mutation_tracker.changes end @@ -98,6 +128,109 @@ module ActiveRecord mutation_tracker.changed_in_place?(attr_name) end + # Did this attribute change when we last saved? This method can be invoked + # as `saved_change_to_name?` instead of `saved_change_to_attribute?("name")`. + # Behaves similarly to +attribute_changed?+. This method is useful in + # after callbacks to determine if the call to save changed a certain + # attribute. + # + # ==== Options + # + # +from+ When passed, this method will return false unless the original + # value is equal to the given option + # + # +to+ When passed, this method will return false unless the value was + # changed to the given value + def saved_change_to_attribute?(attr_name, **options) + mutations_before_last_save.changed?(attr_name, **options) + end + + # Returns the change to an attribute during the last save. If the + # attribute was changed, the result will be an array containing the + # original value and the saved value. + # + # Behaves similarly to +attribute_change+. This method is useful in after + # callbacks, to see the change in an attribute that just occurred + # + # This method can be invoked as `saved_change_to_name` in instead of + # `saved_change_to_attribute("name")` + def saved_change_to_attribute(attr_name) + mutations_before_last_save.change_to_attribute(attr_name) + end + + # Returns the original value of an attribute before the last save. + # Behaves similarly to +attribute_was+. This method is useful in after + # callbacks to get the original value of an attribute before the save that + # just occurred + def attribute_before_last_save(attr_name) + mutations_before_last_save.original_value(attr_name) + end + + # Did the last call to `save` have any changes to change? + def saved_changes? + mutations_before_last_save.any_changes? + end + + # Returns a hash containing all the changes that were just saved. + def saved_changes + mutations_before_last_save.changes + end + + # Alias for `attribute_changed?` + def will_save_change_to_attribute?(attr_name, **options) + mutations_from_database.changed?(attr_name, **options) + end + + # Alias for `attribute_change` + def attribute_change_to_be_saved(attr_name) + mutations_from_database.change_to_attribute(attr_name) + end + + # Alias for `attribute_was` + def attribute_in_database(attr_name) + mutations_from_database.original_value(attr_name) + end + + # Alias for `changed?` + def has_changes_to_save? + mutations_from_database.any_changes? + end + + # Alias for `changes` + def changes_to_save + mutations_from_database.changes + end + + # Alias for `changed` + def changed_attribute_names_to_save + changes_to_save.keys + end + + # Alias for `changed_attributes` + def attributes_in_database + changes_to_save.transform_values(&:first) + end + + def attribute_was(*) + emit_warning_if_needed("attribute_was", "attribute_before_last_save") + super + end + + def attribute_change(*) + emit_warning_if_needed("attribute_change", "saved_change_to_attribute") + super + end + + def attribute_changed?(*) + emit_warning_if_needed("attribute_changed?", "saved_change_to_attribute?") + super + end + + def changed(*) + emit_warning_if_needed("changed", "saved_changes.keys") + super + end + private def mutation_tracker @@ -107,12 +240,37 @@ module ActiveRecord @mutation_tracker ||= AttributeMutationTracker.new(@attributes) end + def emit_warning_if_needed(method_name, new_method_name) + unless mutation_tracker.equal?(mutations_from_database) + ActiveSupport::Deprecation.warn(<<-EOW.squish) + The behavior of `#{method_name}` inside of after callbacks will + be changing in the next version of Rails. The new return value will reflect the + behavior of calling the method after `save` returned (e.g. the opposite of what + it returns now). To maintain the current behavior, use `#{new_method_name}` + instead. + EOW + end + end + + def mutations_from_database + unless defined?(@mutations_from_database) + @mutations_from_database = nil + end + @mutations_from_database ||= mutation_tracker + end + def changes_include?(attr_name) super || mutation_tracker.changed?(attr_name) end def clear_attribute_change(attr_name) mutation_tracker.forget_change(attr_name) + mutations_from_database.forget_change(attr_name) + end + + def attribute_will_change!(attr_name) + super + mutations_from_database.force_change(attr_name) end def _update_record(*) @@ -124,18 +282,27 @@ module ActiveRecord end def keys_for_partial_write - changed & self.class.column_names + changed_attribute_names_to_save & self.class.column_names end - def store_original_attributes + def forget_attribute_assignments @attributes = @attributes.map(&:forgetting_assignment) + end + + def clear_mutation_trackers @mutation_tracker = nil + @mutations_from_database = nil + @mutations_before_last_save = nil end def previous_mutation_tracker @previous_mutation_tracker ||= NullMutationTracker.instance end + def mutations_before_last_save + @mutations_before_last_save ||= previous_mutation_tracker + end + def cache_changed_attributes @cached_changed_attributes = changed_attributes yield diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 6243398a52..8fcac82a0d 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -45,7 +45,12 @@ module ActiveRecord attribute_was(self.class.primary_key) end - protected + def id_in_database + sync_with_transaction_state + attribute_in_database(self.class.primary_key) + end + + private def attribute_method?(attr_name) attr_name == "id" || super @@ -60,7 +65,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 05f0e974b6..10498f4322 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -19,7 +19,7 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else - return false if ActiveRecord::Type::Boolean::FALSE_VALUES.include?(value) + return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) !value.blank? end elsif value.respond_to?(:zero?) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 30f7750884..369a6e35aa 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -4,7 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - protected + private # We want to generate the methods via module_eval rather than # define_method, because define_method is slower on dispatch. @@ -48,7 +48,12 @@ module ActiveRecord # 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, &block) - name = attr_name.to_s + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + name = self.class.primary_key if name == "id".freeze _read_attribute(name, &block) 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 bea1514cdf..500d903857 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -39,7 +39,7 @@ module ActiveRecord end def set_time_zone_without_conversion(value) - ::Time.zone.local_to_utc(value).in_time_zone if value + ::Time.zone.local_to_utc(value).try(:in_time_zone) if value end def map_avoiding_infinite_recursion(value) @@ -70,6 +70,7 @@ module ActiveRecord private def inherited(subclass) + super # 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 @@ -80,7 +81,6 @@ module ActiveRecord TimeZoneConverter.new(type) end end - super end def create_time_zone_conversion_attribute?(name, cast_type) diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index f65c297e01..fe0e01db28 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -8,7 +8,7 @@ module ActiveRecord end module ClassMethods - protected + private def define_method_attribute=(name) safe_name = name.unpack("h*".freeze).first @@ -29,7 +29,13 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - write_attribute_with_type_cast(attr_name, value, true) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + write_attribute_with_type_cast(name, value, true) end def raw_write_attribute(attr_name, value) # :nodoc: |