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

Changeset 8109

Show
Ignore:
Timestamp:
11/07/07 15:07:39 (6 months ago)
Author:
david
Message:

Address shortcomings of changeset [8054] [protocool]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/lib/active_record/associations.rb

    r8102 r8109  
    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     # 
    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    #  
    546490    # == Table Aliasing 
    547491    # 
     
    11781122        def find_with_associations(options = {}) 
    11791123          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]) 
    11871125            rows = select_all_rows(options, join_dependency) 
    11881126            return join_dependency.instantiate(rows) 
     
    14381376          attr_reader :joins, :reflections, :table_aliases 
    14391377 
    1440           def initialize(base, associations, joins, ar_joins = nil
     1378          def initialize(base, associations, joins
    14411379            @joins                 = [JoinBase.new(base, joins)] 
    1442             @ar_joins              = ar_joins 
    14431380            @associations          = associations 
    14441381            @reflections           = [] 
     
    14641401                @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) 
    14651402              end 
    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 
     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) 
    14691406            return @base_records_in_order 
    14701407          end 
     
    15081445                  raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" 
    15091446                  @reflections << reflection 
    1510                   @joins << (@ar_joins ? ARJoinAssociation : JoinAssociation).new(reflection, self, parent) 
     1447                  @joins << build_join_association(reflection, parent) 
    15111448                when Array 
    15121449                  associations.each do |association| 
     
    15211458                  raise ConfigurationError, associations.inspect 
    15221459              end 
     1460            end 
     1461 
     1462            # overridden in InnerJoinDependency subclass 
     1463            def build_join_association(reflection, parent) 
     1464              JoinAssociation.new(reflection, self, parent) 
    15231465            end 
    15241466 
     
    17871729              def interpolate_sql(sql) 
    17881730                instance_eval("%@#{sql.gsub('@', '\@')}@")  
    1789               end 
    1790  
    1791            private 
     1731              end  
     1732 
     1733            private 
     1734 
    17921735              def join_type 
    17931736                "LEFT OUTER JOIN" 
    17941737              end 
    1795  
    1796           end 
    1797           class ARJoinAssociation < JoinAssociation 
     1738          end 
     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 
    17981748            private 
    17991749              def join_type 
     
    18021752          end 
    18031753        end 
     1754 
    18041755    end 
    18051756  end 
  • trunk/activerecord/lib/active_record/base.rb

    r8107 r8109  
    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>: 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      #   Accepts named associations in the form of :include, which will perform an INNER JOIN on the associated table(s)
     385      #   The records will be returned read-only since they will have attributes that do not correspond to the table's columns. 
    386386      #   Pass :readonly => false to override. 
    387       #   See adding joins for associations under Association. 
    388387      # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 
    389388      #   to already defined associations. See eager loading under Associations. 
     
    431430      def find(*args) 
    432431        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) 
    441432        validate_find_options(options) 
    442433        set_readonly_option!(options) 
    443         options[:ar_joins] = ar_joins if ar_joins 
    444434 
    445435        case args.first 
     
    10391029        end 
    10401030 
    1041         # If options contains :joins, with a non-string value 
    1042         #  remove it from options 
    1043         # return the updated or unchanged options, and the ar_join value or nil 
    1044         def extract_ar_join_from_options(options) 
    1045           new_options = options.dup 
    1046           join_option = new_options.delete(:joins) 
    1047           (join_option && !join_option.kind_of?(String)) ? [new_options, join_option] : [options, nil] 
    1048         end 
    1049  
    10501031        def find_every(options) 
    1051           records = scoped?(:find, :include) || options[:include] || scoped?(:find, :ar_joins) || (options[:ar_joins])
     1032          records = scoped?(:find, :include) || options[:include]
    10521033            find_with_associations(options) :  
    10531034            find_by_sql(construct_finder_sql(options)) 
     
    12471228          scope = scope(:find) if :auto == scope 
    12481229          join = (scope && scope[:joins]) || options[:joins] 
    1249           sql << " #{join} " if join 
     1230          case join 
     1231          when Symbol, Hash, Array 
     1232            join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil) 
     1233            sql << " #{join_dependency.join_associations.collect{|join| join.association_join }.join} " 
     1234          else 
     1235            sql << " #{join} " 
     1236          end 
    12501237        end 
    12511238 
     
    14731460          if f = method_scoping[:find] 
    14741461            f.assert_valid_keys(VALID_FIND_OPTIONS) 
    1475             # see note about :joins and :ar_joins in ActiveRecord::Base#find 
    1476             f, ar_joins = *extract_ar_join_from_options(f) 
    14771462            set_readonly_option! f 
    1478             if ar_joins 
    1479               f[:ar_joins] = ar_joins 
    1480               method_scoping[:find] = f 
    1481             end 
    14821463          end 
    14831464 
     
    14921473                      if key == :conditions && merge 
    14931474                        hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") 
    1494                       elsif ([:include, :ar_joins].include?(key)) && merge 
     1475                      elsif key == :include && merge 
    14951476                        hash[method][key] = merge_includes(hash[method][key], params[key]).uniq 
    14961477                      else 
  • trunk/activerecord/lib/active_record/calculations.rb

    r8106 r8109  
    1616      # 
    1717      # * <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. 
    2320      # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 
    2421      #   to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting. 
     
    113110      #   Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors 
    114111      def calculate(operation, column_name, options = {}) 
    115         options, ar_joins = *extract_ar_join_from_options(options) 
    116112        validate_calculation_options(operation, options) 
    117         options[:ar_joins] = ar_joins if ar_joins 
    118113        column_name     = options[:select] if options[:select] 
    119114        column_name     = '*' if column_name == :all 
     
    155150          options = options.symbolize_keys 
    156151 
    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) 
    163153          merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) 
    164           merged_includes = merge_includes(merged_includes, options[:ar_joins]) 
    165154          aggregate_alias = column_alias_for(operation, column_name) 
    166155 
     
    185174          sql << " FROM #{connection.quote_table_name(table_name)} " 
    186175          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]
    188177            sql << join_dependency.join_associations.collect{|join| join.association_join }.join 
    189178          end 
  • trunk/activerecord/test/associations/ar_joins_test.rb

    r8054 r8109  
    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