Allow building and then later saving has_many :through records, such that the join record is automatically saved too. This requires the :inverse_of option to be set on the source association in the join model. See the CHANGELOG for details. [#4329 state:resolved]
This commit is contained in:
parent
f0b9805029
commit
91fd651056
@ -1,5 +1,29 @@
|
||||
*Rails 3.1.0 (unreleased)*
|
||||
|
||||
* Make has_many :through associations work correctly when you build a record and then save it. This
|
||||
requires you to set the :inverse_of option on the source reflection on the join model, like so:
|
||||
|
||||
class Post < ActiveRecord::Base
|
||||
has_many :taggings
|
||||
has_many :tags, :through => :taggings
|
||||
end
|
||||
|
||||
class Tagging < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
belongs_to :tag, :inverse_of => :tagging # :inverse_of must be set!
|
||||
end
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
has_many :taggings
|
||||
has_many :posts, :through => :taggings
|
||||
end
|
||||
|
||||
post = Post.first
|
||||
tag = post.tags.build :name => "ruby"
|
||||
tag.save # will save a Taggable linking to the post
|
||||
|
||||
[Jon Leighton]
|
||||
|
||||
* Support the :dependent option on has_many :through associations. For historical and practical
|
||||
reasons, :delete_all is the default deletion strategy employed by association.delete(*records),
|
||||
despite the fact that the default strategy is :nullify for regular has_many. Also, this only
|
||||
|
@ -523,6 +523,22 @@ def association_instance_set(name, association)
|
||||
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
|
||||
# @group.avatars.delete(@group.avatars.last) # so would this
|
||||
#
|
||||
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
|
||||
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
|
||||
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
|
||||
#
|
||||
# @post = Post.first
|
||||
# @tag = @post.tags.build :name => "ruby"
|
||||
# @tag.save
|
||||
#
|
||||
# The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
|
||||
# <tt>:inverse_of</tt> is set:
|
||||
#
|
||||
# class Taggable < ActiveRecord::Base
|
||||
# belongs_to :post
|
||||
# belongs_to :tag, :inverse_of => :taggings
|
||||
# end
|
||||
#
|
||||
# === Polymorphic Associations
|
||||
#
|
||||
# Polymorphic associations on models are not restricted on what types of models they
|
||||
@ -1043,13 +1059,21 @@ module ClassMethods
|
||||
# [:as]
|
||||
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
||||
# [:through]
|
||||
# Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>
|
||||
# and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You
|
||||
# can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, <tt>has_one</tt>
|
||||
# or <tt>has_many</tt> association on the join model. The collection of join models
|
||||
# can be managed via the collection API. For example, new join models are created for
|
||||
# newly associated objects, and if some are gone their rows are deleted (directly,
|
||||
# no destroy callbacks are triggered).
|
||||
# Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>,
|
||||
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
|
||||
# source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>,
|
||||
# <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
|
||||
#
|
||||
# If the association on the join model is a +belongs_to+, the collection can be modified
|
||||
# and the records on the <tt>:through</tt> model will be automatically created and removed
|
||||
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
|
||||
# <tt>:through</tt> association directly.
|
||||
#
|
||||
# If you are going to modify the association (rather than just read from it), then it is
|
||||
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
|
||||
# join model. This allows associated records to be built which will automatically create
|
||||
# the appropriate join model records when they are saved. (See the 'Association Join Models'
|
||||
# section above.)
|
||||
# [:source]
|
||||
# Specifies the source association name used by <tt>has_many :through</tt> queries.
|
||||
# Only use it if the name cannot be inferred from the association.
|
||||
|
@ -37,16 +37,44 @@ def <<(*records)
|
||||
|
||||
def insert_record(record, validate = true)
|
||||
return if record.new_record? && !record.save(:validate => validate)
|
||||
|
||||
through_association = @owner.send(@reflection.through_reflection.name)
|
||||
through_association.create!(construct_join_attributes(record))
|
||||
|
||||
through_record(record).save!
|
||||
update_counter(1)
|
||||
record
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def through_record(record)
|
||||
through_association = @owner.send(:association_proxy, @reflection.through_reflection.name)
|
||||
attributes = construct_join_attributes(record)
|
||||
|
||||
through_record = Array.wrap(through_association.target).find { |candidate|
|
||||
candidate.attributes.slice(*attributes.keys) == attributes
|
||||
}
|
||||
|
||||
unless through_record
|
||||
through_record = through_association.build(attributes)
|
||||
through_record.send("#{@reflection.source_reflection.name}=", record)
|
||||
end
|
||||
|
||||
through_record
|
||||
end
|
||||
|
||||
def build_record(attributes)
|
||||
record = super(attributes)
|
||||
|
||||
inverse = @reflection.source_reflection.inverse_of
|
||||
if inverse
|
||||
if inverse.macro == :has_many
|
||||
record.send(inverse.name) << through_record(record)
|
||||
elsif inverse.macro == :has_one
|
||||
record.send("#{inverse.name}=", through_record(record))
|
||||
end
|
||||
end
|
||||
|
||||
record
|
||||
end
|
||||
|
||||
def target_reflection_has_associated_record?
|
||||
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank?
|
||||
false
|
||||
@ -79,6 +107,8 @@ def delete_records(records, method)
|
||||
count = scope.delete_all
|
||||
end
|
||||
|
||||
delete_through_records(through, records)
|
||||
|
||||
if @reflection.through_reflection.macro == :has_many && update_through_counter?(method)
|
||||
update_counter(-count, @reflection.through_reflection)
|
||||
end
|
||||
@ -86,6 +116,18 @@ def delete_records(records, method)
|
||||
update_counter(-count)
|
||||
end
|
||||
|
||||
def delete_through_records(through, records)
|
||||
if @reflection.through_reflection.macro == :has_many
|
||||
records.each do |record|
|
||||
through.target.delete(through_record(record))
|
||||
end
|
||||
else
|
||||
records.each do |record|
|
||||
through.target = nil if through.target == through_record(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_target
|
||||
return [] unless target_reflection_has_associated_record?
|
||||
scoped.all
|
||||
|
@ -113,6 +113,24 @@ def test_associate_new_by_building
|
||||
assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted")
|
||||
end
|
||||
|
||||
def test_build_then_save_with_has_many_inverse
|
||||
post = posts(:thinking)
|
||||
person = post.people.build(:first_name => "Bob")
|
||||
person.save
|
||||
post.reload
|
||||
|
||||
assert post.people.include?(person)
|
||||
end
|
||||
|
||||
def test_build_then_save_with_has_one_inverse
|
||||
post = posts(:thinking)
|
||||
person = post.single_people.build(:first_name => "Bob")
|
||||
person.save
|
||||
post.reload
|
||||
|
||||
assert post.single_people.include?(person)
|
||||
end
|
||||
|
||||
def test_delete_association
|
||||
assert_queries(2){posts(:welcome);people(:michael); }
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
class Person < ActiveRecord::Base
|
||||
has_many :readers
|
||||
has_one :reader
|
||||
|
||||
has_many :posts, :through => :readers
|
||||
has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, :conditions => 'comments.id is null'
|
||||
|
||||
|
@ -95,6 +95,7 @@ def add_joins_and_select
|
||||
has_many :readers
|
||||
has_many :readers_with_person, :include => :person, :class_name => "Reader"
|
||||
has_many :people, :through => :readers
|
||||
has_many :single_people, :through => :readers
|
||||
has_many :people_with_callbacks, :source=>:person, :through => :readers,
|
||||
:before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) },
|
||||
:after_add => lambda {|owner, reader| log(:added, :after, reader.first_name) },
|
||||
|
@ -1,4 +1,5 @@
|
||||
class Reader < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
belongs_to :person
|
||||
belongs_to :person, :inverse_of => :readers
|
||||
belongs_to :single_person, :class_name => 'Person', :foreign_key => :person_id, :inverse_of => :reader
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user