Hello all,

I still have a patch for #3438 (eager loading doesn't respect
association orders); it passes all unit tests, and has additional unit
tests with full coverage. The patch is attached.

== The Bug ==

 Author.find(1).posts != Author.find(1, :include => [:posts]).posts

if Author has_many :posts, :order => anything.

This means that either one must avoid eager loading or constantly
specify an :order option (despite having already specified one in the
association options). Awkward.

http://dev.rubyonrails.org/ticket/3438

== The Solution ==

My patch adds ~20 lines of code to
ActiveRecord::Associations::ClassMethods#construct_finder_sql_with_included_associations
which run through the various associations in the join dependency,
extract all :order options, expand them to their full form (e.g.,
'posts.published_at' instead of 'published_at', minding table
aliases), and append them to the SQL's ORDER BY clause, if any exists
(creating one if not).

== The Catch ==

Developers who have previously relied on their database's default
order (if any exists) may be unpleasantly surprised to find that their
records are not coming back in the expected order. For example:

 Author.find(:all) != Author.find(:all, :include => [:posts])

It has been suggested by some that these developers would do well to
be surprised, as not specifying an ORDER BY clause means that the
order of the records is not meaningful to you, and that relying upon
an implementation quirk is bad form. This "fire hot" school of
pedagogy is certainly effective, but may result in additional support
demands.

== The Good News ==

This patch passes all existing unit tests, and adds two additional
tests which assert the proper behavior over a wide variety of model
types, including STI, Polymorphic, Rich Joins, etc., and even cascaded
eager loading. 2 tests, 149 assertions.

== The Previously Addressed Issues ==

My original version of this patch assumed a default SQL sort order. It
no longer does so.

== The Begging ==

C'mon. Please. Dude. C'mon. Duuuude. Please. C'mon. Please? Awww....
dude. Dude? C'mon?

== The End ==

Since Trac is on sabbatical, I figured I'd toss up my patch here, in
the hopes that it will be applied. The last time I tried this I got a
lot of great feedback and even some publicity--thanks, Rodney!--but
everyone was at RailsConf, and I guess that was more fun than fixing
my pet peeves. ;-)

Thanks!

--
Coda Hale
http://blog.codahale.com
Index: activerecord/test/associations_cascaded_eager_loading_test.rb
===================================================================
--- activerecord/test/associations_cascaded_eager_loading_test.rb	(revision 4527)
+++ activerecord/test/associations_cascaded_eager_loading_test.rb	(working copy)
@@ -103,4 +103,16 @@
       authors.first.posts.first.special_comments.first.post.very_special_comment
     end
   end
+  
+  def test_association_orders_work_with_cascaded_eager_loading
+    [ { :posts => :comments }, { :posts => :categories },
+      { :posts => [:comments, :categories] },
+      { :comments => :post }, { :funky_comments => :post } ].each do |association|
+      regular_records = Author.find(:all).sort_by(&:id)
+      eager_records = Author.find(:all, :include => association).sort_by(&:id)
+      regular_records.size.times do |i|
+        assert_equal regular_records[i].send(association.keys.first).map(&:id), eager_records[i].send(association.keys.first).map(&:id), "Author.#{association.keys.first} has a different order when eagerly loaded via #{association.inspect}."
+      end
+    end
+  end
 end
Index: activerecord/test/associations_go_eager_test.rb
===================================================================
--- activerecord/test/associations_go_eager_test.rb	(revision 4527)
+++ activerecord/test/associations_go_eager_test.rb	(working copy)
@@ -356,4 +356,31 @@
     assert_equal 2, one.comments.size
     assert_equal 2, one.categories.size
   end
+  
+  def test_association_orders_work_with_eager_loading
+    { Author         => [ :posts,  :posts_with_comments, :posts_with_categories, :posts_with_comments_and_categories,
+                          :comments, :funky_comments, :special_posts, :hello_posts, :nonexistent_posts, 
+                          :posts_with_callbacks, :posts_with_proc_callbacks, :posts_with_multiple_callbacks,
+                          :unchangable_posts, :author_favorites ],
+      Category       => [ :posts, :special_posts, :hello_posts, :nonexistent_posts ],
+      DependentFirm  => [ :companies ],
+      Firm           => [ :clients, :clients_sorted_desc, :clients_of_firm, :dependent_clients_of_firm,
+                          :exclusively_dependent_clients_of_firm ],
+      Person         => [ :readers, :posts ],
+      Post           => [ :comments, :special_comments, :categories, :special_categories, :readers, :people ]
+    }.each do |model, associations|
+      associations.each do |association|
+        regular_records = model.find(:all).sort_by(&:id)
+        eager_records = model.find(:all, :include => [association]).sort_by(&:id)
+        regular_records.size.times do |i|
+          assert_equal regular_records[i].send(association).map(&:id), eager_records[i].send(association).map(&:id), "#{model}.#{association} has a different order when eagerly loaded."
+        end
+        
+        expected_order = model.find(:all, :order => "#{model.table_name}.#{model.primary_key}").map(&association).flatten.map(&:id)
+        actual_order = model.find(:all, :include => [association], :order => "#{model.table_name}.#{model.primary_key}").map(&association).flatten.map(&:id)
+        assert_equal expected_order, actual_order, "#{model}.#{association} has a different order when eagerly loaded given an :order option"
+      end      
+    end
+  end
+
 end
