未验证 提交 c820d8d7 编写于 作者: R Ryuta Kamizono 提交者: GitHub

Merge pull request #35933 from kamipo/refactor_dirty_tracking

PERF: 2x ~ 30x faster dirty tracking
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/object/duplicable"
module ActiveModel
class AttributeMutationTracker # :nodoc:
OPTION_NOT_GIVEN = Object.new
def initialize(attributes)
def initialize(attributes, forced_changes = Set.new)
@attributes = attributes
@forced_changes = Set.new
@forced_changes = forced_changes
end
def changed_attribute_names
......@@ -18,24 +19,22 @@ def changed_attribute_names
def changed_values
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if changed?(attr_name)
result[attr_name] = attributes[attr_name].original_value
result[attr_name] = original_value(attr_name)
end
end
end
def changes
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
change = change_to_attribute(attr_name)
if change
if change = change_to_attribute(attr_name)
result.merge!(attr_name => change)
end
end
end
def change_to_attribute(attr_name)
attr_name = attr_name.to_s
if changed?(attr_name)
[attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
[original_value(attr_name), fetch_value(attr_name)]
end
end
......@@ -44,29 +43,26 @@ def any_changes?
end
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
attr_name = attr_name.to_s
forced_changes.include?(attr_name) ||
attributes[attr_name].changed? &&
(OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
(OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
attribute_changed?(attr_name) &&
(OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
(OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
end
def changed_in_place?(attr_name)
attributes[attr_name.to_s].changed_in_place?
attributes[attr_name].changed_in_place?
end
def forget_change(attr_name)
attr_name = attr_name.to_s
attributes[attr_name] = attributes[attr_name].forgetting_assignment
forced_changes.delete(attr_name)
end
def original_value(attr_name)
attributes[attr_name.to_s].original_value
attributes[attr_name].original_value
end
def force_change(attr_name)
forced_changes << attr_name.to_s
forced_changes << attr_name
end
private
......@@ -75,45 +71,108 @@ def force_change(attr_name)
def attr_names
attributes.keys
end
def attribute_changed?(attr_name)
forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
end
def fetch_value(attr_name)
attributes.fetch_value(attr_name)
end
end
class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
def initialize(attributes, forced_changes = {})
super
@finalized_changes = nil
end
def changed_in_place?(attr_name)
false
end
def change_to_attribute(attr_name)
if finalized_changes&.include?(attr_name)
finalized_changes[attr_name].dup
else
super
end
end
def forget_change(attr_name)
forced_changes.delete(attr_name)
end
def original_value(attr_name)
if changed?(attr_name)
forced_changes[attr_name]
else
fetch_value(attr_name)
end
end
def force_change(attr_name)
forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
end
def finalize_changes
@finalized_changes = changes
end
private
attr_reader :finalized_changes
def attr_names
forced_changes.keys
end
def attribute_changed?(attr_name)
forced_changes.include?(attr_name)
end
def fetch_value(attr_name)
attributes.send(:_read_attribute, attr_name)
end
def clone_value(attr_name)
value = fetch_value(attr_name)
value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
value
end
end
class NullMutationTracker # :nodoc:
include Singleton
def changed_attribute_names(*)
def changed_attribute_names
[]
end
def changed_values(*)
def changed_values
{}
end
def changes(*)
def changes
{}
end
def change_to_attribute(attr_name)
end
def any_changes?(*)
def any_changes?
false
end
def changed?(*)
def changed?(attr_name, **)
false
end
def changed_in_place?(*)
def changed_in_place?(attr_name)
false
end
def forget_change(*)
end
def original_value(*)
end
def force_change(*)
def original_value(attr_name)
end
end
end
# frozen_string_literal: true
require "active_support/hash_with_indifferent_access"
require "active_support/core_ext/object/duplicable"
require "active_model/attribute_mutation_tracker"
module ActiveModel
......@@ -122,9 +120,6 @@ module Dirty
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
OPTION_NOT_GIVEN = Object.new # :nodoc:
private_constant :OPTION_NOT_GIVEN
included do
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
attribute_method_suffix "_previously_changed?", "_previous_change"
......@@ -145,10 +140,9 @@ def initialize_dup(other) # :nodoc:
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
def changes_applied
unless defined?(@attributes)
@previously_changed = changes
mutations_from_database.finalize_changes
end
@mutations_before_last_save = mutations_from_database
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
......@@ -159,7 +153,7 @@ def changes_applied
# person.name = 'bob'
# person.changed? # => true
def changed?
changed_attributes.present?
mutations_from_database.any_changes?
end
# Returns an array with the name of the attributes with unsaved changes.
......@@ -168,42 +162,37 @@ def changed?
# person.name = 'bob'
# person.changed # => ["name"]
def changed
changed_attributes.keys
mutations_from_database.changed_attribute_names
end
# Handles <tt>*_changed?</tt> for +method_missing+.
def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
!!changes_include?(attr) &&
(to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
(from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
def attribute_changed?(attr_name, **options) # :nodoc:
mutations_from_database.changed?(attr_name.to_s, options)
end
# Handles <tt>*_was</tt> for +method_missing+.
def attribute_was(attr) # :nodoc:
attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
def attribute_was(attr_name) # :nodoc:
mutations_from_database.original_value(attr_name.to_s)
end
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
def attribute_previously_changed?(attr) #:nodoc:
previous_changes_include?(attr)
def attribute_previously_changed?(attr_name) # :nodoc:
mutations_before_last_save.changed?(attr_name.to_s)
end
# Restore all previous data of the provided attributes.
def restore_attributes(attributes = changed)
attributes.each { |attr| restore_attribute! attr }
def restore_attributes(attr_names = changed)
attr_names.each { |attr_name| restore_attribute!(attr_name) }
end
# Clears all dirty data: current changes and previous changes.
def clear_changes_information
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
def clear_attribute_changes(attr_names)
attributes_changed_by_setter.except!(*attr_names)
attr_names.each do |attr_name|
clear_attribute_change(attr_name)
end
......@@ -216,13 +205,7 @@ def clear_attribute_changes(attr_names)
# person.name = 'robert'
# person.changed_attributes # => {"name" => "bob"}
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
attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
end
mutations_from_database.changed_values
end
# Returns a hash of changed attributes indicating their original
......@@ -232,9 +215,7 @@ def changed_attributes
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
cache_changed_attributes do
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
end
mutations_from_database.changes
end
# Returns a hash of attributes that were changed before the model was saved.
......@@ -244,27 +225,23 @@ def changes
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
@previously_changed.merge(mutations_before_last_save.changes)
mutations_before_last_save.changes
end
def attribute_changed_in_place?(attr_name) # :nodoc:
mutations_from_database.changed_in_place?(attr_name)
mutations_from_database.changed_in_place?(attr_name.to_s)
end
private
def clear_attribute_change(attr_name)
mutations_from_database.forget_change(attr_name)
mutations_from_database.forget_change(attr_name.to_s)
end
def mutations_from_database
unless defined?(@mutations_from_database)
@mutations_from_database = nil
end
@mutations_from_database ||= if defined?(@attributes)
ActiveModel::AttributeMutationTracker.new(@attributes)
else
NullMutationTracker.instance
ActiveModel::ForcedMutationTracker.new(self)
end
end
......@@ -276,68 +253,28 @@ def mutations_before_last_save
@mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
end
def cache_changed_attributes
@cached_changed_attributes = changed_attributes
yield
ensure
clear_changed_attributes_cache
end
def clear_changed_attributes_cache
remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
end
# Returns +true+ if attr_name is changed, +false+ otherwise.
def changes_include?(attr_name)
attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
end
alias attribute_changed_by_setter? changes_include?
# Returns +true+ if attr_name were changed before the model was saved,
# +false+ otherwise.
def previous_changes_include?(attr_name)
previous_changes.include?(attr_name)
end
# Handles <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
def attribute_change(attr_name)
mutations_from_database.change_to_attribute(attr_name.to_s)
end
# Handles <tt>*_previous_change</tt> for +method_missing+.
def attribute_previous_change(attr)
previous_changes[attr]
def attribute_previous_change(attr_name)
mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
unless attribute_changed?(attr)
begin
value = _read_attribute(attr)
value = value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
end
set_attribute_was(attr, value)
end
mutations_from_database.force_change(attr)
def attribute_will_change!(attr_name)
mutations_from_database.force_change(attr_name.to_s)
end
# Handles <tt>restore_*!</tt> for +method_missing+.
def restore_attribute!(attr)
if attribute_changed?(attr)
__send__("#{attr}=", changed_attributes[attr])
clear_attribute_changes([attr])
def restore_attribute!(attr_name)
attr_name = attr_name.to_s
if attribute_changed?(attr_name)
__send__("#{attr_name}=", attribute_was(attr_name))
clear_attribute_change(attr_name)
end
end
def attributes_changed_by_setter
@attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
end
# Force an attribute to have a particular "before" value
def set_attribute_was(attr, old_value)
attributes_changed_by_setter[attr] = old_value
end
end
end
......@@ -29,9 +29,7 @@ module Dirty
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
@mutations_from_database = nil
end
end
......@@ -51,7 +49,7 @@ def reload(*)
# +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)
mutations_before_last_save.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute during the last save. If the
......@@ -63,7 +61,7 @@ def saved_change_to_attribute?(attr_name, **options)
# invoked as +saved_change_to_name+ instead of
# <tt>saved_change_to_attribute("name")</tt>.
def saved_change_to_attribute(attr_name)
mutations_before_last_save.change_to_attribute(attr_name)
mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
# Returns the original value of an attribute before the last save.
......@@ -73,7 +71,7 @@ def saved_change_to_attribute(attr_name)
# invoked as +name_before_last_save+ instead of
# <tt>attribute_before_last_save("name")</tt>.
def attribute_before_last_save(attr_name)
mutations_before_last_save.original_value(attr_name)
mutations_before_last_save.original_value(attr_name.to_s)
end
# Did the last call to +save+ have any changes to change?
......@@ -101,7 +99,7 @@ def saved_changes
# +to+ When passed, this method will return false unless the value will be
# changed to the given value
def will_save_change_to_attribute?(attr_name, **options)
mutations_from_database.changed?(attr_name, **options)
mutations_from_database.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute that will be persisted during the
......@@ -115,7 +113,7 @@ def will_save_change_to_attribute?(attr_name, **options)
# If the attribute will change, the result will be an array containing the
# original value and the new value about to be saved.
def attribute_change_to_be_saved(attr_name)
mutations_from_database.change_to_attribute(attr_name)
mutations_from_database.change_to_attribute(attr_name.to_s)
end
# Returns the value of an attribute in the database, as opposed to the
......@@ -127,7 +125,7 @@ def attribute_change_to_be_saved(attr_name)
# saved. It can be invoked as +name_in_database+ instead of
# <tt>attribute_in_database("name")</tt>.
def attribute_in_database(attr_name)
mutations_from_database.original_value(attr_name)
mutations_from_database.original_value(attr_name.to_s)
end
# Will the next call to +save+ have any changes to persist?
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册