Permalink
Browse files

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...
beerlington committed Sep 11, 2012
1 parent f2a44ad commit 3da275c4396d7fad250d2b786027ba4f14344bd4
View
@@ -1,5 +1,17 @@
## Rails 4.0.0 (unreleased) ##
+* Accept belongs_to (including polymorphic) association keys in queries
+
+ The following queries are now equivalent:
+
+ Post.where(:author => author)
+ Post.where(:author_id => author)
+
+ PriceEstimate.where(:estimate_of => treasure)
+ PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+
+ *Peter Brown*
+
* Use native `mysqldump` command instead of `structure_dump` method
when dumping the database structure to a sql file. Fixes #5547.
@@ -1,12 +1,25 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
def self.build_from_hash(engine, attributes, default_table)
- attributes.map do |column, value|
+ queries = []
+
+ attributes.each do |column, value|
table = default_table
if value.is_a?(Hash)
table = Arel::Table.new(column, engine)
- value.map { |k,v| build(table[k.to_sym], v) }
+
+ value.each do |k,v|
+ if rk = find_reflection_key(column, v, v)
+ if rk[:foreign_type]
+ queries << build(table[rk[:foreign_type]], v.class.base_class)
+ end
+
+ k = rk[:foreign_key]
+ end
+
+ queries << build(table[k.to_sym], v)
+ end
else
column = column.to_s
@@ -15,9 +28,19 @@ def self.build_from_hash(engine, attributes, default_table)
table = Arel::Table.new(table_name, engine)
end
- build(table[column.to_sym], value)
+ if rk = find_reflection_key(column, engine, value)
+ if rk[:foreign_type]
+ queries << build(table[rk[:foreign_type]], value.class.base_class)
+ end
+
+ column = rk[:foreign_key]
+ end
+
+ queries << build(table[column.to_sym], value)
end
- end.flatten
+ end
+
+ queries
end
def self.references(attributes)
@@ -31,6 +54,27 @@ def self.references(attributes)
end.compact
end
+ # Find the foreign key when using queries such as:
+ # Post.where(:author => author)
+ #
+ # For polymorphic relationships, find the foreign key and type:
+ # PriceEstimate.where(:estimate_of => treasure)
+ def self.find_reflection_key(parent_column, model, value)
+ # value must be an ActiveRecord object
+ return nil unless value.class < Model::Tag
+
+ if reflection = model.reflections[parent_column.to_sym]
+ if reflection.options[:polymorphic]
+ {
+ :foreign_key => reflection.foreign_key,
+ :foreign_type => reflection.foreign_type
+ }
+ else
+ { :foreign_key => reflection.foreign_key }
+ end
+ end
+ end
+
private
def self.build(attribute, value)
case value
@@ -340,6 +340,24 @@ def bind!(value)
# User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
# # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
#
+ # 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.
+ #
+ # author = Author.find(1)
+ #
+ # # The following queries will be equivalent:
+ # Post.where(:author => author)
+ # Post.where(:author_id => author)
+ #
+ # This also works with polymorphic belongs_to relationships:
+ #
+ # treasure = Treasure.create(:name => 'gold coins')
+ # treasure.price_estimates << PriceEstimate.create(:price => 125)
+ #
+ # # The following queries will be equivalent:
+ # PriceEstimate.where(:estimate_of => treasure)
+ # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+ #
# === Joins
#
# If the relation is the result of a join, you may create a condition which uses any of the
@@ -1,9 +1,77 @@
require "cases/helper"
+require 'models/author'
+require 'models/price_estimate'
+require 'models/treasure'
require 'models/post'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts
+ fixtures :posts, :authors
+
+ def test_belongs_to_shallow_where
+ author = Post.first.author
+ query_with_id = Post.where(:author_id => author)
+ query_with_assoc = Post.where(:author => author)
+
+ assert_equal query_with_id.to_sql, query_with_assoc.to_sql
+ end
+
+ def test_belongs_to_nested_where
+ author = Post.first.author
+ query_with_id = Author.where(:posts => {:author_id => author}).joins(:posts)
+ query_with_assoc = Author.where(:posts => {:author => author}).joins(:posts)
+
+ assert_equal query_with_id.to_sql, query_with_assoc.to_sql
+ end
+
+ def test_polymorphic_shallow_where
+ treasure = Treasure.create(:name => 'gold coins')
+ treasure.price_estimates << PriceEstimate.create(:price => 125)
+
+ query_by_column = PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+ query_by_model = PriceEstimate.where(:estimate_of => treasure)
+
+ assert_equal query_by_column.to_sql, query_by_model.to_sql
+ end
+
+ def test_polymorphic_sti_shallow_where
+ treasure = HiddenTreasure.create!(:name => 'gold coins')
+ treasure.price_estimates << PriceEstimate.create!(:price => 125)
+
+ query_by_column = PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+ query_by_model = PriceEstimate.where(:estimate_of => treasure)
+
+ assert_equal query_by_column.to_sql, query_by_model.to_sql
+ end
+
+ def test_polymorphic_nested_where
+ estimate = PriceEstimate.create :price => 125
+ treasure = Treasure.create :name => 'Booty'
+
+ treasure.price_estimates << estimate
+
+ query_by_column = Treasure.where(:price_estimates => {:estimate_of_type => 'Treasure', :estimate_of_id => treasure}).joins(:price_estimates)
+ query_by_model = Treasure.where(:price_estimates => {:estimate_of => treasure}).joins(:price_estimates)
+
+ assert_equal treasure, query_by_column.first
+ assert_equal treasure, query_by_model.first
+ assert_equal query_by_column.to_a, query_by_model.to_a
+ end
+
+ def test_polymorphic_sti_nested_where
+ estimate = PriceEstimate.create :price => 125
+ treasure = HiddenTreasure.create!(:name => 'gold coins')
+ treasure.price_estimates << PriceEstimate.create!(:price => 125)
+
+ treasure.price_estimates << estimate
+
+ query_by_column = Treasure.where(:price_estimates => {:estimate_of_type => 'Treasure', :estimate_of_id => treasure}).joins(:price_estimates)
+ query_by_model = Treasure.where(:price_estimates => {:estimate_of => treasure}).joins(:price_estimates)
+
+ assert_equal treasure, query_by_column.first
+ assert_equal treasure, query_by_model.first
+ assert_equal query_by_column.to_a, query_by_model.to_a
+ end
def test_where_error
assert_raises(ActiveRecord::StatementInvalid) do
@@ -6,3 +6,6 @@ class Treasure < ActiveRecord::Base
accepts_nested_attributes_for :looter
end
+
+class HiddenTreasure < Treasure
+end
@@ -693,6 +693,7 @@ def create_table(*args, &block)
create_table :treasures, :force => true do |t|
t.column :name, :string
+ t.column :type, :string
t.column :looter_id, :integer
t.column :looter_type, :string
end
@@ -465,6 +465,13 @@ The field name can also be a string:
Client.where('locked' => true)
</ruby>
+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.
+
+<ruby>
+Post.where(:author => author)
+Author.joins(:posts).where(:posts => {:author => author})
+</ruby>
+
NOTE: The values cannot be symbols. For example, you cannot do +Client.where(:status => :active)+.
h5(#hash-range_conditions). Range Conditions

1 comment on commit 3da275c

@henrik

This comment has been minimized.

Show comment
Hide comment
@henrik

henrik Mar 2, 2013

Contributor

Awesome. Just looked into adding this.

Contributor

henrik commented on 3da275c Mar 2, 2013

Awesome. Just looked into adding this.

Please sign in to comment.