Index: activerecord/test/fixtures/post.rb
===================================================================
--- activerecord/test/fixtures/post.rb	(revision 4527)
+++ activerecord/test/fixtures/post.rb	(working copy)
@@ -15,10 +15,10 @@
 
   has_one  :very_special_comment
   has_one  :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post
-  has_many :special_comments
+  has_many :special_comments, :order => 'type'
 
-  has_and_belongs_to_many :categories
-  has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
+  has_and_belongs_to_many :categories, :order => 'name'
+  has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id', :order => 'name DESC, type ASC'
 
   has_many :taggings, :as => :taggable
   has_many :tags, :through => :taggings, :include => :tagging do
@@ -38,8 +38,8 @@
   has_many :categorizations, :foreign_key => :category_id
   has_many :authors, :through => :categorizations
 
-  has_many :readers
-  has_many :people, :through => :readers
+  has_many :readers, :order => 'readers.person_id'
+  has_many :people, :through => :readers, :order => 'first_name DESC, id ASC'
 
   def self.what_are_you
     'a post...'
Index: activerecord/test/fixtures/author.rb
===================================================================
--- activerecord/test/fixtures/author.rb	(revision 4527)
+++ activerecord/test/fixtures/author.rb	(working copy)
@@ -1,7 +1,7 @@
 class Author < ActiveRecord::Base
-  has_many :posts
-  has_many :posts_with_comments, :include => :comments, :class_name => "Post"
-  has_many :posts_with_categories, :include => :categories, :class_name => "Post"
+  has_many :posts, :order => 'title DESC'
+  has_many :posts_with_comments, :include => :comments, :class_name => "Post", :order => 'title DESC'
+  has_many :posts_with_categories, :include => :categories, :class_name => "Post", :order => 'title'
   has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post"
   has_many :posts_with_extension, :class_name => "Post" do #, :extend => ProxyTestExtension
     def testing_proxy_owner
@@ -14,8 +14,8 @@
       proxy_target
     end
   end
-  has_many :comments, :through => :posts
-  has_many :funky_comments, :through => :posts, :source => :comments
+  has_many :comments, :through => :posts, :order => 'body DESC'
+  has_many :funky_comments, :through => :posts, :source => :comments, :order => 'body'
 
   has_many :special_posts,        :class_name => "Post"
   has_many :hello_posts,          :class_name => "Post", :conditions=>"\#{aliased_table_name}.body = 'hello'"
Index: activerecord/lib/active_record/associations.rb
===================================================================
--- activerecord/lib/active_record/associations.rb	(revision 4527)
+++ activerecord/lib/active_record/associations.rb	(working copy)
@@ -1148,7 +1148,33 @@
           add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
 
           sql << "ORDER BY #{options[:order]} " if options[:order]
- 
+          
+          # Collect all eagerly-loaded assocations with any order options, and
+          # add in their table names unless it's already there.
+          associated_orders = join_dependency.join_associations.select do |assoc|
+            assoc.reflection.options[:order]
+          end.map do |assoc|
+            orders = assoc.reflection.options[:order].split(',').map(&:strip)
+            orders.map do |order|
+              if order =~ /\./
+                order
+              else
+                assoc.aliased_table_name + '.' + order
+              end
+            end
+          end
+          
+          # Add the association orders to the SQL statement.
+          if associated_orders.any?
+            if options[:order]
+              sql << ", "
+            else
+              sql << "ORDER BY "
+            end
+            sql << associated_orders.flatten.join(', ')
+          end
+          sql << ' '
+
           add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
  
           return sanitize_sql(sql)
_______________________________________________
Rails-core mailing list
Rails-core@lists.rubyonrails.org
http://lists.rubyonrails.org/mailman/listinfo/rails-core

Reply via email to