Merge branch 'update_columns'

This commit is contained in:
Rafael Mendonça França 2012-07-24 20:20:48 -03:00
commit 864b49d42f
16 changed files with 212 additions and 63 deletions

View File

@ -1,5 +1,17 @@
## Rails 4.0.0 (unreleased) ##
* Deprecate `update_column` method in favor of `update_columns`.
*Rafael Mendonça França*
* 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 + Rafael Mendonça França*
* Removed `:finder_sql` and `:counter_sql` collection association options. Please
use scopes instead.

View File

@ -36,7 +36,7 @@ module ActiveRecord
when :destroy
target.destroy
when :nullify
target.update_column(reflection.foreign_key, nil)
target.update_columns(reflection.foreign_key => nil)
end
end
end

View File

@ -167,22 +167,6 @@ module ActiveRecord
became
end
# Updates a single attribute of an object, 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 the +name+
# attribute is marked as readonly.
def update_column(name, value)
name = name.to_s
verify_readonly_attribute(name)
raise ActiveRecordError, "can not update on a new record object" unless persisted?
raw_write_attribute(name, value)
self.class.where(self.class.primary_key => id).update_all(name => value) == 1
end
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
@ -211,6 +195,45 @@ module ActiveRecord
end
end
# Updates a single attribute of an object, 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 the +name+
# attribute is marked as readonly.
def update_column(name, value)
msg = "update_column is deprecated and will be removed in 4.1. Please use update_columns. " \
"E.g. update_columns(foo: 'bar')"
ActiveSupport::Deprecation.warn(msg)
update_columns(name => value)
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 do |key|
raise ActiveRecordError, "#{key.to_s} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s)
end
attributes.each do |k,v|
raw_write_attribute(k,v)
end
self.class.where(self.class.primary_key => id).update_all(attributes) == 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+.
@ -225,7 +248,7 @@ module ActiveRecord
# Saving is not subjected to validation checks. Returns +true+ if the
# record could be saved.
def increment!(attribute, by = 1)
increment(attribute, by).update_column(attribute, self[attribute])
increment(attribute, by).update_columns(attribute => self[attribute])
end
# Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
@ -242,7 +265,7 @@ module ActiveRecord
# Saving is not subjected to validation checks. Returns +true+ if the
# record could be saved.
def decrement!(attribute, by = 1)
decrement(attribute, by).update_column(attribute, self[attribute])
decrement(attribute, by).update_columns(attribute => self[attribute])
end
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
@ -259,7 +282,7 @@ module ActiveRecord
# Saving is not subjected to validation checks. Returns +true+ if the
# record could be saved.
def toggle!(attribute)
toggle(attribute).update_column(attribute, self[attribute])
toggle(attribute).update_columns(attribute => self[attribute])
end
# Reloads the attributes of this object from the database.

View File

