Changeset 8054
- Timestamp:
- 10/29/07 21:39:52 (10 months ago)
- Files:
-
- trunk/activerecord/CHANGELOG (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations.rb (modified) (12 diffs)
- trunk/activerecord/lib/active_record/base.rb (modified) (5 diffs)
- trunk/activerecord/lib/active_record/calculations.rb (modified) (4 diffs)
- trunk/activerecord/test/associations/ar_joins_test.rb (added)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/CHANGELOG
r8051 r8054 1 1 *SVN* 2 3 * Introduce finder :joins with associations. Same :include syntax but with inner rather than outer joins. #10012 [RubyRedRick] 4 # Find users with an avatar 5 User.find(:all, :joins => :avatar) 6 7 # Find posts with a high-rated comment. 8 Post.find(:all, :joins => :comments, :conditions => 'comments.rating > 3') 2 9 3 10 * Associations: speedup duplicate record check. #10011 [lifofifo] trunk/activerecord/lib/active_record/associations.rb
r8051 r8054 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, 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 # 490 546 # == Table Aliasing 491 547 # … … 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) … … 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 = [] … … 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) 1404 end 1405 remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) 1466 construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) unless @ar_joins 1467 end 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 … … 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| … … 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 … … 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), … … 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), … … 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", … … 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, … … 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 … … 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 1796 end 1797 class ARJoinAssociation < JoinAssociation 1798 private 1799 def join_type 1800 "INNER JOIN" 1801 end 1727 1802 end 1728 1803 end trunk/activerecord/lib/active_record/base.rb
r8032 r8054 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. … … 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 … … 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)) … … 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 … … 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 trunk/activerecord/lib/active_record/calculations.rb
r7192 r8054 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. … … 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 … … 150 155 options = options.symbolize_keys 151 156 152 scope = scope(:find) 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 … … 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