Ticket #10061: correct_ar_joins.diff
| File correct_ar_joins.diff, 16.1 kB (added by protocool, 10 months ago) |
|---|
-
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>: 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. 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. 23 20 # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 24 21 # to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting. 25 22 # See eager loading under Associations. … … 112 109 # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' 113 110 # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors 114 111 def calculate(operation, column_name, options = {}) 115 options, ar_joins = *extract_ar_join_from_options(options)116 112 validate_calculation_options(operation, options) 117 options[:ar_joins] = ar_joins if ar_joins118 113 column_name = options[:select] if options[:select] 119 114 column_name = '*' if column_name == :all 120 115 column = column_for column_name … … 154 149 operation = operation.to_s.downcase 155 150 options = options.symbolize_keys 156 151 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 152 scope = scope(:find) 163 153 merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) 164 merged_includes = merge_includes(merged_includes, options[:ar_joins])165 154 aggregate_alias = column_alias_for(operation, column_name) 166 155 167 156 if operation == 'count' … … 184 173 sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround 185 174 sql << " FROM #{connection.quote_table_name(table_name)} " 186 175 if merged_includes.any? 187 join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins] , options[:ar_joins])176 join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins]) 188 177 sql << join_dependency.join_associations.collect{|join| join.association_join }.join 189 178 end 190 179 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 # 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, the 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 # 489 # 546 490 # == Table Aliasing 547 491 # 548 492 # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once, … … 1177 1121 1178 1122 def find_with_associations(options = {}) 1179 1123 catch :invalid_query do 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]) 1124 join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) 1187 1125 rows = select_all_rows(options, join_dependency) 1188 1126 return join_dependency.instantiate(rows) 1189 1127 end … … 1437 1375 class JoinDependency # :nodoc: 1438 1376 attr_reader :joins, :reflections, :table_aliases 1439 1377 1440 def initialize(base, associations, joins , ar_joins = nil)1378 def initialize(base, associations, joins) 1441 1379 @joins = [JoinBase.new(base, joins)] 1442 @ar_joins = ar_joins1443 1380 @associations = associations 1444 1381 @reflections = [] 1445 1382 @base_records_hash = {} … … 1463 1400 unless @base_records_hash[primary_id] 1464 1401 @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) 1465 1402 end 1466 construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) unless @ar_joins1403 construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) 1467 1404 end 1468 remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) unless @ar_joins1405 remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) 1469 1406 return @base_records_in_order 1470 1407 end 1471 1408 … … 1507 1444 reflection = parent.reflections[associations.to_s.intern] or 1508 1445 raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" 1509 1446 @reflections << reflection 1510 @joins << (@ar_joins ? ARJoinAssociation : JoinAssociation).new(reflection, self, parent)1447 @joins << build_join_association(reflection, parent) 1511 1448 when Array 1512 1449 associations.each do |association| 1513 1450 build(association, parent) … … 1522 1459 end 1523 1460 end 1524 1461 1462 # overridden in InnerJoinDependency subclass 1463 def build_join_association(reflection, parent) 1464 JoinAssociation.new(reflection, self, parent) 1465 end 1466 1525 1467 def construct(parent, associations, joins, row) 1526 1468 case associations 1527 1469 when Symbol, String … … 1786 1728 1787 1729 def interpolate_sql(sql) 1788 1730 instance_eval("%@#{sql.gsub('@', '\@')}@") 1789 end 1731 end 1790 1732 1791 private 1733 private 1734 1792 1735 def join_type 1793 1736 "LEFT OUTER JOIN" 1794 1737 end 1795 1796 1738 end 1797 class ARJoinAssociation < JoinAssociation 1739 end 1740 1741 class InnerJoinDependency < JoinDependency # :nodoc: 1742 protected 1743 def build_join_association(reflection, parent) 1744 InnerJoinAssociation.new(reflection, self, parent) 1745 end 1746 1747 class InnerJoinAssociation < JoinAssociation 1798 1748 private 1799 1749 def join_type 1800 1750 "INNER JOIN" 1801 1751 end 1802 1752 end 1803 1753 end 1754 1804 1755 end 1805 1756 end 1806 1757 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>: 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. 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. 386 385 # Pass :readonly => false to override. 387 # See adding joins for associations under Association.388 386 # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 389 387 # to already defined associations. See eager loading under Associations. 390 388 # * <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 … … 430 428 # end 431 429 def find(*args) 432 430 options = args.extract_options! 433 # Note: we extract any :joins option with a non-string value from the options, and turn it into434 # an internal option :ar_joins. This allows code called from her to find the ar_joins, and435 # 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 tables437 # 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 these439 # bogus attributes. See JoinDependency#instantiate, and JoinBase#instantiate in associations.rb.440 options, ar_joins = *extract_ar_join_from_options(options)441 431 validate_find_options(options) 442 432 set_readonly_option!(options) 443 options[:ar_joins] = ar_joins if ar_joins444 433 445 434 case args.first 446 435 when :first then find_initial(options) … … 1031 1020 find_every(options).first 1032 1021 end 1033 1022 1034 # If options contains :joins, with a non-string value1035 # remove it from options1036 # return the updated or unchanged options, and the ar_join value or nil1037 def extract_ar_join_from_options(options)1038 new_options = options.dup1039 join_option = new_options.delete(:joins)1040 (join_option && !join_option.kind_of?(String)) ? [new_options, join_option] : [options, nil]1041 end1042 1043 1023 def find_every(options) 1044 records = scoped?(:find, :include) || options[:include] || scoped?(:find, :ar_joins) || (options[:ar_joins])?1024 records = scoped?(:find, :include) || options[:include] ? 1045 1025 find_with_associations(options) : 1046 1026 find_by_sql(construct_finder_sql(options)) 1047 1027 … … 1239 1219 def add_joins!(sql, options, scope = :auto) 1240 1220 scope = scope(:find) if :auto == scope 1241 1221 join = (scope && scope[:joins]) || options[:joins] 1242 sql << " #{join} " if join 1222 case join 1223 when Symbol, Hash, Array 1224 join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil) 1225 sql << " #{join_dependency.join_associations.collect{|join| join.association_join }.join} " 1226 else 1227 sql << " #{join} " 1228 end 1243 1229 end 1244 1230 1245 1231 # Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed. … … 1465 1451 1466 1452 if f = method_scoping[:find] 1467 1453 f.assert_valid_keys(VALID_FIND_OPTIONS) 1468 # see note about :joins and :ar_joins in ActiveRecord::Base#find1469 f, ar_joins = *extract_ar_join_from_options(f)1470 1454 set_readonly_option! f 1471 if ar_joins1472 f[:ar_joins] = ar_joins1473 method_scoping[:find] = f1474 end1475 1455 end 1476 1456 1477 1457 # Merge scopings … … 1484 1464 merge = hash[method][key] && params[key] # merge if both scopes have the same key 1485 1465 if key == :conditions && merge 1486 1466 hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") 1487 elsif ([:include, :ar_joins].include?(key))&& merge1467 elsif key == :include && merge 1488 1468 hash[method][key] = merge_includes(hash[method][key], params[key]).uniq 1489 1469 else 1490 1470 hash[method][key] = hash[method][key] || params[key]