Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

New #update_columns method. #1190

Closed
wants to merge 3 commits into from

9 participants

@smartinez87

Some time ago I pushed the #update_column method, and it can be seen here 245542e#commitcomment-384953 how someone asked for #update_columns, which to me makes sense for completeness and consistency.

So here it is :)

@pixeltrix
Owner

What about mass-assignment security?

@smartinez87

I don't see this method being used say on the controller passing the params hash, where you definitely need mass-assignment security. It is not intended to be a replacement of #update_attributes.
I see it being used say in your logic where you need to set a couple of attributes, without even executing callbacks or validations.

activerecord/lib/active_record/persistence.rb
@@ -169,6 +169,23 @@ module ActiveRecord
end
end
+ # Updates the attributes from the passed-in hash, without calling save.
+ #
+ # * Validation is skipped.
+ # * Callbacks are skipped.
+ # * +updated_at+/+updated_on+ column is not updated if that column is available.
+ #
+ # Raises an +ActiveRecordError+ when called on new objects, or when at least
+ # one if the attributes is marked as readonly.
@dasch
dasch added a note

You probably mean "one of", not "one if".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@TheEmpty

I agree, if it's bypassing validations and callbacks, it should by pass everything. Yeah this is pretty nice, but update_attribute doesn't activate callbacks or validations either does it? Because I've used that several times to bypass validations and what not; so either I'm crazy or this would do the same thing?

Also I'm reading a nice testing book now, is Topic a stub or is that really coming from the database? I would imagine with Rails those kind of things can make the tests much faster.

@smartinez87

you're right @dasch, thanks

@henrikhodne

I think this should probably bypass everything too.

@rafaelfranca

This pull request can't be automatically merge.

If you still want to make this one part of the Rails please rebase it.

@smartinez87

@rafaelfranca just rebased it. Also added the entry to the CHANGELOG for it.
Thanks!

@tenderlove
Owner

What is the difference between this and assign_attributes?

@smartinez87

The main difference I see is that update_columns impacts the database.
Note that this pull request was made to have completeness with #update_column, that people was asking for when it was merged.

@rgarver

This is a good addition since there is not an easy alternative except multiple update_column calls.

@tenderlove
Owner

Could this be implemented as:

def update_columns(attributes)
  assign_attributes(attributes)
  save
end
@smartinez87

@tenderlove Wouldn't calling save execute callbacks and validations?
perhaps something like this:

