aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/dirty.rb
blob: 603496381153cbf05d76cacb66405be81aea30b9 (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
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.superclass_delegating_accessor :partial_updates
      base.partial_updates = false
    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

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

      # Handle *_changed? for method_missing.
      def attribute_changed?(attr)
        changed_attributes.include?(attr)
      end

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

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

      # Handle *_will_change! 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.
        unless changed_attributes.include?(attr)
          old = clone_attribute_value(:read_attribute, attr)

          # Remember the original value if it's different.
          typecasted = if column = column_for_attribute(attr)
                         column.type_cast(value)
                       else
                         value
                       end
          changed_attributes[attr] = old unless old == typecasted
        end

        # Carry on.
        write_attribute_without_dirty(attr, value)
      end

      def update_with_dirty
        if partial_updates?
          update_without_dirty(changed)
        else
          update_without_dirty
        end
      end
  end
end