Skip to content
This repository
Browse code

Added new #update_column method.

Signed-off-by: Santiago Pastorino <santiago@wyeworks.com>
  • Loading branch information...
commit 245542ea2994961731be105db6c076256a22a7a9 1 parent e6a8a3a
Sebastian Martinez authored March 27, 2011 spastorino committed March 27, 2011
9  activerecord/CHANGELOG
... ...
@@ -1,5 +1,14 @@
1 1
 *Rails 3.1.0 (unreleased)*
2 2
 
  3
+* Added an update_column method on ActiveRecord. This new method updates a given attribute on an object, skipping validations and callbacks.
  4
+  It is recommended to use #update_attribute unless you are sure you do not want to execute any callback, including the modification of
  5
+  the updated_at column. It should not be called on new records.
  6
+  Example:
  7
+
  8
+    User.first.update_column(:name, "sebastian")         # => true
  9
+
  10
+  [Sebastian Martinez]
  11
+
3 12
 * Associations with a :through option can now use *any* association as the
4 13
   through or source association, including other associations which have a
5 14
   :through option and has_and_belongs_to_many associations
1  activerecord/lib/active_record/attribute_methods/write.rb
@@ -32,6 +32,7 @@ def write_attribute(attr_name, value)
32 32
           @attributes[attr_name] = value
33 33
         end
34 34
       end
  35
+      alias_method :raw_write_attribute, :write_attribute
35 36
 
36 37
       private
37 38
         # Handle *= for method_missing.
14  activerecord/lib/active_record/persistence.rb
@@ -119,6 +119,20 @@ def update_attribute(name, value)
119 119
       save(:validate => false)
120 120
     end
121 121
 
  122
+    # Updates a single attribute of an object, without calling save.
  123
+    #
  124
+    # * Validation is skipped.
  125
+    # * Callbacks are skipped.
  126
+    # * updated_at/updated_on column is not updated if that column is available.
  127
+    #
  128
+    def update_column(name, value)
  129
+      name = name.to_s
  130
+      raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
  131
+      raise ActiveRecordError, "can not update on a new record object" unless persisted?
  132
+      raw_write_attribute(name, value)
  133
+      self.class.update_all({ name => value }, self.class.primary_key => id) == 1
  134
+    end
  135
+
122 136
     # Updates the attributes of the model from the passed-in hash and saves the
123 137
     # record, all wrapped in a transaction. If the object is invalid, the saving
124 138
     # will fail and false will be returned.
2  activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -604,7 +604,7 @@ def test_update_attributes_after_push_without_duplicate_join_table_rows
604 604
     project = SpecialProject.create("name" => "Special Project")
605 605
     assert developer.save
606 606
     developer.projects << project
607  
-    developer.update_attribute("name", "Bruza")
  607
+    developer.update_column("name", "Bruza")
608 608
     assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i
609 609
       SELECT count(*) FROM developers_projects
610 610
       WHERE project_id = #{project.id}
6  activerecord/test/cases/associations/has_many_associations_test.rb
@@ -639,7 +639,7 @@ def test_deleting_updates_counter_cache_without_dependent_option
639 639
 
640 640
   def test_deleting_updates_counter_cache_with_dependent_delete_all
641 641
     post = posts(:welcome)
642  
-    post.update_attribute(:taggings_with_delete_all_count, post.taggings_count)
  642
+    post.update_column(:taggings_with_delete_all_count, post.taggings_count)
643 643
 
644 644
     assert_difference "post.reload.taggings_with_delete_all_count", -1 do
645 645
       post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -648,7 +648,7 @@ def test_deleting_updates_counter_cache_with_dependent_delete_all
648 648
 
649 649
   def test_deleting_updates_counter_cache_with_dependent_destroy
650 650
     post = posts(:welcome)
651  
-    post.update_attribute(:taggings_with_destroy_count, post.taggings_count)
  651
+    post.update_column(:taggings_with_destroy_count, post.taggings_count)
652 652
 
653 653
     assert_difference "post.reload.taggings_with_destroy_count", -1 do
654 654
       post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
