Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert model name to foreign key in queries #7273

Merged
merged 1 commit into from Sep 12, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions activerecord/CHANGELOG.md
@@ -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.

Expand Down
52 changes: 48 additions & 4 deletions activerecord/lib/active_record/relation/predicate_builder.rb
@@ -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

Expand All @@ -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)
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions activerecord/lib/active_record/relation/query_methods.rb
Expand Up @@ -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
Expand Down
70 changes: 69 additions & 1 deletion activerecord/test/cases/relation/where_test.rb
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions activerecord/test/models/treasure.rb
Expand Up @@ -6,3 +6,6 @@ class Treasure < ActiveRecord::Base

accepts_nested_attributes_for :looter
end

class HiddenTreasure < Treasure
end
1 change: 1 addition & 0 deletions activerecord/test/schema/schema.rb
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions guides/source/active_record_querying.textile
Expand Up @@ -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
Expand Down