def update_columns(attributes)
  raise ActiveRecordError, "can not update on a new record object" unless persisted?
  attributes.each_key {|key| raise ActiveRecordError, "#{key.to_s} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) }
  assign_attributes(attributes)
  self.class.update_all(attributes, self.class.primary_key => id) == 1
end
@sobrinho

Any news about this pull request?

Currently we needed that on a migration and workarounded using this:

ResourceAnnul.find_each do |annul|
  # annul.update_column :resource_id, annul.price_collection_proposal_id
  # annul.update_column :resource_type, 'PriceCollectionProposal'

  ResourceAnnul.update_all( {:resource_id => annul.price_collection_proposal_id, :resource_type => 'PriceCollectionProposal'}, {:id => annul.id} )
end

I think the intention of update_columns is update without hitting validations and/or callbacks.

Something like:

  • update_attributes - hit validations, hit callbacks, respect mass assignment
  • update_attribute - ignore validations, hit callbacks, ignore mass assignment
  • update_columns - ignore validations, ignore callbacks, ignore mass assignment
  • update_column - ignore validations, ignore callbacks, ignore mass assignment

But the difference is if you need to update more than one attribute and don't want to querying one time per attribute like happened on my migration.

What you think, @tenderlove ?

@rafaelfranca

I don't see the value to still have the update_column since we are adding a method that can be used to one or more fields.

That said I proposing the following path:

  • update_attributes - hit validations, hit callbacks, respect mass assignment
  • update_columns - ignore validations, ignore callbacks, ignore mass assignment

update_attribute was removed in 4.0 (#6738) and deprecated in 3-2-stable (#6739)
update_column will be deprecated in 4.0 and remove in the next major release.

I'll work on this.

@rafaelfranca

Done @ 864b49d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
8 activerecord/CHANGELOG.md
@@ -1,5 +1,13 @@
## Rails 4.0.0 (unreleased) ##
+* Added an update_columns method. This new method updates the given attributes on an object,
+ without calling save, hence skipping validations and callbacks.
+ Example:
+
+ User.first.update_columns({:name => "sebastian", :age => 25}) # => true
+
+ *Sebastian Martinez*
+
* Added an :index option to automatically create indexes for references
and belongs_to statements in migrations.
View
17 activerecord/lib/active_record/persistence.rb
@@ -209,6 +209,23 @@ def update_attributes!(attributes, options = {})
end
end
+ # Updates the attributes from the passed-in hash, without calling save.
+ #
+ # * Validation is skipped.
+ # * Callbacks are skipped.
+ # * +updated_at+/+updated_on+ column is not updated if that column is available.
+ #
+ # Raises an +ActiveRecordError+ when called on new objects, or when at least
+ # one of the attributes is marked as readonly.
+ def update_columns(attributes)
+ raise ActiveRecordError, "can not update on a new record object" unless persisted?
+ attributes.each_key {|key| raise ActiveRecordError, "#{key.to_s} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) }
+ attributes.each do |k,v|
+ raw_write_attribute(k,v)
+ end
+ self.class.update_all(attributes, self.class.primary_key => id) == 1
+ end
+
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
# The increment is performed directly on the underlying attribute, no setter is invoked.
# Only makes sense for number-based attributes. Returns +self+.
View
57 activerecord/test/cases/persistence_test.rb
@@ -516,6 +516,63 @@ def test_update_column_with_one_changed_and_one_updated
assert_equal 'super_title', t.title
end
+ def test_update_columns
+ topic = Topic.find(1)
+ topic.update_columns({ "approved" => true, :title => "Sebastian Topic" })
+ assert topic.approved?
+ assert_equal "Sebastian Topic", topic.title
+ topic.reload
+ assert topic.approved?
+ assert_equal "Sebastian Topic", topic.title
+ end
+
+ def test_update_columns_should_raise_exception_if_new_record
+ topic = Topic.new
+ assert_raises(ActiveRecord::ActiveRecordError) { topic.update_columns({ :approved => false }) }
+ end
+
+ def test_update_columns_should_not_leave_the_object_dirty
+ topic = Topic.find(1)
+ topic.update_attributes({ "content" => "Have a nice day", :author_name => "Jose" })
+
+ topic.reload
+ topic.update_columns({ :content => "You too", "author_name" => "Sebastian" })
+ assert_equal [], topic.changed
+
+ topic.reload
+ topic.update_columns({ :content => "Have a nice day", :author_name => "Jose" })
+ assert_equal [], topic.changed
+ end
+
+ def test_update_columns_with_one_readonly_attribute
+ minivan = Minivan.find('m1')
+ prev_color = minivan.color
+ prev_name = minivan.name
+ assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_columns({ :name => "My old minivan", :color => 'black' }) }
+ assert_equal prev_color, minivan.color
+ assert_equal prev_name, minivan.name
+
+ minivan.reload
+ assert_equal prev_color, minivan.color
+ assert_equal prev_name, minivan.name
+ end
+
+ def test_update_columns_should_not_modify_updated_at
+ developer = Developer.find(1)
+ prev_month = Time.now.prev_month
+
+ developer.update_column(:updated_at, prev_month)
+ assert_equal prev_month, developer.updated_at
+
+ developer.update_columns({ :salary, 80000 })
+ assert_equal prev_month, developer.updated_at
+ assert_equal 80000, developer.salary
+
+ developer.reload
+ assert_equal prev_month.to_i, developer.updated_at.to_i
+ assert_equal 80000, developer.salary
+ end
+
def test_update_attributes
topic = Topic.find(1)
assert !topic.approved?
View
2  guides/source/active_record_validations_callbacks.textile
@@ -86,6 +86,7 @@ The following methods skip validations, and will save the object to the database
* +update_all+
* +update_attribute+
* +update_column+
+* +update_columns+
* +update_counters+
Note that +save+ also has the ability to skip validations if passed +:validate => false+ as argument. This technique should be used with caution.
@@ -1082,6 +1083,7 @@ Just as with validations, it is also possible to skip callbacks. These methods s
* +toggle+
* +touch+
* +update_column+
+* +update_columns+
* +update_all+
* +update_counters+
Something went wrong with that request. Please try again.