@@ -787,7 +787,7 @@ def test_delete_all_association_with_primary_key_deletes_correct_records
787 787
     firm = Firm.find(:first)
788 788
     # break the vanilla firm_id foreign key
789 789
     assert_equal 2, firm.clients.count
790  
-    firm.clients.first.update_attribute(:firm_id, nil)
  790
+    firm.clients.first.update_column(:firm_id, nil)
791 791
     assert_equal 1, firm.clients(true).count
792 792
     assert_equal 1, firm.clients_using_primary_key_with_delete_all.count
793 793
     old_record = firm.clients_using_primary_key_with_delete_all.first
4  activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -286,7 +286,7 @@ def test_update_counter_caches_on_delete
286 286
   def test_update_counter_caches_on_delete_with_dependent_destroy
287 287
     post = posts(:welcome)
288 288
     tag  = post.tags.create!(:name => 'doomed')
289  
-    post.update_attribute(:tags_with_destroy_count, post.tags.count)
  289
+    post.update_column(:tags_with_destroy_count, post.tags.count)
290 290
 
291 291
     assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
292 292
       posts(:welcome).tags_with_destroy.delete(tag)
@@ -296,7 +296,7 @@ def test_update_counter_caches_on_delete_with_dependent_destroy
296 296
   def test_update_counter_caches_on_delete_with_dependent_nullify
297 297
     post = posts(:welcome)
298 298
     tag  = post.tags.create!(:name => 'doomed')
299  
-    post.update_attribute(:tags_with_nullify_count, post.tags.count)
  299
+    post.update_column(:tags_with_nullify_count, post.tags.count)
300 300
 
301 301
     assert_no_difference 'post.reload.taggings_count' do
302 302
       assert_difference 'post.reload.tags_with_nullify_count', -1 do
4  activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -90,12 +90,12 @@ def test_has_one_through_eager_loading_through_polymorphic
90 90
   def test_has_one_through_with_conditions_eager_loading
91 91
     # conditions on the through table
92 92
     assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :favourite_club).favourite_club
93  
-    memberships(:membership_of_favourite_club).update_attribute(:favourite, false)
  93
+    memberships(:membership_of_favourite_club).update_column(:favourite, false)
94 94
     assert_equal nil,                    Member.find(@member.id, :include => :favourite_club).reload.favourite_club
95 95
 
96 96
     # conditions on the source table
97 97
     assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :hairy_club).hairy_club
98  
-    clubs(:moustache_club).update_attribute(:name, "Association of Clean-Shaven Persons")
  98
+    clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons")
99 99
     assert_equal nil,                    Member.find(@member.id, :include => :hairy_club).reload.hairy_club
100 100
   end
101 101
 
12  activerecord/test/cases/associations/join_model_test.rb
@@ -161,7 +161,7 @@ def test_create_polymorphic_has_one_with_scope
161 161
 
162 162
   def test_delete_polymorphic_has_many_with_delete_all
163 163
     assert_equal 1, posts(:welcome).taggings.count
164  
-    posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll'
  164
+    posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDeleteAll'
165 165
     post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
166 166
 
167 167
     old_count = Tagging.count
@@ -172,7 +172,7 @@ def test_delete_polymorphic_has_many_with_delete_all
172 172
 
173 173
   def test_delete_polymorphic_has_many_with_destroy
174 174
     assert_equal 1, posts(:welcome).taggings.count
175  
-    posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy'
  175
+    posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDestroy'
176 176
     post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
177 177
 
178 178
     old_count = Tagging.count
@@ -183,7 +183,7 @@ def test_delete_polymorphic_has_many_with_destroy
183 183
 
184 184
   def test_delete_polymorphic_has_many_with_nullify
185 185
     assert_equal 1, posts(:welcome).taggings.count
186  
-    posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify'
  186
+    posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyNullify'
187 187
     post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
188 188
 
189 189
     old_count = Tagging.count
@@ -194,7 +194,7 @@ def test_delete_polymorphic_has_many_with_nullify
194 194
 
195 195
   def test_delete_polymorphic_has_one_with_destroy
