Skip to content
This repository
Browse code

Accept belongs_to assoc. keys in ActiveRecord queries

Allows you to specify the model association key in a belongs_to
relationship instead of the foreign key.

The following queries are now equivalent:

Post.where(:author_id => Author.first)
Post.where(:author => Author.first)

PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
PriceEstimate.where(:estimate_of => treasure)
  • Loading branch information...
commit 3da275c4396d7fad250d2b786027ba4f14344bd4 1 parent f2a44ad
Peter Brown authored September 11, 2012
12  activerecord/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,17 @@
1 1
 ## Rails 4.0.0 (unreleased) ##
2 2
 
  3
+*   Accept belongs_to (including polymorphic) association keys in queries
  4
+
  5
+    The following queries are now equivalent:
  6
+
  7
+        Post.where(:author => author)
  8
+        Post.where(:author_id => author)
  9
+
  10
+        PriceEstimate.where(:estimate_of => treasure)
  11
+        PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
  12
+
  13
+    *Peter Brown*
  14
+
3 15
 *   Use native `mysqldump` command instead of `structure_dump` method
4 16
     when dumping the database structure to a sql file. Fixes #5547.
5 17
 
52  activerecord/lib/active_record/relation/predicate_builder.rb
... ...
@@ -1,12 +1,25 @@
1 1
 module ActiveRecord
2 2
   class PredicateBuilder # :nodoc:
3 3
     def self.build_from_hash(engine, attributes, default_table)
4  
-      attributes.map do |column, value|
  4
+      queries = []
  5
+
  6
+      attributes.each do |column, value|
5 7
         table = default_table
6 8
 
7 9
         if value.is_a?(Hash)
8 10
           table = Arel::Table.new(column, engine)
9  
-          value.map { |k,v| build(table[k.to_sym], v) }
  11
+
  12
+          value.each do |k,v|
  13
+            if rk = find_reflection_key(column, v, v)
  14
+              if rk[:foreign_type]
  15
+                queries << build(table[rk[:foreign_type]], v.class.base_class)
  16
+              end
  17
+
  18
+              k = rk[:foreign_key]
  19
+            end
  20
+
  21
+            queries << build(table[k.to_sym], v)
  22
+          end
10 23
         else
11 24
           column = column.to_s
12 25
 
@@ -15,9 +28,19 @@ def self.build_from_hash(engine, attributes, default_table)
15 28
             table = Arel::Table.new(table_name, engine)
16 29
           end
17 30
 
18  
-          build(table[column.to_sym], value)
  31
+          if rk = find_reflection_key(column, engine, value)
  32
+            if rk[:foreign_type]
  33
+              queries << build(table[rk[:foreign_type]], value.class.base_class)
  34
+            end
  35
+
  36
+            column = rk[:foreign_key]
  37
+          end
  38
+
  39
+          queries << build(table[column.to_sym], value)
19 40
         end
20  
-      end.flatten
  41
+      end
  42
+
  43
+      queries
21 44
     end
22 45
 
23 46
     def self.references(attributes)
@@ -31,6 +54,27 @@ def self.references(attributes)
31 54
       end.compact
32 55
     end
33 56
 
  57
+    # Find the foreign key when using queries such as:
  58
+    # Post.where(:author => author)
  59
+    #
  60
+    # For polymorphic relationships, find the foreign key and type:
  61
+    # PriceEstimate.where(:estimate_of => treasure)
  62
+    def self.find_reflection_key(parent_column, model, value)
  63
+      # value must be an ActiveRecord object
  64
+      return nil unless value.class < Model::Tag
  65
+
  66
+      if reflection = model.reflections[parent_column.to_sym]
  67
+        if reflection.options[:polymorphic]
  68
+          {
  69
+            :foreign_key  => reflection.foreign_key,
  70
+            :foreign_type => reflection.foreign_type
  71
+          }
  72
+        else
  73
+          { :foreign_key => reflection.foreign_key }
  74
+        end
  75
+      end
  76
+    end
  77
+
34 78
     private
35 79
       def self.build(attribute, value)
36 80
         case value
18  activerecord/lib/active_record/relation/query_methods.rb
@@ -340,6 +340,24 @@ def bind!(value)
340 340
     #    User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
341 341
     #    # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
342 342
     #
  343
+    # In the case of a belongs_to relationship, an association key can be used
  344
+    # to specify the model if an ActiveRecord object is used as the value.
  345
+    #
  346
+    #    author = Author.find(1)
  347
+    #
  348
+    #    # The following queries will be equivalent:
  349
+    #    Post.where(:author => author)
  350
+    #    Post.where(:author_id => author)
  351
+    #
  352
+    # This also works with polymorphic belongs_to relationships:
  353
+    #
  354
+    #    treasure = Treasure.create(:name => 'gold coins')
  355
+    #    treasure.price_estimates << PriceEstimate.create(:price => 125)
  356
+    #
  357
+    #    # The following queries will be equivalent:
  358
+    #    PriceEstimate.where(:estimate_of => treasure)
  359
+    #    PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
  360
+    #
343 361
     # === Joins
344 362
     #
345 363
     # If the relation is the result of a join, you may create a condition which uses any of the
70  activerecord/test/cases/relation/where_test.rb
... ...
@@ -1,9 +1,77 @@
1 1
 require "cases/helper"
  2
