Skip to content

Commit

Permalink
Added actual database-changing behavior to collection assigment for h…
Browse files Browse the repository at this point in the history
…as_many and has_and_belongs_to_many #1425 [Sebastian Kanthak]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1428 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
dhh committed Jun 16, 2005
1 parent 7b47f15 commit bfe6a75
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 4 deletions.
13 changes: 13 additions & 0 deletions activerecord/CHANGELOG
@@ -1,5 +1,18 @@
*SVN*

* Added actual database-changing behavior to collection assigment for has_many and has_and_belongs_to_many #1425 [Sebastian Kanthak].
Example:

david.projects = [Project.find(1), Project.new("name" => "ActionWebSearch")]
david.save

If david.projects already contain the project with ID 1, this is left unchanged. Any other projects are dropped. And the new
project is saved when david.save is called.

Also included is a way to do assignments through IDs, which is perfect for checkbox updating, so you get to do:

david.project_ids = [1, 5, 7]

* Corrected typo in find SQL for has_and_belongs_to_many. #1312 [ben@bensinclair.com]

* Added ActiveRecord::Recursion for guarding against recursive saves
Expand Down
18 changes: 17 additions & 1 deletion activerecord/lib/active_record/associations.rb
Expand Up @@ -200,6 +200,8 @@ module ClassMethods
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL.
# This will also destroy the objects if they're declared as belongs_to and dependent on this model.
# * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
# * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
# * <tt>collection.size</tt> - returns the number of associated objects.
Expand All @@ -215,6 +217,8 @@ module ClassMethods
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients=</tt>
# * <tt>Firm#client_ids=</tt>
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
Expand Down Expand Up @@ -463,6 +467,8 @@ def belongs_to(association_id, options = {})
# (collection.concat_with_attributes is an alias to this method).
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
# * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
# * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
# * <tt>collection.size</tt> - returns the number of associated objects.
Expand All @@ -474,6 +480,8 @@ def belongs_to(association_id, options = {})
# * <tt>Developer#projects<<</tt>
# * <tt>Developer#projects.push_with_attributes</tt>
# * <tt>Developer#projects.delete</tt>
# * <tt>Developer#projects=</tt>
# * <tt>Developer#project_ids=</tt>
# * <tt>Developer#projects.clear</tt>
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
Expand Down Expand Up @@ -634,6 +642,10 @@ def collection_accessor_methods(association_name, association_class_name, associ
association.replace(new_value)
association
end

define_method("#{Inflector.singularize(association_name)}_ids=") do |new_value|
send("#{association_name}=", association_class_name.constantize.find(new_value))
end
end

def require_association_class(class_name)
Expand Down Expand Up @@ -687,7 +699,11 @@ def association_constructor_method(constructor, association_name, association_cl
instance_variable_set("@#{association_name}", association)
end

association.send(constructor, attributees, replace_existing)
if association_proxy_class == HasOneAssociation
association.send(constructor, attributees, replace_existing)
else
association.send(constructor, attributees)
end
end
end

Expand Down
@@ -1,3 +1,5 @@
require 'set'

module ActiveRecord
module Associations
class AssociationCollection < AssociationProxy #:nodoc:
Expand Down Expand Up @@ -83,11 +85,19 @@ def uniq(collection = self)
collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
end

# Replace this collection with +other_array+
# This will perform a diff and delete/add only records that have changed.
def replace(other_array)
other_array.each{ |val| raise_on_type_mismatch(val) }
other_array.each { |val| raise_on_type_mismatch(val) }

load_target
other = other_array.size < 100 ? other_array : other_array.to_set
current = @target.size < 100 ? @target : @target.to_set

@target = other_array
@loaded = true
@owner.transaction do
delete(@target.select { |v| !other.include?(v) })
concat(other_array.select { |v| !current.include?(v) })
end
end

private
Expand Down
59 changes: 59 additions & 0 deletions activerecord/test/associations_test.rb
Expand Up @@ -596,6 +596,42 @@ def test_find_all_without_conditions
firm = companies(:first_firm)
assert_equal 2, firm.clients.find(:all).length
end

def test_replace_with_less
firm = Firm.find_first
firm.clients = [companies(:first_client)]
assert firm.save, "Could not save firm"
firm.reload
assert_equal 1, firm.clients.length
end

def test_replace_with_new
firm = Firm.find_first
new_client = Client.new("name" => "New Client")
firm.clients = [companies(:second_client),new_client]
firm.save
firm.reload
assert_equal 2, firm.clients.length
assert !firm.clients.include?(:first_client)
end

def test_replace_on_new_object
firm = Firm.new("name" => "New Firm")
firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
assert firm.save
firm.reload
assert_equal 2, firm.clients.length
assert firm.clients.include?(Client.find_by_name("New Client"))
end

def test_assign_ids
firm = Firm.new("name" => "Apple")
firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
firm.save
firm.reload
assert_equal 2, firm.clients.length
assert firm.clients.include?(companies(:second_client))
end
end

class BelongsToAssociationsTest < Test::Unit::TestCase
Expand Down Expand Up @@ -734,6 +770,7 @@ def xtest_counter_cache
apple.clients.to_s
assert_equal 1, apple.clients.size, "Should not use the cached number, but go to the database"
end

end


Expand Down Expand Up @@ -972,4 +1009,26 @@ def xtest_find_in_association_with_options
assert_equal developers(:david), projects(:active_record).developers.find(:first, :conditions => "salary < 10000")
assert_equal developers(:jamis), projects(:active_record).developers.find(:first, :order => "salary DESC")
end

def test_replace_with_less
david = developers(:david)
david.projects = [projects(:action_controller)]
assert david.save
assert_equal 1, david.projects.length
end

def test_replace_with_new
david = developers(:david)
david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
david.save
assert_equal 2, david.projects.length
assert !david.projects.include?(projects(:active_record))
end

def test_replace_on_new_object
new_developer = Developer.new("name" => "Matz")
new_developer.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
new_developer.save
assert_equal 2, new_developer.projects.length
end
end

0 comments on commit bfe6a75

Please sign in to comment.