@ -298,12 +298,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
def test_belongs_to_counter_when_update_column
def test_belongs_to_counter_when_update_columns
topic = Topic.create!(:title => "37s")
topic.replies.create!(:title => "re: 37s", :content => "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.update_column(:content, "rails is wonderfull")
topic.update_columns(content: "rails is wonderfull")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end

View File

@ -582,7 +582,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
project = SpecialProject.create("name" => "Special Project")
assert developer.save
developer.projects << project
developer.update_column("name", "Bruza")
developer.update_columns("name" => "Bruza")
assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i
SELECT count(*) FROM developers_projects
WHERE project_id = #{project.id}

View File

@ -634,7 +634,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
post.update_column(:taggings_with_delete_all_count, post.taggings_count)
post.update_columns(taggings_with_delete_all_count: post.taggings_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@ -643,7 +643,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
post.update_column(:taggings_with_destroy_count, post.taggings_count)
post.update_columns(taggings_with_destroy_count: post.taggings_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
@ -813,7 +813,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = Firm.first
# break the vanilla firm_id foreign key
assert_equal 2, firm.clients.count
firm.clients.first.update_column(:firm_id, nil)
firm.clients.first.update_columns(firm_id: nil)
assert_equal 1, firm.clients(true).count
assert_equal 1, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first

View File

@ -327,7 +327,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_update_counter_caches_on_delete_with_dependent_destroy
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
post.update_column(:tags_with_destroy_count, post.tags.count)
post.update_columns(tags_with_destroy_count: post.tags.count)
assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
@ -337,7 +337,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_update_counter_caches_on_delete_with_dependent_nullify
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
post.update_column(:tags_with_nullify_count, post.tags.count)
post.update_columns(tags_with_nullify_count: post.tags.count)
assert_no_difference 'post.reload.taggings_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do

View File

@ -90,12 +90,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_with_conditions_eager_loading
# conditions on the through table
assert_equal clubs(:moustache_club), Member.scoped(:includes => :favourite_club).find(@member.id).favourite_club
memberships(:membership_of_favourite_club).update_column(:favourite, false)
memberships(:membership_of_favourite_club).update_columns(favourite: false)
assert_equal nil, Member.scoped(:includes => :favourite_club).find(@member.id).reload.favourite_club
# conditions on the source table
assert_equal clubs(:moustache_club), Member.scoped(:includes => :hairy_club).find(@member.id).hairy_club
clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons")
clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons")
assert_equal nil, Member.scoped(:includes => :hairy_club).find(@member.id).reload.hairy_club
end

View File

@ -175,7 +175,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_many_with_delete_all
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDeleteAll'
posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyDeleteAll'
post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
old_count = Tagging.count
@ -186,7 +186,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_many_with_destroy
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDestroy'
posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyDestroy'
post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
old_count = Tagging.count
@ -197,7 +197,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_many_with_nullify
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyNullify'
posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyNullify'
post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
old_count = Tagging.count
@ -208,7 +208,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_one_with_destroy
assert posts(:welcome).tagging
posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneDestroy'
posts(:welcome).tagging.update_columns taggable_type: 'PostWithHasOneDestroy'
post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
old_count = Tagging.count
@ -219,7 +219,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_one_with_nullify
assert posts(:welcome).tagging
posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneNullify'
posts(:welcome).tagging.update_columns taggable_type: 'PostWithHasOneNullify'
post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
old_count = Tagging.count
@ -734,7 +734,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
Post.find(post_id).update_column :type, class_name
Post.find(post_id).update_columns type: class_name
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.table_name = 'posts'
klass.send(association, association_name, :as => :taggable, :dependent => dependency)

View File

@ -67,7 +67,7 @@ class AssociationsTest < ActiveRecord::TestCase
ship = Ship.create!(:name => "The good ship Dollypop")
part = ship.parts.create!(:name => "Mast")
part.mark_for_destruction
ShipPart.find(part.id).update_column(:name, 'Deck')
ShipPart.find(part.id).update_columns(name: 'Deck')
ship.parts.send(:load_target)
assert_equal 'Deck', ship.parts[0].name
end
@ -176,7 +176,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = developers(:david)
assert !david.projects.loaded?
david.update_column(:created_at, Time.now)
david.update_columns(created_at: Time.now)
assert !david.projects.loaded?
end

View File

@ -609,7 +609,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 'value', weird.send('a$b')
assert_equal 'value', weird.read_attribute('a$b')
weird.update_column('a$b', 'value2')
weird.update_columns('a$b' => 'value2')
weird.reload
assert_equal 'value2', weird.send('a$b')
assert_equal 'value2', weird.read_attribute('a$b')
@ -1896,7 +1896,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_cache_key_format_for_existing_record_with_nil_updated_at
dev = Developer.first
dev.update_column(:updated_at, nil)
dev.update_columns(updated_at: nil)
assert_match(/\/#{dev.id}$/, dev.cache_key)
end

View File

@ -331,8 +331,8 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_count_scoped_select_with_options
Account.update_all("credit_limit = NULL")
Account.last.update_column('credit_limit', 49)
Account.first.update_column('credit_limit', 51)
Account.last.update_columns('credit_limit' => 49)
Account.first.update_columns('credit_limit' => 51)
assert_equal 1, Account.scoped(:select => "credit_limit").where('credit_limit >= 50').count
end

View File

@ -424,7 +424,7 @@ class DirtyTest < ActiveRecord::TestCase
with_partial_updates(Topic) do
Topic.create!(:author_name => 'Bill', :content => {:a => "a"})
topic = Topic.select('id, author_name').first
topic.update_column :author_name, 'John'
topic.update_columns author_name: 'John'
topic = Topic.first
assert_not_nil topic.content
end

View File

@ -377,22 +377,21 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column
topic = Topic.find(1)
topic.update_column("approved", true)
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
topic.update_column("approved", true)
end
assert topic.approved?
topic.reload
assert topic.approved?
topic.update_column(:approved, false)
assert !topic.approved?
topic.reload
assert !topic.approved?
end
def test_update_column_should_not_use_setter_method
dev = Developer.find(1)
dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end }
dev.update_column(:salary, 80000)
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
dev.update_column(:salary, 80000)
end
assert_equal 80000, dev.salary
dev.reload
@ -401,19 +400,29 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_should_raise_exception_if_new_record
topic = Topic.new
assert_raises(ActiveRecord::ActiveRecordError) { topic.update_column("approved", false) }
assert_raises(ActiveRecord::ActiveRecordError) do
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
topic.update_column("approved", false)
end
end
end
def test_update_column_should_not_leave_the_object_dirty
topic = Topic.find(1)
topic.update_column("content", "Have a nice day")
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
topic.update_column("content", "Have a nice day")
end
topic.reload
topic.update_column(:content, "You too")
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
topic.update_column(:content, "You too")
end
assert_equal [], topic.changed
topic.reload
topic.update_column("content", "Have a nice day")
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
topic.update_column("content", "Have a nice day")
end
assert_equal [], topic.changed
end
@ -421,14 +430,20 @@ class PersistencesTest < ActiveRecord::TestCase
minivan = Minivan.find('m1')
new_name = 'sebavan'
minivan.update_column(:name, new_name)
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
minivan.update_column(:name, new_name)
end
assert_equal new_name, minivan.name
end
def test_update_column_for_readonly_attribute
minivan = Minivan.find('m1')
prev_color = minivan.color
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_column(:color, 'black') }
assert_raises(ActiveRecord::ActiveRecordError) do
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
minivan.update_column(:color, 'black')
end
end
assert_equal prev_color, minivan.color
end
@ -436,10 +451,14 @@ class PersistencesTest < ActiveRecord::TestCase
developer = Developer.find(1)
prev_month = Time.now.prev_month
developer.update_column(:updated_at, prev_month)
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
developer.update_column(:updated_at, prev_month)
end
assert_equal prev_month, developer.updated_at
developer.update_column(:salary, 80001)
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
developer.update_column(:salary, 80001)
end
assert_equal prev_month, developer.updated_at
developer.reload
@ -448,9 +467,102 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_with_one_changed_and_one_updated
t = Topic.order('id').limit(1).first
title, author_name = t.title, t.author_name
author_name = t.author_name
t.author_name = 'John'
t.update_column(:title, 'super_title')
assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
t.update_column(:title, 'super_title')
end
assert_equal 'John', t.author_name
assert_equal 'super_title', t.title
assert t.changed?, "topic should have changed"
assert t.author_name_changed?, "author_name should have changed"
t.reload
assert_equal author_name, t.author_name
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_not_use_setter_method
dev = Developer.find(1)
dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end }
dev.update_columns(salary: 80000)
assert_equal 80000, dev.salary
dev.reload
assert_equal 80000, dev.salary
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_model_having_primary_key_other_than_id
minivan = Minivan.find('m1')
new_name = 'sebavan'
minivan.update_columns(name: new_name)
assert_equal new_name, minivan.name
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_columns(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_columns_with_one_changed_and_one_updated
t = Topic.order('id').limit(1).first
author_name = t.author_name
t.author_name = 'John'
t.update_columns(title: 'super_title')
assert_equal 'John', t.author_name
assert_equal 'super_title', t.title
assert t.changed?, "topic should have changed"

View File

@ -11,7 +11,7 @@ class TimestampTest < ActiveRecord::TestCase
def setup
@developer = Developer.first
@developer.update_column(:updated_at, Time.now.prev_month)
@developer.update_columns(updated_at: Time.now.prev_month)
@previously_updated_at = @developer.updated_at
end
@ -133,7 +133,7 @@ class TimestampTest < ActiveRecord::TestCase
pet = Pet.first
owner = pet.owner
owner.update_column(:happy_at, 3.days.ago)
owner.update_columns(happy_at: 3.days.ago)
previously_owner_updated_at = owner.updated_at
pet.name = "I'm a parrot"
@ -153,7 +153,7 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
time = 3.days.ago
owner.update_column(:updated_at, time)
owner.update_columns(updated_at: time)
toy.touch
owner.reload

View File

@ -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+