msgbartop
Just a little about development! Think before you write!
msgbarbottom

01 May 08 Dirty models and partial SQL updates for Rails 2.0.2

First of all, this is not my code, this is just a backport of a Rails EDGE feature to work with Rails 2.0.2 …

I searched through Rails code and found the points tha needed some changes to work with rails 2.0.2, and not the only thing you need if you want this features but can’t use EDGE right now, just place this code into a file in the config/initializers (I named it dirty.rb) …

So, let’s stop the cheap talk and show the code:

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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
module ActiveRecord
 
  class Base
 
	  # Updates the associated record with values matching those of the instance attributes.
 
	  # Returns the number of affected rows.
 
	  def update(attribute_names = @attributes.keys)
 
		quoted_attributes = attributes_with_quotes(false, false, attribute_names)
 
		return 0 if quoted_attributes.empty?
 
		connection.update(
 
		  "UPDATE #{self.class.quoted_table_name} " +
 
		  "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " +
 
		  "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
 
		  "#{self.class.name} Update"
 
		)
 
	  end
 
      # Returns a copy of the attributes hash where all the values have been safely quoted for use in
 
     # an SQL statement.
 
     def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
 
       quoted = {}
 
       connection = self.class.connection
 
       attribute_names.each do |name|
 
         if column = column_for_attribute(name)
 
           quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary
 
         end
 
       end
 
       include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
 
     end
 
  end
 
  # 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 = 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
 
 
 
 
 
    # Clear changed attributes after they are saved.
 
    def save_with_dirty(*args) #:nodoc:
 
      save_without_dirty(*args)
 
    ensure
 
      changed_attributes.clear
 
    end
 
 
 
    # Clear changed attributes after they are saved.
 
    def save_with_dirty!(*args) #:nodoc:
 
      save_without_dirty!(*args)
 
    ensure
 
      changed_attributes.clear
 
    end
 
 
 
    def self.partial_updates?
 
      @partial_updates
 
    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
 
        update_without_dirty(changed)
 
      end
 
  end
 
end
 
 
 
ActiveRecord::Base.send :include, ActiveRecord::Dirty
 
#ActiveRecord::Base.partial_updates = true -- Now there is no way to disable this ugly partial updates hack

I hope it can be usefull for others too :D
you can see how it works in this posts about rails EDGE.
If you have any problem, just leave a comment.

PS.: yes, it can be cleaned up a little, but it is already working fine for me, and it is just a backport :D

If you enjoyed this post, make sure you subscribe to my RSS feed!

Tags: , , , , ,