mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add has_one :through support, finally. Closes #4756 [thechrisoshow]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@9067 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
d8f76e66a1
commit
273b21faa9
15 changed files with 246 additions and 23 deletions
|
@ -1,5 +1,7 @@
|
|||
*SVN*
|
||||
|
||||
* Add has_one :through support. #4756 [thechrisoshow]
|
||||
|
||||
* Migrations: create_table supports primary_key_prefix_type. #10314 [student, thechrisoshow]
|
||||
|
||||
* Added logging for dependency load errors with fixtures #11056 [stuthulhu]
|
||||
|
|
|
@ -49,6 +49,14 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
|
||||
parent_records.each do |parent_record|
|
||||
association_proxy = parent_record.send(reflection_name)
|
||||
association_proxy.loaded
|
||||
association_proxy.target = associated_record
|
||||
end
|
||||
end
|
||||
|
||||
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
|
||||
associated_records.each do |associated_record|
|
||||
mapped_records = id_to_record_map[associated_record[key].to_i]
|
||||
|
@ -98,10 +106,26 @@ module ActiveRecord
|
|||
|
||||
def preload_has_one_association(records, reflection, preload_options={})
|
||||
id_to_record_map, ids = construct_id_map(records)
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
options = reflection.options
|
||||
if options[:through]
|
||||
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
|
||||
through_records = preload_through_records(records, reflection, options[:through])
|
||||
through_reflection = reflections[options[:through]]
|
||||
through_primary_key = through_reflection.primary_key_name
|
||||
unless through_records.empty?
|
||||
source = reflection.source_reflection.name
|
||||
through_records.first.class.preload_associations(through_records, source)
|
||||
through_records.compact.each do |through_record|
|
||||
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_i],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
end
|
||||
else
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
|
||||
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
|
||||
reflection.primary_key_name)
|
||||
|
||||
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
|
||||
end
|
||||
end
|
||||
|
||||
def preload_has_many_association(records, reflection, preload_options={})
|
||||
|
|
|
@ -6,6 +6,7 @@ require 'active_record/associations/has_one_association'
|
|||
require 'active_record/associations/has_many_association'
|
||||
require 'active_record/associations/has_many_through_association'
|
||||
require 'active_record/associations/has_and_belongs_to_many_association'
|
||||
require 'active_record/associations/has_one_through_association'
|
||||
|
||||
module ActiveRecord
|
||||
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
|
@ -737,6 +738,12 @@ module ActiveRecord
|
|||
# as the default +foreign_key+.
|
||||
# * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded.
|
||||
# * <tt>:as</tt>: Specifies a polymorphic interface (See <tt>#belongs_to</tt>).
|
||||
# * <tt>:through</tt>: 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>has_one</tt> or <tt>belongs_to</tt> association on the join model.
|
||||
# * <tt>:source</tt>: Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
|
||||
# inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a
|
||||
# <tt>:favorite</tt> on +Favorite+, unless a <tt>:source</tt> is given.
|
||||
# * <tt>:readonly</tt> - if set to +true+, the associated object is readonly through the association.
|
||||
#
|
||||
# Option examples:
|
||||
|
@ -746,27 +753,34 @@ module ActiveRecord
|
|||
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
|
||||
# has_one :attachment, :as => :attachable
|
||||
# has_one :boss, :readonly => :true
|
||||
# has_one :club, :through => :membership
|
||||
# has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
|
||||
def has_one(association_id, options = {})
|
||||
reflection = create_has_one_reflection(association_id, options)
|
||||
if options[:through]
|
||||
reflection = create_has_one_through_reflection(association_id, options)
|
||||
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
|
||||
else
|
||||
reflection = create_has_one_reflection(association_id, options)
|
||||
|
||||
ivar = "@#{reflection.name}"
|
||||
ivar = "@#{reflection.name}"
|
||||
|
||||
method_name = "has_one_after_save_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
||||
method_name = "has_one_after_save_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
||||
|
||||
if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
|
||||
association["#{reflection.primary_key_name}"] = id
|
||||
association.save(true)
|
||||
if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
|
||||
association["#{reflection.primary_key_name}"] = id
|
||||
association.save(true)
|
||||
end
|
||||
end
|
||||
after_save method_name
|
||||
|
||||
association_accessor_methods(reflection, HasOneAssociation)
|
||||
association_constructor_method(:build, reflection, HasOneAssociation)
|
||||
association_constructor_method(:create, reflection, HasOneAssociation)
|
||||
|
||||
configure_dependency_for_has_one(reflection)
|
||||
end
|
||||
after_save method_name
|
||||
|
||||
association_accessor_methods(reflection, HasOneAssociation)
|
||||
association_constructor_method(:build, reflection, HasOneAssociation)
|
||||
association_constructor_method(:create, reflection, HasOneAssociation)
|
||||
|
||||
configure_dependency_for_has_one(reflection)
|
||||
end
|
||||
|
||||
# Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
|
||||
|
@ -1058,7 +1072,12 @@ module ActiveRecord
|
|||
association = association_proxy_class.new(self, reflection)
|
||||
end
|
||||
|
||||
association.replace(new_value)
|
||||
if association_proxy_class == HasOneThroughAssociation
|
||||
association.create_through_record(new_value)
|
||||
self.send(reflection.name, new_value)
|
||||
else
|
||||
association.replace(new_value)
|
||||
end
|
||||
|
||||
instance_variable_set(ivar, new_value.nil? ? nil : association)
|
||||
end
|
||||
|
@ -1301,6 +1320,13 @@ module ActiveRecord
|
|||
create_reflection(:has_one, association_id, options, self)
|
||||
end
|
||||
|
||||
def create_has_one_through_reflection(association_id, options)
|
||||
options.assert_valid_keys(
|
||||
:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source
|
||||
)
|
||||
create_reflection(:has_one, association_id, options, self)
|
||||
end
|
||||
|
||||
def create_belongs_to_reflection(association_id, options)
|
||||
options.assert_valid_keys(
|
||||
:class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class HasOneThroughAssociation < ActiveRecord::Associations::HasManyThroughAssociation
|
||||
|
||||
def create_through_record(new_value) #nodoc:
|
||||
klass = @reflection.through_reflection.klass
|
||||
|
||||
current_object = @owner.send(@reflection.through_reflection.name)
|
||||
|
||||
if current_object
|
||||
klass.destroy(current_object)
|
||||
@owner.clear_association_cache
|
||||
end
|
||||
|
||||
@owner.send(@reflection.through_reflection.name, klass.send(:create, construct_join_attributes(new_value)))
|
||||
end
|
||||
|
||||
private
|
||||
def find(*args)
|
||||
super(args.merge(:limit => 1))
|
||||
end
|
||||
|
||||
def find_target
|
||||
super.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,10 @@ require 'models/parrot'
|
|||
require 'models/pirate'
|
||||
require 'models/treasure'
|
||||
require 'models/price_estimate'
|
||||
require 'models/club'
|
||||
require 'models/member'
|
||||
require 'models/membership'
|
||||
require 'models/sponsor'
|
||||
|
||||
class AssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
|
||||
|
@ -476,6 +480,63 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
|
|||
|
||||
end
|
||||
|
||||
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :members, :clubs, :memberships, :sponsors
|
||||
|
||||
def setup
|
||||
@member = members(:groucho)
|
||||
end
|
||||
|
||||
def test_has_one_through_with_has_one
|
||||
assert_equal clubs(:boring_club), @member.club
|
||||
end
|
||||
|
||||
def test_has_one_through_with_has_many
|
||||
assert_equal clubs(:moustache_club), @member.favourite_club
|
||||
end
|
||||
|
||||
def test_creating_association_creates_through_record
|
||||
new_member = Member.create(:name => "Chris")
|
||||
new_member.club = Club.create(:name => "LRUG")
|
||||
assert_not_nil new_member.current_membership
|
||||
assert_not_nil new_member.club
|
||||
end
|
||||
|
||||
def test_replace_target_record
|
||||
new_club = Club.create(:name => "Marx Bros")
|
||||
@member.club = new_club
|
||||
@member.reload
|
||||
assert_equal new_club, @member.club
|
||||
end
|
||||
|
||||
def test_replacing_target_record_deletes_old_association
|
||||
assert_no_difference "Membership.count" do
|
||||
new_club = Club.create(:name => "Bananarama")
|
||||
@member.club = new_club
|
||||
@member.reload
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_one_through_polymorphic
|
||||
assert_equal clubs(:moustache_club), @member.sponsor_club
|
||||
end
|
||||
|
||||
def has_one_through_to_has_many
|
||||
assert_equal 2, @member.fellow_members.size
|
||||
end
|
||||
|
||||
def test_has_one_through_eager_loading
|
||||
members = Member.find(:all, :include => :club)
|
||||
assert_equal 2, members.size
|
||||
assert_not_nil assert_no_queries {members[0].club}
|
||||
end
|
||||
|
||||
def test_has_one_through_eager_loading_through_polymorphic
|
||||
members = Member.find(:all, :include => :sponsor_club)
|
||||
assert_equal 2, members.size
|
||||
assert_not_nil assert_no_queries {members[0].sponsor_club}
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :accounts, :companies, :developers, :projects,
|
||||
|
|
6
activerecord/test/fixtures/clubs.yml
vendored
Normal file
6
activerecord/test/fixtures/clubs.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
boring_club:
|
||||
name: Banana appreciation society
|
||||
moustache_club:
|
||||
name: Moustache and Eyebrow Fancier Club
|
||||
crazy_club:
|
||||
name: Skull and bones
|
4
activerecord/test/fixtures/members.yml
vendored
Normal file
4
activerecord/test/fixtures/members.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
groucho:
|
||||
name: Groucho Marx
|
||||
some_other_guy:
|
||||
name: Englebert Humperdink
|
20
activerecord/test/fixtures/memberships.yml
vendored
Normal file
20
activerecord/test/fixtures/memberships.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
membership_of_boring_club:
|
||||
joined_on: <%= 3.weeks.ago.to_s(:db) %>
|
||||
club: boring_club
|
||||
member: groucho
|
||||
favourite: false
|
||||
type: CurrentMembership
|
||||
|
||||
membership_of_favourite_club:
|
||||
joined_on: <%= 3.weeks.ago.to_s(:db) %>
|
||||
club: moustache_club
|
||||
member: groucho
|
||||
favourite: true
|
||||
type: Membership
|
||||
|
||||
other_guys_membership:
|
||||
joined_on: <%= 4.weeks.ago.to_s(:db) %>
|
||||
club: boring_club
|
||||
member: some_other_guy
|
||||
favourite: false
|
||||
type: CurrentMembership
|
3
activerecord/test/fixtures/sponsors.yml
vendored
Normal file
3
activerecord/test/fixtures/sponsors.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
moustache_club_sponsor_for_groucho:
|
||||
sponsor_club: moustache_club
|
||||
sponsorable: groucho (Member)
|
6
activerecord/test/models/club.rb
Normal file
6
activerecord/test/models/club.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class Club < ActiveRecord::Base
|
||||
has_many :memberships
|
||||
has_many :members, :through => :memberships
|
||||
has_many :current_memberships
|
||||
has_many :sponsors
|
||||
end
|
9
activerecord/test/models/member.rb
Normal file
9
activerecord/test/models/member.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class Member < ActiveRecord::Base
|
||||
has_one :current_membership
|
||||
has_many :memberships
|
||||
has_many :fellow_members, :through => :club, :source => :members
|
||||
has_one :club, :through => :current_membership
|
||||
has_one :favourite_club, :through => :memberships, :conditions => ["memberships.favourite = ?", true], :source => :club
|
||||
has_one :sponsor, :as => :sponsorable
|
||||
has_one :sponsor_club, :through => :sponsor
|
||||
end
|
9
activerecord/test/models/membership.rb
Normal file
9
activerecord/test/models/membership.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class Membership < ActiveRecord::Base
|
||||
belongs_to :member
|
||||
belongs_to :club
|
||||
end
|
||||
|
||||
class CurrentMembership < Membership
|
||||
belongs_to :member
|
||||
belongs_to :club
|
||||
end
|
4
activerecord/test/models/sponsor.rb
Normal file
4
activerecord/test/models/sponsor.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Sponsor < ActiveRecord::Base
|
||||
belongs_to :sponsor_club, :class_name => "Club", :foreign_key => "club_id"
|
||||
belongs_to :sponsorable, :polymorphic => true
|
||||
end
|
|
@ -50,6 +50,10 @@ ActiveRecord::Schema.define do
|
|||
t.integer :post_id, :null => false
|
||||
end
|
||||
|
||||
create_table :clubs, :force => true do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
||||
create_table :colnametests, :force => true do |t|
|
||||
t.integer :references, :null => false
|
||||
end
|
||||
|
@ -117,6 +121,17 @@ ActiveRecord::Schema.define do
|
|||
t.integer :version, :null => false, :default => 0
|
||||
end
|
||||
|
||||
create_table :members, :force => true do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
||||
create_table :memberships, :force => true do |t|
|
||||
t.datetime :joined_on
|
||||
t.integer :club_id, :member_id
|
||||
t.boolean :favourite, :default => false
|
||||
t.string :type
|
||||
end
|
||||
|
||||
create_table :minimalistics, :force => true do |t|
|
||||
end
|
||||
|
||||
|
@ -178,6 +193,12 @@ ActiveRecord::Schema.define do
|
|||
t.integer :person_id, :null => false
|
||||
end
|
||||
|
||||
create_table :sponsors, :force => true do |t|
|
||||
t.integer :club_id
|
||||
t.integer :sponsorable_id
|
||||
t.integer :sponsorable_type
|
||||
end
|
||||
|
||||
create_table :subscribers, :force => true, :id => false do |t|
|
||||
t.string :nick, :null => false
|
||||
t.string :name
|
||||
|
|
Loading…
Reference in a new issue