Ticket #10061: correct_joins_with_tests.diff
| File correct_joins_with_tests.diff, 27.4 kB (added by protocool, 8 months 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::TestCase14 fixtures :authors, :posts, :comments, :categories, :categories_posts, :people,15 :developers, :projects, :developers_projects,16 :categorizations, :companies, :accounts, :topics17 18 def test_ar_joins19 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 end25 end26 27 def test_ar_joins_with_cascaded_two_levels28 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 end34 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 end40 end41 42 43 def test_ar_joins_with_complex_conditions44 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 end52 assert_equal("David", authors.first.name)53 end54 55 def test_ar_join_with_has_many_and_limit_and_scoped_and_explicit_conditions56 Post.with_scope(:find => { :conditions => "1=1" }) do57 posts = authors(:david).posts.find(:all,58 :joins => :comments,59 :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",60 :limit => 261 )62 assert_equal 2, posts.size63 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 => 268 )69 assert_equal count, posts.size70 end71 end72 73 def test_ar_join_with_scoped_order_using_association_limiting_without_explicit_scope74 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'}) do76 Post.find(:all, :conditions => 'comments.id is not null', :joins => :comments, :limit => 2)77 end78 assert_equal posts_with_explicit_order, posts_with_scoped_order79 end80 81 def test_scoped_find_include82 # with the include, will retrieve only developers for the given project83 scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do84 Developer.find(:all, :conditions => 'projects.id = 2')85 end86 assert scoped_developers.include?(developers(:david))87 assert !scoped_developers.include?(developers(:jamis))88 assert_equal 1, scoped_developers.size89 end90 91 92 def test_nested_scoped_find_ar_join93 Developer.with_scope(:find => { :joins => :projects }) do94 Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do95 assert_equal('David', Developer.find(:first).name)96 end97 end98 end99 100 def test_nested_scoped_find_merged_ar_join101 # :include's remain unique and don't "double up" when merging102 Developer.with_scope(:find => { :joins => :projects, :conditions => "projects.id = 2" }) do103 Developer.with_scope(:find => { :joins => :projects }) do104 assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length105 assert_equal('David', Developer.find(:first).name)106 end107 end108 # the nested scope doesn't remove the first :include109 Developer.with_scope(:find => { :joins => :projects, :conditions => "projects.id = 2" }) do110 Developer.with_scope(:find => { :joins => [] }) do111 assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length112 assert_equal('David', Developer.find(:first).name)113 end114 end115 # mixing array and symbol include's will merge correctly116 Developer.with_scope(:find => { :joins => [:projects], :conditions => "projects.id = 2" }) do117 Developer.with_scope(:find => { :joins => :projects }) do118 assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length119 assert_equal('David', Developer.find(:first).name)120 end121 end122 end123 124 def test_nested_scoped_find_replace_include125 Developer.with_scope(:find => { :joins => :projects }) do126 Developer.with_exclusive_scope(:find => { :joins => [] }) do127 assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length128 end129 end130 end131 132 #133 # Calculations134 #135 def test_count_with_ar_joins136 assert_equal(2, Author.count(:joins => :posts, :conditions => ['posts.type = ?', "Post"]))137 assert_equal(1, Author.count(:joins => :posts, :conditions => ['posts.type = ?', "SpecialPost"]))138 end139 140 def test_should_get_maximum_of_field_with_joins141 assert_equal 50, Account.maximum(:credit_limit, :joins=> :firm, :conditions => "companies.name != 'Summit'")142 end143 144 def test_should_get_maximum_of_field_with_scoped_include145 Account.with_scope :find => { :joins => :firm, :conditions => "companies.name != 'Summit'" } do146 assert_equal 50, Account.maximum(:credit_limit)147 end148 end149 150 def test_should_not_modify_options_when_using_ar_joins_on_count151 options = {:conditions => 'companies.id > 1', :joins => :firm}152 options_copy = options.dup153 154 Account.count(:all, options)155 assert_equal options_copy, options156 end157 158 end -
activerecord/test/associations/inner_join_association_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 8 class InnerJoinAssociationTest < Test::Unit::TestCase 9 fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations 10 11 def test_construct_finder_sql_creates_inner_joins 12 sql = Author.send(:construct_finder_sql, :joins => :posts) 13 assert_match /INNER JOIN posts ON posts.author_id = authors.id/, sql 14 end 15 16 def test_construct_finder_sql_cascades_inner_joins 17 sql = Author.send(:construct_finder_sql, :joins => {:posts => :comments}) 18 assert_match /INNER JOIN posts ON posts.author_id = authors.id/, sql 19 assert_match /INNER JOIN comments ON comments.post_id = posts.id/, sql 20 end 21 22 def test_construct_finder_sql_inner_joins_through_associations 23 sql = Author.send(:construct_finder_sql, :joins => :categorized_posts) 24 assert_match /INNER JOIN categorizations.*INNER JOIN posts/, sql 25 end 26 27 def test_construct_finder_sql_applies_association_conditions 28 sql = Author.send(:construct_finder_sql, :joins => :categories_like_general, :conditions => "TERMINATING_MARKER") 29 assert_match /INNER JOIN categories ON.*AND.*'General'.*TERMINATING_MARKER/, sql 30 end 31 32 def test_construct_finder_sql_unpacks_nested_joins 33 sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]}) 34 assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present" 35 assert_match /INNER JOIN posts ON posts.author_id = authors.id/, sql 36 assert_match /INNER JOIN comments ON comments.post_id = posts.id/, sql 37 end 38 39 def test_construct_finder_sql_ignores_empty_joins_hash 40 sql = Author.send(:construct_finder_sql, :joins => {}) 41 assert_no_match /JOIN/i, sql 42 end 43 44 def test_construct_finder_sql_ignores_empty_joins_array 45 sql = Author.send(:construct_finder_sql, :joins => []) 46 assert_no_match /JOIN/i, sql 47 end 48 49 def test_find_with_implicit_inner_joins_honors_readonly_without_select 50 authors = Author.find(:all, :joins => :posts) 51 assert !authors.empty?, "expected authors to be non-empty" 52 assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly" 53 end 54 55 def test_find_with_implicit_inner_joins_honors_readonly_with_select 56 authors = Author.find(:all, :select => 'authors.*', :joins => :posts) 57 assert !authors.empty?, "expected authors to be non-empty" 58 assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly" 59 end 60 61 def test_find_with_implicit_inner_joins_honors_readonly_false 62 authors = Author.find(:all, :joins => :posts, :readonly => false) 63 assert !authors.empty?, "expected authors to be non-empty" 64 assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly" 65 end 66 67 def test_find_with_implicit_inner_joins_does_not_set_associations 68 authors = Author.find(:all, :select => 'authors.*', :joins => :posts) 69 assert !authors.empty?, "expected authors to be non-empty" 70 assert authors.all? {|a| !a.send(:instance_variables).include?("@posts")}, "expected no authors to have the @posts association loaded" 71 end 72 73 def test_count_honors_implicit_inner_joins 74 real_count = Author.find(:all).sum{|a| a.posts.count } 75 assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records" 76 end 77 78 def test_calculate_honors_implicit_inner_joins 79 real_count = Author.find(:all).sum{|a| a.posts.count } 80 assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records" 81 end 82 83 def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions 84 real_count = Author.find(:all).select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length 85 authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'") 86 assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" 87 end 88 end -
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 # 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. 386 386 # Pass :readonly => false to override. 387 # See adding joins for associations under Association.388 387 # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 389 388 # to already defined associations. See eager loading under Associations. 390 389 # * <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 429 # end 431 430 def find(*args) 432 431 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 432 validate_find_options(options) 442 433 set_readonly_option!(options) 443 options[:ar_joins] = ar_joins if ar_joins444 434 445 435 case args.first 446 436 when :first then find_initial(options) … … 1038 1028 find_every(options).first 1039 1029 end 1040 1030 1041 # If options contains :joins, with a non-string value1042 # remove it from options1043 # return the updated or unchanged options, and the ar_join value or nil1044 def extract_ar_join_from_options(options)1045 new_options = options.dup1046 join_option = new_options.delete(:joins)1047 (join_option && !join_option.kind_of?(String)) ? [new_options, join_option] : [options, nil]1048 end1049 1050 1031 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] ? 1052 1033 find_with_associations(options) : 1053 1034 find_by_sql(construct_finder_sql(options)) 1054 1035 … … 1246 1227 def add_joins!(sql, options, scope = :auto) 1247 1228 scope = scope(:find) if :auto == scope 1248 1229 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 1250 1237 end 1251 1238 1252 1239 # Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed. … … 1472 1459 1473 1460 if f = method_scoping[:find] 1474 1461 f.assert_valid_keys(VALID_FIND_OPTIONS) 1475 # see note about :joins and :ar_joins in ActiveRecord::Base#find1476 f, ar_joins = *extract_ar_join_from_options(f)1477 1462 set_readonly_option! f 1478 if ar_joins1479 f[:ar_joins] = ar_joins1480 method_scoping[:find] = f1481 end1482 1463 end 1483 1464 1484 1465 # Merge scopings … … 1491 1472 merge = hash[method][key] && params[key] # merge if both scopes have the same key 1492 1473 if key == :conditions && merge 1493 1474 hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") 1494 elsif ([:include, :ar_joins].include?(key))&& merge1475 elsif key == :include && merge 1495 1476 hash[method][key] = merge_includes(hash[method][key], params[key]).uniq 1496 1477 else 1497 1478 hash[method][key] = hash[method][key] || params[key]