Ticket #10012: ar_joins_fixed_for_10011.diff
| File ar_joins_fixed_for_10011.diff, 25.6 kB (added by RubyRedRick, 1 year ago) |
|---|
-
activerecord/test/associations/ar_joins_test.rb
old new 1 require 'abstract_unit' 2 require 'fixtures/post' 3 require 'fixtures/comment' 4 require 'fixtures/author' 5 require 'fixtures/category' 6 require 'fixtures/categorization' 7 require 'fixtures/company' 8 require 'fixtures/topic' 9 require 'fixtures/reply' 10 require 'fixtures/developer' 11 require 'fixtures/project' 12 13 class ArJoinsTest < Test::Unit::TestCase 14 fixtures :authors, :posts, :comments, :categories, :categories_posts, :people, 15 :developers, :projects, :developers_projects, 16 :categorizations, :companies, :accounts, :topics 17 18 def test_ar_joins 19 authors = Author.find(:all, :joins => :posts, :conditions => ['posts.type = ?', "Post"]) 20 assert_not_equal(0 , authors.length) 21 authors.each do |author| 22 assert !(author.send(:instance_variables).include? "@posts") 23 assert(!author.readonly?, "non-string join value produced read-only result.") 24 end 25 end 26 27 def test_ar_joins_with_cascaded_two_levels 28 authors = Author.find(:all, :joins=>{:posts=>:comments}) 29 assert_equal(2, authors.length) 30 authors.each do |author| 31 assert !(author.send(:instance_variables).include? "@posts") 32 assert(!author.readonly?, "non-string join value produced read-only result.") 33 end 34 authors = Author.find(:all, :joins=>{:posts=>:comments}, :conditions => ["comments.body = ?", "go crazy" ]) 35 assert_equal(1, authors.length) 36 authors.each do |author| 37 assert !(author.send(:instance_variables).include? "@posts") 38 assert(!author.readonly?, "non-string join value produced read-only result.") 39 end 40 end 41 42 43 def test_ar_joins_with_complex_conditions 44 authors = Author.find(:all, :joins=>{:posts=>[:comments, :categories]}, 45 :conditions => ["categories.name = ? AND posts.title = ?", "General", "So I was thinking"] 46 ) 47 assert_equal(1, authors.length) 48 authors.each do |author| 49 assert !(author.send(:instance_variables).include? "@posts") 50 assert(!author.readonly?, "non-string join value produced read-only result.") 51 end 52 assert_equal("David", authors.first.name) 53 end 54 55 def test_ar_join_with_has_many_and_limit_and_scoped_and_explicit_conditions 56 Post.with_scope(:find => { :conditions => "1=1" }) do 57 posts = authors(:david).posts.find(:all, 58 :joins => :comments, 59 :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", 60 :limit => 2 61 ) 62 assert_equal 2, posts.size 63 64 count = Post.count( 65 :joins => [ :comments, :author ], 66 :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", 67 :limit => 2 68 ) 69 assert_equal count, posts.size 70 end 71 end 72 73 def test_ar_join_with_scoped_order_using_association_limiting_without_explicit_scope 74 posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :joins => :comments, :order => 'posts.id DESC', :limit => 2) 75 posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do 76 Post.find(:all, :conditions => 'comments.id is not null', :joins => :comments, :limit => 2) 77 end 78 assert_equal posts_with_explicit_order, posts_with_scoped_order 79 end 80 81 def test_scoped_find_include 82 # with the include, will retrieve only developers for the given project 83 scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do 84 Developer.find(:all, :conditions => 'projects.id = 2') 85 end 86 assert scoped_developers.include?(developers(:david)) 87 assert !scoped_developers.include?(developers(:jamis)) 88 assert_equal 1, scoped_developers.size 89 end 90 91 92 def test_nested_scoped_find_ar_join 93 Developer.with_scope(:find => { :joins => :projects }) do 94 Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do 95 assert_equal('David', Developer.find(:first).name) 96 end 97 end 98 end 99 100 def test_nested_scoped_find_merged_ar_join 101 # :include's remain unique and don't "double up" when merging 102 Developer.with_scope(:find => { :joins => :projects, :conditions => "projects.id = 2" }) do 103 Developer.with_scope(:find => { :joins => :projects }) do 104 assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length 105 assert_equal('David', Developer.find(:first).name) 106 end 107 end 108 # the nested scope doesn't remove the first :include 109 Developer.with_scope(:find => { :joins => :projects, :conditions => "projects.id = 2" }) do 110 Developer.with_scope(:find => { :joins => [] }) do 111 assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length 112 assert_equal('David', Developer.find(:first).name) 113 end 114 end 115 # mixing array and symbol include's will merge correctly 116 Developer.with_scope(:find => { :joins => [:projects], :conditions => "projects.id = 2" }) do 117 Developer.with_scope(:find => { :joins => :projects }) do 118 assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length 119 assert_equal('David', Developer.find(:first).name) 120 end 121 end 122 end 123 124 def test_nested_scoped_find_replace_include 125 Developer.with_scope(:find => { :joins => :projects }) do 126 Developer.with_exclusive_scope(:find => { :joins => [] }) do 127 assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length 128 end 129 end 130 end 131 132 # 133 # Calculations 134 # 135 def test_count_with_ar_joins 136 assert_equal(2, Author.count(:joins => :posts, :conditions => ['posts.type = ?', "Post"])) 137 assert_equal(1, Author.count(:joins => :posts, :conditions => ['posts.type = ?', "SpecialPost"])) 138 end 139 140 def test_should_get_maximum_of_field_with_joins 141 assert_equal 50, Account.maximum(:credit_limit, :joins=> :firm, :conditions => "companies.name != 'Summit'") 142 end 143 144 def test_should_get_maximum_of_field_with_scoped_include 145 Account.with_scope :find => { :joins => :firm, :conditions => "companies.name != 'Summit'" } do 146 assert_equal 50, Account.maximum(:credit_limit) 147 end 148 end 149 150 def test_should_not_modify_options_when_using_ar_joins_on_count 151 options = {:conditions => 'companies.id > 1', :joins => :firm} 152 options_copy = options.dup 153 154 Account.count(:all, options) 155 assert_equal options_copy, options 156 end 157 158 end 159 -
activerecord/lib/active_record/calculations.rb
old new 15 15 # The third approach, count using options, accepts an option hash as the only parameter. The options are: 16 16 # 17 17 # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. 18 # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 19 # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. 18 # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 19 # or names associations in the same form used for the :include option. 20 # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. 21 # Pass :readonly => false to override. 22 # See adding joins for associations under Association. 20 23 # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 21 24 # to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting. 22 25 # See eager loading under Associations. … … 109 112 # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' 110 113 # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors 111 114 def calculate(operation, column_name, options = {}) 115 options, ar_joins = *extract_ar_join_from_options(options) 112 116 validate_calculation_options(operation, options) 117 options[:ar_joins] = ar_joins if ar_joins 113 118 column_name = options[:select] if options[:select] 114 119 column_name = '*' if column_name == :all 115 120 column = column_for column_name … … 150 155 options = options.symbolize_keys 151 156 152 157 scope = scope(:find) 158 if scope &&scope[:ar_joins] 159 scope = scope.dup 160 options = options.dup 161 options[:ar_joins] = scope.delete(:ar_joins) 162 end 153 163 merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) 164 merged_includes = merge_includes(merged_includes, options[:ar_joins]) 154 165 aggregate_alias = column_alias_for(operation, column_name) 155 166 156 167 if operation == 'count' … … 173 184 sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround 174 185 sql << " FROM #{table_name} " 175 186 if merged_includes.any? 176 join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins] )187 join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins], options[:ar_joins]) 177 188 sql << join_dependency.join_associations.collect{|join| join.association_join }.join 178 189 end 179 190 add_joins!(sql, options, scope) -
activerecord/lib/active_record/associations.rb
old new 486 486 # 487 487 # When eager loaded, conditions are interpolated in the context of the model class, not the model instance. Conditions are lazily interpolated 488 488 # before the actual model exists. 489 # 489 # 490 # == Adding Joins For Associations to Queries Using the :joins option 491 # 492 # ActiveRecord::Base#find provides a :joins option, which takes either a string or values accepted by the :include option. 493 # if the value is a string, then it should contain a SQL fragment containing a join clause. 494 # 495 # Non-string values of :joins will add an automatic join clause to the query in the same way that the :include option does but with two critical 496 # differences: 497 # 498 # 1. A normal (inner) join will be performed instead of the outer join generated by :include. 499 # this means that only objects which have objects attached to the association will be included in the result. 500 # For example, suppose we have the following tables (in yaml format): 501 # 502 # Authors 503 # fred: 504 # id: 1 505 # name: Fred 506 # steve: 507 # id: 2 508 # name: Steve 509 # 510 # Contributions 511 # only: 512 # id: 1 513 # author_id: 1 514 # description: Atta Boy Letter for Steve 515 # date: 2007-10-27 14:09:54 516 # 517 # and corresponding AR Classes 518 # 519 # class Author: < ActiveRecord::Base 520 # has_many :contributions 521 # end 522 # 523 # class Contribution < ActiveRecord::Base 524 # belongs_to :author 525 # end 526 # 527 # The query Author.find(:all) will return both authors, but Author.find(:all, :joins => :contributions) will 528 # only return authors who have at least one contribution, in this case only the first. 529 # This is only a degenerate case of the more typical use of :joins with a non-string value. 530 # For example to find authors who have at least one contribution before a certain date we can use: 531 # 532 # Author.find(:all, :joins => :contributions, :conditions => ["contributions.date <= ?", 1.week.ago.to_s(:db)]) 533 # 534 # 2. Only instances of the class to which the find is sent will be instantiated. ActiveRecord objects will not 535 # be instantiated for rows reached through the associations. 536 # 537 # The difference between using :joins vs :include to name associated records is that :joins allows associated tables to 538 # participate in selection criteria in the query without incurring the overhead of instantiating associated objects. 539 # This can be important when the number of associated objects in the database is large, and they will not be used, or 540 # only those associated with a paricular object or objects will be used after the query, making two queries more 541 # efficient than one. 542 # 543 # Note that while using a string value for :joins marks the result objects as read-only, the objects resulting 544 # from a call to find with a non-string :joins option value will be writable. 545 # 490 546 # == Table Aliasing 491 547 # 492 548 # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once, … … 1121 1177 1122 1178 def find_with_associations(options = {}) 1123 1179 catch :invalid_query do 1124 join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) 1180 if ar_joins = scope(:find, :ar_joins) 1181 options = options.dup 1182 options[:ar_joins] = ar_joins 1183 end 1184 includes = merge_includes(scope(:find, :include), options[:include]) 1185 includes = merge_includes(includes, options[:ar_joins]) 1186 join_dependency = JoinDependency.new(self, includes, options[:joins], options[:ar_joins]) 1125 1187 rows = select_all_rows(options, join_dependency) 1126 1188 return join_dependency.instantiate(rows) 1127 1189 end … … 1375 1437 class JoinDependency # :nodoc: 1376 1438 attr_reader :joins, :reflections, :table_aliases 1377 1439 1378 def initialize(base, associations, joins )1440 def initialize(base, associations, joins, ar_joins = nil) 1379 1441 @joins = [JoinBase.new(base, joins)] 1442 @ar_joins = ar_joins 1380 1443 @associations = associations 1381 1444 @reflections = [] 1382 1445 @base_records_hash = {} … … 1400 1463 unless @base_records_hash[primary_id] 1401 1464 @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) 1402 1465 end 1403 construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) 1466 construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) unless @ar_joins 1404 1467 end 1405 remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) 1468 remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) unless @ar_joins 1406 1469 return @base_records_in_order 1407 1470 end 1408 1471 … … 1444 1507 reflection = parent.reflections[associations.to_s.intern] or 1445 1508 raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" 1446 1509 @reflections << reflection 1447 @joins << JoinAssociation.new(reflection, self, parent)1510 @joins << (@ar_joins ? ARJoinAssociation : JoinAssociation).new(reflection, self, parent) 1448 1511 when Array 1449 1512 associations.each do |association| 1450 1513 build(association, parent) … … 1548 1611 end 1549 1612 end 1550 1613 1551 class JoinAssociation < JoinBase # :nodoc: 1614 class JoinAssociation < JoinBase # :nodoc: 1552 1615 attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name 1553 1616 delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection 1554 1617 … … 1591 1654 end 1592 1655 end 1593 1656 end 1594 1657 1595 1658 def association_join 1596 1659 join = case reflection.macro 1597 1660 when :has_and_belongs_to_many 1598 " LEFT OUTER JOIN%s ON %s.%s = %s.%s " % [1661 " #{join_type} %s ON %s.%s = %s.%s " % [ 1599 1662 table_alias_for(options[:join_table], aliased_join_table_name), 1600 1663 aliased_join_table_name, 1601 1664 options[:foreign_key] || reflection.active_record.to_s.foreign_key, 1602 1665 parent.aliased_table_name, reflection.active_record.primary_key] + 1603 " LEFT OUTER JOIN%s ON %s.%s = %s.%s " % [1666 " #{join_type} %s ON %s.%s = %s.%s " % [ 1604 1667 table_name_and_alias, aliased_table_name, klass.primary_key, 1605 1668 aliased_join_table_name, options[:association_foreign_key] || klass.to_s.foreign_key 1606 1669 ] … … 1658 1721 end 1659 1722 end 1660 1723 1661 " LEFT OUTER JOIN%s ON (%s.%s = %s.%s%s%s%s) " % [1724 " #{join_type} %s ON (%s.%s = %s.%s%s%s%s) " % [ 1662 1725 table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), 1663 1726 parent.aliased_table_name, reflection.active_record.connection.quote_column_name(parent.primary_key), 1664 1727 aliased_join_table_name, reflection.active_record.connection.quote_column_name(jt_foreign_key), 1665 1728 jt_as_extra, jt_source_extra, jt_sti_extra 1666 1729 ] + 1667 " LEFT OUTER JOIN%s ON (%s.%s = %s.%s%s) " % [1730 " #{join_type} %s ON (%s.%s = %s.%s%s) " % [ 1668 1731 table_name_and_alias, 1669 1732 aliased_table_name, reflection.active_record.connection.quote_column_name(first_key), 1670 1733 aliased_join_table_name, reflection.active_record.connection.quote_column_name(second_key), … … 1672 1735 ] 1673 1736 1674 1737 when reflection.options[:as] && [:has_many, :has_one].include?(reflection.macro) 1675 " LEFT OUTER JOIN%s ON %s.%s = %s.%s AND %s.%s = %s" % [1738 " #{join_type} %s ON %s.%s = %s.%s AND %s.%s = %s" % [ 1676 1739 table_name_and_alias, 1677 1740 aliased_table_name, "#{reflection.options[:as]}_id", 1678 1741 parent.aliased_table_name, parent.primary_key, … … 1681 1744 ] 1682 1745 else 1683 1746 foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key 1684 " LEFT OUTER JOIN%s ON %s.%s = %s.%s " % [1747 " #{join_type} %s ON %s.%s = %s.%s " % [ 1685 1748 table_name_and_alias, 1686 1749 aliased_table_name, foreign_key, 1687 1750 parent.aliased_table_name, parent.primary_key 1688 1751 ] 1689 1752 end 1690 1753 when :belongs_to 1691 " LEFT OUTER JOIN%s ON %s.%s = %s.%s " % [1754 " #{join_type} %s ON %s.%s = %s.%s " % [ 1692 1755 table_name_and_alias, aliased_table_name, reflection.klass.primary_key, 1693 1756 parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key 1694 1757 ] … … 1723 1786 1724 1787 def interpolate_sql(sql) 1725 1788 instance_eval("%@#{sql.gsub('@', '\@')}@") 1726 end 1789 end 1790 1791 private 1792 def join_type 1793 "LEFT OUTER JOIN" 1794 end 1795 1727 1796 end 1797 class ARJoinAssociation < JoinAssociation 1798 private 1799 def join_type 1800 "INNER JOIN" 1801 end 1802 end 1728 1803 end 1729 1804 end 1730 1805 end -
activerecord/lib/active_record/base.rb
old new 380 380 # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. 381 381 # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned. 382 382 # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4. 383 # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 384 # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. 383 # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 384 # or names associations in the same form used for the :include option. 385 # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. 385 386 # Pass :readonly => false to override. 387 # See adding joins for associations under Association. 386 388 # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 387 389 # to already defined associations. See eager loading under Associations. 388 390 # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not … … 428 430 # end 429 431 def find(*args) 430 432 options = args.extract_options! 433 # Note: we extract any :joins option with a non-string value from the options, and turn it into 434 # an internal option :ar_joins. This allows code called from her to find the ar_joins, and 435 # it bypasses marking the result as read_only. 436 # A normal string join marks the result as read-only because it contains attributes from joined tables 437 # which are not in the base table and therefore prevent the result from being saved. 438 # In the case of an ar_join, the JoinDependency created to instantiate the results eliminates these 439 # bogus attributes. See JoinDependency#instantiate, and JoinBase#instantiate in associations.rb. 440 options, ar_joins = *extract_ar_join_from_options(options) 431 441 validate_find_options(options) 432 442 set_readonly_option!(options) 443 options[:ar_joins] = ar_joins if ar_joins 433 444 434 445 case args.first 435 446 when :first then find_initial(options) … … 1019 1030 options.update(:limit => 1) unless options[:include] 1020 1031 find_every(options).first 1021 1032 end 1022 1033 1034 # If options contains :joins, with a non-string value 1035 # remove it from options 1036 # return the updated or unchanged options, and the ar_join value or nil 1037 def extract_ar_join_from_options(options) 1038 new_options = options.dup 1039 join_option = new_options.delete(:joins) 1040 (join_option && !join_option.kind_of?(String)) ? [new_options, join_option] : [options, nil] 1041 end 1042 1023 1043 def find_every(options) 1024 records = scoped?(:find, :include) || options[:include] ?1044 records = scoped?(:find, :include) || options[:include] || scoped?(:find, :ar_joins) || (options[:ar_joins]) ? 1025 1045 find_with_associations(options) : 1026 1046 find_by_sql(construct_finder_sql(options)) 1027 1047 … … 1445 1465 1446 1466 if f = method_scoping[:find] 1447 1467 f.assert_valid_keys(VALID_FIND_OPTIONS) 1468 # see note about :joins and :ar_joins in ActiveRecord::Base#find 1469 f, ar_joins = *extract_ar_join_from_options(f) 1448 1470 set_readonly_option! f 1471 if ar_joins 1472 f[:ar_joins] = ar_joins 1473 method_scoping[:find] = f 1474 end 1449 1475 end 1450 1476 1451 1477 # Merge scopings … … 1458 1484 merge = hash[method][key] && params[key] # merge if both scopes have the same key 1459 1485 if key == :conditions && merge 1460 1486 hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") 1461 elsif key == :include&& merge1487 elsif ([:include, :ar_joins].include?(key)) && merge 1462 1488 hash[method][key] = merge_includes(hash[method][key], params[key]).uniq 1463 1489 else 1464 1490 hash[method][key] = hash[method][key] || params[key] … … 1473 1499 hash 1474 1500 end 1475 1501 end 1476 1502 1477 1503 self.scoped_methods << method_scoping 1478 1504 1479 1505 begin