aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/attribute_methods/dirty.rb
blob: 435aea9b09659ed5a7d274c86a766392e0c19811 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
require 'active_support/core_ext/object/blank'

module ActiveRecord
  module AttributeMethods
    module Dirty
      extend ActiveSupport::Concern
      include ActiveModel::Dirty

      included do
        alias_method_chain :save,   :dirty
        alias_method_chain :save!,  :dirty
        alias_method_chain :update, :dirty
        alias_method_chain :reload, :dirty

        superclass_delegating_accessor :partial_updates
        self.partial_updates = true
      end

      # Attempts to +save+ the record and clears changed attributes if successful.
      def save_with_dirty(*args) #:nodoc:
        if status = save_without_dirty(*args)
          @previously_changed = changes
          @changed_attributes.clear
        end
        status
      end

      # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
      def save_with_dirty!(*args) #:nodoc:
        save_without_dirty!(*args).tap do
          @previously_changed = changes
          @changed_attributes.clear
        end
      end

      # <tt>reload</tt> the record and clears changed attributes.
      def reload_with_dirty(*args) #:nodoc:
        reload_without_dirty(*args).tap do
          @previously_changed.clear
          @changed_attributes.clear
        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.
          if attribute_changed?(attr)
            old = @changed_attributes[attr]
            @changed_attributes.delete(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)
          end

          # Carry on.
          super(attr, value)
        end

        def update_with_dirty
          if partial_updates?
            # Serialized attributes should always be written in case they've been
            # changed in place.
            update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
          else
            update_without_dirty
          end
        end

        def field_changed?(attr, old, value)
          if column = column_for_attribute(attr)
            if column.number? && column.null && (old.nil? || old == 0) && value.blank?
              # 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)
              value = nil
            else
              value = column.type_cast(value)
            end
          end

          old != value
        end
    end
  end
end