196 196
     assert posts(:welcome).tagging
197  
-    posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy'
  197
+    posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneDestroy'
198 198
     post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
199 199
 
200 200
     old_count = Tagging.count
@@ -205,7 +205,7 @@ def test_delete_polymorphic_has_one_with_destroy
205 205
 
206 206
   def test_delete_polymorphic_has_one_with_nullify
207 207
     assert posts(:welcome).tagging
208  
-    posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify'
  208
+    posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneNullify'
209 209
     post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
210 210
 
211 211
     old_count = Tagging.count
@@ -707,7 +707,7 @@ def test_has_many_through_goes_through_all_sti_classes
707 707
     # create dynamic Post models to allow different dependency options
708 708
     def find_post_with_dependency(post_id, association, association_name, dependency)
709 709
       class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
710  
-      Post.find(post_id).update_attribute :type, class_name
  710
+      Post.find(post_id).update_column :type, class_name
711 711
       klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
712 712
       klass.set_table_name 'posts'
713 713
       klass.send(association, association_name, :as => :taggable, :dependent => dependency)
4  activerecord/test/cases/associations_test.rb
@@ -66,7 +66,7 @@ def test_loading_the_association_target_should_load_most_recent_attributes_for_c
66 66
     ship = Ship.create!(:name => "The good ship Dollypop")
67 67
     part = ship.parts.create!(:name => "Mast")
68 68
     part.mark_for_destruction
69  
-    ShipPart.find(part.id).update_attribute(:name, 'Deck')
  69
+    ShipPart.find(part.id).update_column(:name, 'Deck')
70 70
     ship.parts.send(:load_target)
71 71
     assert_equal 'Deck', ship.parts[0].name
72 72
   end
@@ -170,7 +170,7 @@ def test_save_on_parent_does_not_load_target
170 170
     david = developers(:david)
171 171
 
172 172
     assert !david.projects.loaded?
173  
-    david.update_attribute(:created_at, Time.now)
  173
+    david.update_column(:created_at, Time.now)
174 174
     assert !david.projects.loaded?
175 175
   end
176 176
 
2  activerecord/test/cases/base_test.rb
@@ -484,7 +484,7 @@ def test_non_valid_identifier_column_name
484 484
     weird.reload
485 485
     assert_equal 'value', weird.send('a$b')
486 486
 
487  
-    weird.update_attribute('a$b', 'value2')
  487
+    weird.update_column('a$b', 'value2')
488 488
     weird.reload
489 489
     assert_equal 'value2', weird.send('a$b')
490 490
   end
4  activerecord/test/cases/calculations_test.rb
@@ -311,8 +311,8 @@ def test_should_count_scoped_select
311 311
 
312 312
   def test_should_count_scoped_select_with_options
313 313
     Account.update_all("credit_limit = NULL")
314  
-    Account.last.update_attribute('credit_limit', 49)
315  
-    Account.first.update_attribute('credit_limit', 51)
  314
+    Account.last.update_column('credit_limit', 49)
  315
+    Account.first.update_column('credit_limit', 51)
316 316
 
317 317
     assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50'])
318 318
   end
2  activerecord/test/cases/dirty_test.rb
@@ -413,7 +413,7 @@ def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_p
413 413
     with_partial_updates(Topic) do
414 414
       Topic.create!(:author_name => 'Bill', :content => {:a => "a"})
415 415
       topic = Topic.select('id, author_name').first
416  
-      topic.update_attribute :author_name, 'John'
  416
+      topic.update_column :author_name, 'John'
417 417
       topic = Topic.first
418 418
       assert_not_nil topic.content
419 419
     end
86  activerecord/test/cases/persistence_test.rb
@@ -389,6 +389,92 @@ def test_update_attribute_for_updated_at_on
389 389
     assert_not_equal prev_month, developer.updated_at
390 390
   end
391 391
 
  392
+  def test_update_column
  393
+    topic = Topic.find(1)
  394
+    topic.update_column("approved", true)
  395
+    assert topic.approved?
  396
+    topic.reload
  397
