From efa81dad5158eb61243d00811e4664248dd2c167 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 25 Jan 2005 12:45:01 +0000 Subject: Added the option of supplying an array of ids and attributes to Base#update, so that multiple records can be updated at once (inspired by #526/Duane Johnson). Added the option of supplying an array of attributes to Base#create, so that multiple records can be created at once. Added that Base#delete and Base#destroy both can take an array of ids to delete/destroy #336. Added that has_many association build and create methods can take arrays of record data like Base#create and Base#build to build/create multiple records at once. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@504 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 11 +++- .../associations/association_collection.rb | 10 ++- .../associations/has_many_association.rb | 14 +++-- activerecord/lib/active_record/base.rb | 71 ++++++---------------- activerecord/test/associations_test.rb | 13 ++++ activerecord/test/base_test.rb | 36 ++++++----- 6 files changed, 81 insertions(+), 74 deletions(-) diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index edd278e42b..3b9e273c91 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,6 +1,15 @@ *SVN* -* Added Base.update_collection that can update an array of id/attribute pairs, such as the ones produced by the recent added support for automatic id-based indexing for lists of items #526 [Duane Johnson] +* Added that has_many association build and create methods can take arrays of record data like Base#create and Base#build to build/create multiple records at once. + +* Added that Base#delete and Base#destroy both can take an array of ids to delete/destroy #336 + +* Added the option of supplying an array of attributes to Base#create, so that multiple records can be created at once. + +* Added the option of supplying an array of ids and attributes to Base#update, so that multiple records can be updated at once (inspired by #526/Duane Johnson). Example + + people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } + Person.update(people.keys, people.values) * Added ActiveRecord::Base.timestamps_gmt that can be set to true to make the automated timestamping use GMT instead of local time #520 [Scott Baron] diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 9507822f40..cf1d0ecefc 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -53,9 +53,13 @@ module ActiveRecord def create(attributes = {}) # Can't use Base.create since the foreign key may be a protected attribute. - record = build(attributes) - record.save unless @owner.new_record? - record + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr) } + else + record = build(attributes) + record.save unless @owner.new_record? + record + end end # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index f3ab6bcdab..0d72b9b0c5 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -9,11 +9,15 @@ module ActiveRecord end def build(attributes = {}) - load_target - record = @association_class.new(attributes) - record[@association_class_primary_key_name] = @owner.id unless @owner.new_record? - @target << record - record + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr) } + else + load_target + record = @association_class.new(attributes) + record[@association_class_primary_key_name] = @owner.id unless @owner.new_record? + @target << record + record + end end def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cb13f78f7d..b0ad90b10b 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -340,28 +340,38 @@ module ActiveRecord #:nodoc: # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save # fail under validations, the unsaved object is still returned. def create(attributes = nil) - object = new(attributes) - object.save - object + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr) } + else + object = new(attributes) + object.save + object + end end # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it), # and returns it. If the save fail under validations, the unsaved object is still returned. def update(id, attributes) - object = find(id) - object.attributes = attributes - object.save - object + if id.is_a?(Array) + idx = -1 + id.collect { |id| idx += 1; update(id, attributes[idx]) } + else + object = find(id) + object.update_attributes(attributes) + object + end end - # Deletes the record with the given +id+ without instantiating an object first. + # Deletes the record with the given +id+ without instantiating an object first. If an array of ids is provided, all of them + # are deleted. def delete(id) - delete_all([ "#{primary_key} = ?", id ]) + delete_all([ "#{primary_key} IN (?)", id ]) end # Destroys the record with the given +id+ by instantiating the object and calling #destroy (all the callbacks are the triggered). + # If an array of ids is provided, all of them are destroyed. def destroy(id) - find(id).destroy + id.is_a?(Array) ? id.each { |id| destroy(id) } : find(id).destroy end # Updates all records with the SET-part of an SQL update statement in +updates+ and returns an integer with the number of rows updates. @@ -373,47 +383,6 @@ module ActiveRecord #:nodoc: return connection.update(sql, "#{name} Update") end - # Updates several records at a time using the pattern of a hash that contains id => {attributes} pairs as contained in +id_and_attributes_pairs+. - # If there are certain conditions that must be met in order for the update to occur, an optional block - # containing a conditional statement may be used. Example: - # Person.update_collection { 23 => { "first_name" => "John", "last_name" => "Peterson" }, - # 25 => { "first_name" => "Duane", "last_name" => "Johnson" } } - # - # # Updates only those records whose first name begins with 'duane' or 'Duane' - # Person.update_collection @params['people'] do |activerecord_object, new_attributes| - # activerecord_object.first_name =~ /^[dD]uane.*/ - # end - # - # The conditional block may also be of use when you have more than one kind of key in the +id_and_attributes_pairs+ hash. - # For example, if you have a view that contains form elements of both existing and new records, you might end up with - # a hash that looks like this: - # @params['people'] = { "1" => { "first_name" => "Bob", "last_name" => "Schilling" }, - # "2" => { "first_name" => "Joe", "last_name" => "Tycoon" }, - # "new_person" => { "first_name" => "Mary", "last_name" => "Smith" } } - # In such a case, you could discriminate against 'updating' the new_person record with the following code: - # Person.update_collection(@params['people']) { |ar, attrs| ar.id.to_i > 0 } - # - # This works because the to_i method converts all non-integer strings to 0. - def update_collection(id_and_attributes_pairs) - updated_records = Array.new - - transaction do - records = find(id_and_attributes_pairs.keys) - id_and_attributes_pairs.each do |id, attrs| - record = records.select { |r| r.id.to_s == id }.first - - # Update each record unless the closure is false - if (!block_given? || (block_given? && yield(record, attrs))) - record.update_attributes(attrs) - updated_records << record - end - end - end - - return updated_records - end - - # Destroys the objects for all the records that matches the +condition+ by instantiating each object and calling # the destroy method. Example: # Person.destroy_all "last_login < '2004-04-04'" diff --git a/activerecord/test/associations_test.rb b/activerecord/test/associations_test.rb index 7ede042879..2095f70cd3 100755 --- a/activerecord/test/associations_test.rb +++ b/activerecord/test/associations_test.rb @@ -386,6 +386,14 @@ class HasManyAssociationsTest < Test::Unit::TestCase assert_equal 2, @signals37.clients_of_firm(true).size end + def test_build_many + new_clients = @signals37.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) + assert_equal 2, new_clients.size + + assert @signals37.save + assert_equal 3, @signals37.clients_of_firm(true).size + end + def test_invalid_build new_client = @signals37.clients_of_firm.build assert new_client.new_record? @@ -403,6 +411,11 @@ class HasManyAssociationsTest < Test::Unit::TestCase assert_equal new_client, @signals37.clients_of_firm.last assert_equal new_client, @signals37.clients_of_firm(true).last end + + def test_create_many + @signals37.clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) + assert_equal 3, @signals37.clients_of_firm(true).size + end def test_deleting force_signal37_to_load_all_clients_of_firm diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb index 2119785caf..0f93f2ffbc 100755 --- a/activerecord/test/base_test.rb +++ b/activerecord/test/base_test.rb @@ -114,6 +114,12 @@ class BasicsTest < Test::Unit::TestCase topicReloaded = Topic.find(id) assert_equal("New Topic", topicReloaded.title) end + + def test_create_many + topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) + assert_equal 2, topics.size + assert_equal "first", topics.first.title + end def test_create_columns_not_equal_attributes topic = Topic.new @@ -256,12 +262,22 @@ class BasicsTest < Test::Unit::TestCase end def test_destroy_all - assert_equal(2, Topic.find_all.size) + assert_equal 2, Topic.find_all.size Topic.destroy_all "author_name = 'Mary'" - assert_equal(1, Topic.find_all.size) + assert_equal 1, Topic.find_all.size end - + + def test_destroy_many + Client.destroy([2, 3]) + assert_equal 0, Client.count + end + + def test_delete_many + Topic.delete([1, 2]) + assert_equal 0, Topic.count + end + def test_boolean_attributes assert ! Topic.find(1).approved? assert Topic.find(2).approved? @@ -292,21 +308,13 @@ class BasicsTest < Test::Unit::TestCase assert_equal "bulk updated again!", Topic.find(2).content end - def test_update_collection - ids_and_attributes = { "1" => { "content" => "1 updated" }, "2" => { "content" => "2 updated" } } - updated = Topic.update_collection(ids_and_attributes) + def test_update_many + topic_data = { "1" => { "content" => "1 updated" }, "2" => { "content" => "2 updated" } } + updated = Topic.update(topic_data.keys, topic_data.values) assert_equal 2, updated.size assert_equal "1 updated", Topic.find(1).content assert_equal "2 updated", Topic.find(2).content - - ids_and_attributes["1"]["content"] = "one updated" - ids_and_attributes["2"]["content"] = "two updated" - updated = Topic.update_collection(ids_and_attributes) { |ar, attrs| ar.id == 1 } - - assert_equal 1, updated.size - assert_equal "one updated", Topic.find(1).content - assert_equal "2 updated", Topic.find(2).content end def test_delete_all -- cgit v1.2.3