Skip to content

Commit

Permalink
Preload uses exclusive scope [#643 state:resolved]
Browse files Browse the repository at this point in the history
With self referential associations, the scope for the the top level should not affect fetching of associations, for example
when doing

Person.male.find :all, :include => :friends

we should load all of the friends for each male, not just the male friends.
  • Loading branch information
fcheung committed Dec 26, 2008
1 parent eb457ce commit 5cebe69
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 16 deletions.
31 changes: 18 additions & 13 deletions activerecord/lib/active_record/association_preload.rb
Expand Up @@ -43,7 +43,7 @@ def self.included(base)
# loading in a more high-level (application developer-friendly) manner.
module ClassMethods
protected

# Eager loads the named associations for the given ActiveRecord record(s).
#
# In this description, 'association name' shall refer to the name passed
Expand Down Expand Up @@ -113,7 +113,7 @@ def preload_one_association(records, association, preload_options={})
# unnecessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection

# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
# the following could call 'preload_belongs_to_association',
# 'preload_has_many_association', etc.
Expand All @@ -128,7 +128,7 @@ def add_preloaded_records_to_collection(parent_records, reflection_name, associa
association_proxy.target.push(*[associated_record].flatten)
end
end

def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
parent_record.send("set_#{reflection_name}_target", associated_record)
Expand Down Expand Up @@ -183,18 +183,19 @@ def preload_has_and_belongs_to_many_association(records, reflection, preload_opt
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)

associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
:order => options[:order])

associated_records = reflection.klass.with_exclusive_scope do
reflection.klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
:order => options[:order])
end
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
end

def preload_has_one_association(records, reflection, preload_options={})
return if records.first.send("loaded_#{reflection.name}?")
id_to_record_map, ids = construct_id_map(records)
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
Expand Down Expand Up @@ -248,7 +249,7 @@ def preload_has_many_association(records, reflection, preload_options={})
reflection.primary_key_name)
end
end

def preload_through_records(records, reflection, through_association)
through_reflection = reflections[through_association]
through_primary_key = through_reflection.primary_key_name
Expand Down Expand Up @@ -333,11 +334,13 @@ def preload_belongs_to_association(records, reflection, preload_options={})
end
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
associated_records = klass.find(:all, :conditions => [conditions, ids],
associated_records = klass.with_exclusive_scope do
klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
end
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
Expand All @@ -355,13 +358,15 @@ def find_associated_records(ids, reflection, preload_options)

conditions << append_conditions(reflection, preload_options)

reflection.klass.find(:all,
reflection.klass.with_exclusive_scope do
reflection.klass.find(:all,
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
:include => preload_options[:include] || options[:include],
:conditions => [conditions, ids],
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
:order => preload_options[:order] || options[:order])
end
end


Expand Down
15 changes: 15 additions & 0 deletions activerecord/test/cases/associations/eager_test.rb
Expand Up @@ -771,4 +771,19 @@ def test_eager_loading_with_conditions_on_join_model_preloads
assert_equal author_addresses(:david_address), authors[0].author_address
end

def test_preload_belongs_to_uses_exclusive_scope
people = Person.males.find(:all, :include => :primary_contact)
assert_not_equal people.length, 0
people.each do |person|
assert_no_queries {assert_not_nil person.primary_contact}
assert_equal Person.find(person.id).primary_contact, person.primary_contact
end
end

def test_preload_has_many_uses_exclusive_scope
people = Person.males.find :all, :include => :agents
people.each do |person|
assert_equal Person.find(person.id).agents, person.agents
end
end
end
11 changes: 10 additions & 1 deletion activerecord/test/fixtures/people.yml
@@ -1,6 +1,15 @@
michael:
id: 1
first_name: Michael
primary_contact_id: 2
gender: M
david:
id: 2
first_name: David
first_name: David
primary_contact_id: 3
gender: M
susan:
id: 3
first_name: Susan
primary_contact_id: 2
gender: F
6 changes: 6 additions & 0 deletions activerecord/test/models/person.rb
Expand Up @@ -7,4 +7,10 @@ class Person < ActiveRecord::Base
has_many :jobs, :through => :references
has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true]
has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id'

belongs_to :primary_contact, :class_name => 'Person'
has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'

named_scope :males, :conditions => { :gender => 'M' }
named_scope :females, :conditions => { :gender => 'F' }
end
6 changes: 4 additions & 2 deletions activerecord/test/schema/schema.rb
Expand Up @@ -298,8 +298,10 @@ def create_table(*args, &block)
end

create_table :people, :force => true do |t|
t.string :first_name, :null => false
t.integer :lock_version, :null => false, :default => 0
t.string :first_name, :null => false
t.references :primary_contact
t.string :gender, :limit => 1
t.integer :lock_version, :null => false, :default => 0
end

create_table :pets, :primary_key => :pet_id ,:force => true do |t|
Expand Down

0 comments on commit 5cebe69

Please sign in to comment.