提交 877ea784 编写于 作者: S Sean Griffin 提交者: Godfrey Chan

Implement `_was` and `changes` for in-place mutations of AR attributes

上级 88d27ae9
......@@ -114,7 +114,7 @@ module Dirty
include ActiveModel::AttributeMethods
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was', '_was='
attribute_method_affix prefix: 'reset_', suffix: '!'
attribute_method_affix prefix: 'restore_', suffix: '!'
end
......@@ -180,13 +180,26 @@ def attribute_was(attr) # :nodoc:
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
# Handle <tt>*_was=</tt> for +method_missing+
def attribute_was=(attr, old_value)
attributes_changed_by_setter[attr] = old_value
end
alias_method :set_attribute_was, :attribute_was=
# Restore all previous data of the provided attributes.
def restore_attributes(attributes = changed)
attributes.each { |attr| restore_attribute! attr }
end
# Remove changes information for the provided attributes.
def clear_attribute_changes(attributes)
attributes_changed_by_setter.except!(*attributes)
end
private
alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
# Removes current changes and makes them accessible through +previous_changes+.
def changes_applied # :doc:
@previously_changed = changes
......@@ -219,7 +232,7 @@ def attribute_will_change!(attr)
rescue TypeError, NoMethodError
end
changed_attributes[attr] = value
set_attribute_was(attr, value)
end
# Handle <tt>reset_*!</tt> for +method_missing+.
......@@ -233,7 +246,7 @@ def reset_attribute!(attr)
def restore_attribute!(attr)
if attribute_changed?(attr)
__send__("#{attr}=", changed_attributes[attr])
changed_attributes.delete(attr)
clear_attribute_changes([attr])
end
end
end
......
......@@ -103,7 +103,7 @@ def update_counter_in_memory(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.changed_attributes.delete(counter) # eww
owner.clear_attribute_changes([counter]) # eww
end
end
......
......@@ -30,10 +30,14 @@ def initialize(name, value_before_type_cast, type)
def value
# `defined?` is cheaper than `||=` when we get back falsy values
@value = type_cast(value_before_type_cast) unless defined?(@value)
@value = original_value unless defined?(@value)
@value
end
def original_value
type_cast(value_before_type_cast)
end
def value_for_database
type.type_cast_for_database(value)
end
......@@ -54,7 +58,7 @@ def with_value_from_database(value)
self.class.from_database(name, value, type)
end
def type_cast
def type_cast(*)
raise NotImplementedError
end
......
......@@ -51,14 +51,6 @@ def changed
super | changed_in_place
end
def attribute_changed?(attr_name, options = {})
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)
result
end
def changes_applied
super
store_original_raw_attributes
......@@ -69,12 +61,16 @@ def clear_changes_information
original_raw_attributes.clear
end
def changed_attributes
super.reverse_merge(attributes_changed_in_place).freeze
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)
set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
end
end
......@@ -100,9 +96,9 @@ def raw_write_attribute(attr, value)
def save_changed_attribute(attr, old_value)
if attribute_changed?(attr)
changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
else
changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
end
end
......@@ -132,6 +128,13 @@ 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|
changed_in_place?(attr_name)
......
......@@ -145,11 +145,11 @@ def save_changed_attribute(attr_name, old)
value = read_attribute(attr_name)
if attribute_changed?(attr_name)
if mapping[old] == value
changed_attributes.delete(attr_name)
clear_attribute_changes([attr_name])
end
else
if old != value
changed_attributes[attr_name] = mapping.key old
set_attribute_was(attr_name, mapping.key(old))
end
end
else
......
......@@ -466,7 +466,7 @@ def touch(*names)
changes[self.class.locking_column] = increment_lock if locking_enabled?
changed_attributes.except!(*changes.keys)
clear_attribute_changes(changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
else
......
......@@ -114,7 +114,7 @@ def current_time_from_proper_timezone
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
changed_attributes.delete(attribute_name)
clear_attribute_changes([attribute_name])
end
end
end
......
......@@ -661,6 +661,27 @@ def type_cast_for_database(value)
assert_not model.foo_changed?
end
test "in place mutation detection" do
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
assert pirate.catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", "arrrr matey!"]
}
assert_equal(expected_changes, pirate.changes)
assert_equal("arrrr", pirate.catchphrase_was)
assert pirate.catchphrase_changed?(from: "arrrr")
assert_not pirate.catchphrase_changed?(from: "anything else")
assert pirate.changed_attributes.include?(:catchphrase)
pirate.save!
pirate.reload
assert_equal "arrrr matey!", pirate.catchphrase
assert_not pirate.changed?
end
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册