Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 8054

Show
Ignore:
Timestamp:
10/29/07 21:39:52 (1 year ago)
Author:
bitsweat
Message:

Introduce finder :joins with associations. Same :include syntax but with inner rather than outer joins. Closes #10012.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/CHANGELOG

    r8051 r8054  
    11*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') 
    29 
    310* Associations: speedup duplicate record check.  #10011 [lifofifo] 
  • trunk/activerecord/lib/active_record/associations.rb

    r8051 r8054  
    487487    # When eager loaded, conditions are interpolated in the context of the model class, not the model instance.  Conditions are lazily interpolated 
    488488    # 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    # 
    490546    # == Table Aliasing 
    491547    # 
     
    11221178        def find_with_associations(options = {}) 
    11231179          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]) 
    11251187            rows = select_all_rows(options, join_dependency) 
    11261188            return join_dependency.instantiate(rows) 
     
    13761438          attr_reader :joins, :reflections, :table_aliases 
    13771439 
    1378           def initialize(base, associations, joins
     1440          def initialize(base, associations, joins, ar_joins = nil
    13791441            @joins                 = [JoinBase.new(base, joins)] 
     1442            @ar_joins              = ar_joins 
    13801443            @associations          = associations 
    13811444            @reflections           = [] 
     
    14011464                @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) 
    14021465              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 
    14061469            return @base_records_in_order 
    14071470          end 
     
    14451508                  raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" 
    14461509                  @reflections << reflection 
    1447                   @joins << JoinAssociation.new(reflection, self, parent) 
     1510                  @joins << (@ar_joins ? ARJoinAssociation : JoinAssociation).new(reflection, self, parent) 
    14481511                when Array 
    14491512                  associations.each do |association| 
     
    15961659              join = case reflection.macro 
    15971660                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 " % [ 
    15991662                     table_alias_for(options[:join_table], aliased_join_table_name), 
    16001663                     aliased_join_table_name, 
    16011664                     options[:foreign_key] || reflection.active_record.to_s.foreign_key, 
    16021665                     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 " % [ 
    16041667                     table_name_and_alias, aliased_table_name, klass.primary_key, 
    16051668                     aliased_join_table_name, options[:association_foreign_key] || klass.to_s.foreign_key 
     
    16591722                      end 
    16601723 
    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) " % [ 
    16621725                        table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), 
    16631726                        parent.aliased_table_name, reflection.active_record.connection.quote_column_name(parent.primary_key), 
     
    16651728                        jt_as_extra, jt_source_extra, jt_sti_extra 
    16661729                      ] + 
    1667                       " LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s) " % [ 
     1730                      " #{join_type} %s ON (%s.%s = %s.%s%s) " % [ 
    16681731                        table_name_and_alias,  
    16691732                        aliased_table_name, reflection.active_record.connection.quote_column_name(first_key), 
     
    16731736 
    16741737                    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" % [ 
    16761739                        table_name_and_alias, 
    16771740                        aliased_table_name, "#{reflection.options[:as]}_id", 
     
    16821745                    else 
    16831746                      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 " % [ 
    16851748                        table_name_and_alias, 
    16861749                        aliased_table_name, foreign_key, 
     
    16891752                  end 
    16901753                when :belongs_to 
    1691                   " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ 
     1754                  " #{join_type} %s ON %s.%s = %s.%s " % [ 
    16921755                     table_name_and_alias, aliased_table_name, reflection.klass.primary_key, 
    16931756                     parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key 
     
    17241787              def interpolate_sql(sql) 
    17251788                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 
    17271802          end 
    17281803        end 
  • trunk/activerecord/lib/active_record/base.rb

    r8032 r8054  
    381381      # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned. 
    382382      # * <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. 
    385386      #   Pass :readonly => false to override. 
     387      #   See adding joins for associations under Association. 
    386388      # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 
    387389      #   to already defined associations. See eager loading under Associations. 
     
    429431      def find(*args) 
    430432        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) 
    431441        validate_find_options(options) 
    432442        set_readonly_option!(options) 
     443        options[:ar_joins] = ar_joins if ar_joins 
    433444 
    434445        case args.first 
     
    10211032        end 
    10221033 
     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 
    10231043        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])
    10251045            find_with_associations(options) :  
    10261046            find_by_sql(construct_finder_sql(options)) 
     
    14461466          if f = method_scoping[:find] 
    14471467            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) 
    14481470            set_readonly_option! f 
     1471            if ar_joins 
     1472              f[:ar_joins] = ar_joins 
     1473              method_scoping[:find] = f 
     1474            end 
    14491475          end 
    14501476 
     
    14591485                      if key == :conditions && merge 
    14601486                        hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") 
    1461                       elsif key == :include && merge 
     1487                      elsif ([:include, :ar_joins].include?(key)) && merge 
    14621488                        hash[method][key] = merge_includes(hash[method][key], params[key]).uniq 
    14631489                      else 
  • trunk/activerecord/lib/active_record/calculations.rb

    r7192 r8054  
    1616      # 
    1717      # * <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. 
    2023      # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 
    2124      #   to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting. 
     
    110113      #   Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors 
    111114      def calculate(operation, column_name, options = {}) 
     115        options, ar_joins = *extract_ar_join_from_options(options) 
    112116        validate_calculation_options(operation, options) 
     117        options[:ar_joins] = ar_joins if ar_joins 
    113118        column_name     = options[:select] if options[:select] 
    114119        column_name     = '*' if column_name == :all 
     
    150155          options = options.symbolize_keys 
    151156 
    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 
    153163          merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) 
     164          merged_includes = merge_includes(merged_includes, options[:ar_joins]) 
    154165          aggregate_alias = column_alias_for(operation, column_name) 
    155166 
     
    174185          sql << " FROM #{table_name} " 
    175186          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]
    177188            sql << join_dependency.join_associations.collect{|join| join.association_join }.join 
    178189          end