+    assert topic.approved?
  398
+
  399
+    topic.update_column(:approved, false)
  400
+    assert !topic.approved?
  401
+    topic.reload
  402
+    assert !topic.approved?
  403
+  end
  404
+
  405
+  def test_update_column_should_not_use_setter_method
  406
+    dev = Developer.find(1)
  407
+    dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end }
  408
+
  409
+    dev.update_column(:salary, 80000)
  410
+    assert_equal 80000, dev.salary
  411
+
  412
+    dev.reload
  413
+    assert_equal 80000, dev.salary
  414
+  end
  415
+
  416
+  def test_update_column_should_raise_exception_if_new_record
  417
+    topic = Topic.new
  418
+    assert_raises(ActiveRecord::ActiveRecordError) { topic.update_column("approved", false) }
  419
+  end
  420
+
  421
+  def test_update_column_should_not_leave_the_object_dirty
  422
+    topic = Topic.find(1)
  423
+    topic.update_attribute("content", "Have a nice day")
  424
+
  425
+    topic.reload
  426
+    topic.update_column(:content, "You too")
  427
+    assert_equal [], topic.changed
  428
+
  429
+    topic.reload
  430
+    topic.update_column("content", "Have a nice day")
  431
+    assert_equal [], topic.changed
  432
+  end
  433
+
  434
+  def test_update_column_with_model_having_primary_key_other_than_id
  435
+    minivan = Minivan.find('m1')
  436
+    new_name = 'sebavan'
  437
+
  438
+    minivan.update_column(:name, new_name)
  439
+    assert_equal new_name, minivan.name
  440
+  end
  441
+
  442
+  def test_update_column_for_readonly_attribute
  443
+    minivan = Minivan.find('m1')
  444
+    prev_color = minivan.color
  445
+    assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_column(:color, 'black') }
  446
+    assert_equal prev_color, minivan.color
  447
+  end
  448
+
  449
+  def test_update_column_should_not_modify_updated_at
  450
+    developer = Developer.find(1)
  451
+    prev_month = Time.now.prev_month
  452
+
  453
+    developer.update_column(:updated_at, prev_month)
  454
+    assert_equal prev_month, developer.updated_at
  455
+
  456
+    developer.update_column(:salary, 80001)
  457
+    assert_equal prev_month, developer.updated_at
  458
+
  459
+    developer.reload
  460
+    assert_equal prev_month, developer.updated_at
  461
+  end
  462
+
  463
+  def test_update_column_with_one_changed_and_one_updated
  464
+    t = Topic.order('id').limit(1).first
  465
+    title, author_name = t.title, t.author_name
  466
+    t.author_name = 'John'
  467
+    t.update_column(:title, 'super_title')
  468
+    assert_equal 'John', t.author_name
  469
+    assert_equal 'super_title', t.title
  470
+    assert t.changed?, "topic should have changed"
  471
+    assert t.author_name_changed?, "author_name should have changed"
  472
+
  473
+    t.reload
  474
+    assert_equal author_name, t.author_name
  475
+    assert_equal 'super_title', t.title
  476
+  end
  477
+
392 478
   def test_update_attributes
393 479
     topic = Topic.find(1)
394 480
     assert !topic.approved?
3  activerecord/test/cases/timestamp_test.rb
@@ -131,8 +131,9 @@ def test_touching_a_record_touches_parent_record_and_grandparent_record
131 131
     toy = Toy.first
132 132
     pet = toy.pet
133 133
     owner = pet.owner
  134
+    time = 3.days.ago
134 135
 
135  
-    owner.update_attribute(:updated_at, (time = 3.days.ago))
  136
+    owner.update_column(:updated_at, time)
136 137
     toy.touch
137 138
     owner.reload
138 139
 

4 notes on commit 245542e

Neeraj Singh
Collaborator

It took a long time but it is finally here. :-)

Andy Lindeman

Good stuff!

Joost Baaij

In other words, update_attribute_with_validation_skipping is back?

Michael Grosser

how about update_columns, which update_column could use ?

Please sign in to comment.
Something went wrong with that request. Please try again.