+require 'models/author'
  3
+require 'models/price_estimate'
  4
+require 'models/treasure'
2 5
 require 'models/post'
3 6
 
4 7
 module ActiveRecord
5 8
   class WhereTest < ActiveRecord::TestCase
6  
-    fixtures :posts
  9
+    fixtures :posts, :authors
  10
+
  11
+    def test_belongs_to_shallow_where
  12
+      author = Post.first.author
  13
+      query_with_id = Post.where(:author_id => author)
  14
+      query_with_assoc = Post.where(:author => author)
  15
+
  16
+      assert_equal query_with_id.to_sql, query_with_assoc.to_sql
  17
+    end
  18
+
  19
+    def test_belongs_to_nested_where
  20
+      author = Post.first.author
  21
+      query_with_id = Author.where(:posts => {:author_id => author}).joins(:posts)
  22
+      query_with_assoc = Author.where(:posts => {:author => author}).joins(:posts)
  23
+
  24
+      assert_equal query_with_id.to_sql, query_with_assoc.to_sql
  25
+    end
  26
+
  27
+    def test_polymorphic_shallow_where
  28
+      treasure = Treasure.create(:name => 'gold coins')
  29
+      treasure.price_estimates << PriceEstimate.create(:price => 125)
  30
+
  31
+      query_by_column = PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
  32
+      query_by_model = PriceEstimate.where(:estimate_of => treasure)
  33
+
  34
+      assert_equal query_by_column.to_sql, query_by_model.to_sql
  35
+    end
  36
+
  37
+    def test_polymorphic_sti_shallow_where
  38
+      treasure = HiddenTreasure.create!(:name => 'gold coins')
  39
+      treasure.price_estimates << PriceEstimate.create!(:price => 125)
  40
+
  41
+      query_by_column = PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
  42
+      query_by_model = PriceEstimate.where(:estimate_of => treasure)
  43
+
  44
+      assert_equal query_by_column.to_sql, query_by_model.to_sql
  45
+    end
  46
+
  47
+    def test_polymorphic_nested_where
  48
+      estimate = PriceEstimate.create :price => 125
  49
+      treasure = Treasure.create :name => 'Booty'
  50
+
  51
+      treasure.price_estimates << estimate
  52
+
  53
+      query_by_column = Treasure.where(:price_estimates => {:estimate_of_type => 'Treasure', :estimate_of_id => treasure}).joins(:price_estimates)
  54
+      query_by_model = Treasure.where(:price_estimates => {:estimate_of => treasure}).joins(:price_estimates)
  55
+
  56
+      assert_equal treasure, query_by_column.first
  57
+      assert_equal treasure, query_by_model.first
  58
+      assert_equal query_by_column.to_a, query_by_model.to_a
  59
+    end
  60
+
  61
+    def test_polymorphic_sti_nested_where
  62
+      estimate = PriceEstimate.create :price => 125
  63
+      treasure = HiddenTreasure.create!(:name => 'gold coins')
  64
+      treasure.price_estimates << PriceEstimate.create!(:price => 125)
  65
+
  66
+      treasure.price_estimates << estimate
  67
+
  68
+      query_by_column = Treasure.where(:price_estimates => {:estimate_of_type => 'Treasure', :estimate_of_id => treasure}).joins(:price_estimates)
  69
+      query_by_model = Treasure.where(:price_estimates => {:estimate_of => treasure}).joins(:price_estimates)
  70
+
  71
+      assert_equal treasure, query_by_column.first
  72
+      assert_equal treasure, query_by_model.first
  73
+      assert_equal query_by_column.to_a, query_by_model.to_a
  74
+    end
7 75
 
8 76
     def test_where_error
9 77
       assert_raises(ActiveRecord::StatementInvalid) do
3  activerecord/test/models/treasure.rb
@@ -6,3 +6,6 @@ class Treasure < ActiveRecord::Base
6 6
 
7 7
   accepts_nested_attributes_for :looter
8 8
 end
  9
+
  10
+class HiddenTreasure < Treasure
  11
+end
1  activerecord/test/schema/schema.rb
@@ -693,6 +693,7 @@ def create_table(*args, &block)
693 693
 
694 694
   create_table :treasures, :force => true do |t|
695 695
     t.column :name, :string
  696
+    t.column :type, :string
696 697
     t.column :looter_id, :integer
697 698
     t.column :looter_type, :string
698 699
   end
7  guides/source/active_record_querying.textile
Source Rendered
@@ -465,6 +465,13 @@ The field name can also be a string:
465 465
 Client.where('locked' => true)
466 466
 </ruby>
467 467
 
  468
+In the case of a belongs_to relationship, an association key can be used to specify the model if an ActiveRecord object is used as the value. This method works with polymorphic relationships as well.
  469
+
  470
+<ruby>
  471
+Post.where(:author => author)
  472
+Author.joins(:posts).where(:posts => {:author => author})
  473
+</ruby>
  474
+
468 475
 NOTE: The values cannot be symbols. For example, you cannot do +Client.where(:status => :active)+.
469 476
 
470 477
 h5(#hash-range_conditions). Range Conditions

1 note on commit 3da275c

Henrik Nyh

Awesome. Just looked into adding this.

Please sign in to comment.
Something went wrong with that request. Please try again.