aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/dirty.rb
blob: 7e246e62caf7caf442c5ea336e27cada93441f55 (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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
module ActiveRecord
  # Track unsaved attribute changes.
  #
  # A newly instantiated object is unchanged:
  #   person = Person.find_by_name('uncle bob')
  #   person.changed?       # => false
  #
  # Change the name:
  #   person.name = 'Bob'
  #   person.changed?       # => true
  #   person.name_changed?  # => true
  #   person.name_was       # => 'uncle bob'
  #   person.name_change    # => ['uncle bob', 'Bob']
  #   person.name = 'Bill'
  #   person.name_change    # => ['uncle bob', 'Bill']
  #
  # Save the changes:
  #   person.save
  #   person.changed?       # => false
  #   person.name_changed?  # => false
  #
  # Assigning the same value leaves the attribute unchanged:
  #   person.name = 'Bill'
  #   person.name_changed?  # => false
  #   person.name_change    # => nil
  #
  # Which attributes have changed?
  #   person.name = 'bob'
  #   person.changed        # => ['name']
  #   person.changes        # => { 'name' => ['Bill', 'bob'] }
  #
  # Before modifying an attribute in-place:
  #   person.name_will_change!
  #   person.name << 'by'
  #   person.name_change    # => ['uncle bob', 'uncle bobby']
  module Dirty
    def self.included(base)
      base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
      base.alias_method_chain :write_attribute, :dirty
      base.alias_method_chain :save,            :dirty
      base.alias_method_chain :save!,           :dirty
      base.alias_method_chain :update,          :dirty
      base.alias_method_chain :reload,          :dirty

      base.superclass_delegating_accessor :partial_updates
      base.partial_updates = true
    end

    # Do any attributes have unsaved changes?
    #   person.changed? # => false
    #   person.name = 'bob'
    #   person.changed? # => true
    def changed?
      !changed_attributes.empty?
    end

    # List of attributes with unsaved changes.
    #   person.changed # => []
    #   person.name = 'bob'
    #   person.changed # => ['name']
    def changed
      changed_attributes.keys
    end

    # Map of changed attrs => [original value, new value].
    #   person.changes # => {}
    #   person.name = 'bob'
    #   person.changes # => { 'name' => ['bill', 'bob'] }
    def changes
      changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
    end

    # Attempts to +save+ the record and clears changed attributes if successful.
    def save_with_dirty(*args) #:nodoc:
      if status = save_without_dirty(*args)
        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:
      status = save_without_dirty!(*args)
      changed_attributes.clear
      status
    end

    # <tt>reload</tt> the record and clears changed attributes.
    def reload_with_dirty(*args) #:nodoc:
      record = reload_without_dirty(*args)
      changed_attributes.clear
      record
    end

    private
      # Map of change <tt>attr => original value</tt>.
      def changed_attributes
        @changed_attributes ||= {}
      end

      # Handle <tt>*_changed?</tt> for +method_missing+.
      def attribute_changed?(attr)
        changed_attributes.include?(attr)
      end

      # Handle <tt>*_change</tt> for +method_missing+.
      def attribute_change(attr)
        [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
      end

      # Handle <tt>*_was</tt> for +method_missing+.
      def attribute_was(attr)
        attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
      end

      # Handle <tt>*_will_change!</tt> for +method_missing+.
      def attribute_will_change!(attr)
        changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
      end

      # Wrap write_attribute to remember original attribute value.
      def write_attribute_with_dirty(attr, value)
        attr = attr.to_s

        # The attribute already has an unsaved change.
        if changed_attributes.include?(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.
        write_attribute_without_dirty(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 | 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.type == :integer && column.null && (old.nil? || old == 0)
            # For nullable integer 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 if value.blank?
          else
            value = column.type_cast(value)
          end
        end

        old != value
      end

  end
end