Permalink
Browse files

Allow conditions on multiple tables to be specified using hash.

Examples:

  User.all :joins => :items, :conditions => { :age => 10, :items => { :color => 'black' } }
  Item.first :conditions => { :items => { :color => 'red' } }

Note : Hash key in :conditions is referring to the actual table name or the alias defined in query.
  • Loading branch information...
1 parent 582bff7 commit cd994eff9a343df376bfaec59de5b24a2ab51256 @lifo lifo committed Jun 28, 2008
Showing with 37 additions and 9 deletions.
  1. +5 −0 activerecord/CHANGELOG
  2. +15 −9 activerecord/lib/active_record/base.rb
  3. +17 −0 activerecord/test/cases/finder_test.rb
View
@@ -1,5 +1,10 @@
*Edge*
+* Allow conditions on multiple tables to be specified using hash. [Pratik Naik]. Example:
+
+ User.all :joins => :items, :conditions => { :age => 10, :items => { :color => 'black' } }
+ Item.first :conditions => { :items => { :color => 'red' } }
+
* Always treat integer :limit as byte length. #420 [Tarmo Tänav]
* Partial updates don't update lock_version if nothing changed. #426 [Daniel Morrison]
@@ -1999,24 +1999,28 @@ def expand_hash_conditions_for_aggregates(attrs)
# # => "age BETWEEN 13 AND 18"
# { 'other_records.id' => 7 }
# # => "`other_records`.`id` = 7"
+ # { :other_records => { :id => 7 } }
+ # # => "`other_records`.`id` = 7"
# And for value objects on a composed_of relationship:
# { :address => Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
- def sanitize_sql_hash_for_conditions(attrs)
+ def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name)
attrs = expand_hash_conditions_for_aggregates(attrs)
conditions = attrs.map do |attr, value|
- attr = attr.to_s
+ unless value.is_a?(Hash)
+ attr = attr.to_s
+
+ # Extract table name from qualified attribute names.
+ if attr.include?('.')
+ table_name, attr = attr.split('.', 2)
+ table_name = connection.quote_table_name(table_name)
+ end
- # Extract table name from qualified attribute names.
- if attr.include?('.')
- table_name, attr = attr.split('.', 2)
- table_name = connection.quote_table_name(table_name)
+ "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
else
- table_name = quoted_table_name
+ sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
end
-
- "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
end.join(' AND ')
replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
@@ -2070,6 +2074,8 @@ def expand_range_bind_variables(bind_vars) #:nodoc:
expanded = []
bind_vars.each do |var|
+ next if var.is_a?(Hash)
+
if var.is_a?(Range)
expanded << var.first
expanded << var.last
@@ -200,6 +200,23 @@ def test_find_on_hash_conditions_with_explicit_table_name
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
end
+ def test_find_on_hash_conditions_with_hashed_table_name
+ assert Topic.find(1, :conditions => {:topics => { :approved => false }})
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
+ end
+
+ def test_find_with_hash_conditions_on_joined_table
+ firms = Firm.all :joins => :account, :conditions => {:accounts => { :credit_limit => 50 }}
+ assert_equal 1, firms.size
+ assert_equal companies(:first_firm), firms.first
+ end
+
+ def test_find_with_hash_conditions_on_joined_table_and_with_range
+ firms = DependentFirm.all :joins => :account, :conditions => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}
+ assert_equal 1, firms.size
+ assert_equal companies(:rails_core), firms.first
+ end
+
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
david = customers(:david)
assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address })

4 comments on commit cd994ef

Hi,

Cool. This is very useful because it allows me to not have to worry about the name of the table when I join to a table and then back again, eg. like finding another book on the same bookshelf:

Books.all(
:joins => {:bookshelf => :books},
:conditions => {:bookshelves => {:books => {:title => ‘Another Book on the Same Shelf’}}}
)

Without having to worry about the name of the second book table in the SQL like I used to. So thanks!

I have 2 suggestions, though.
- I don’t seem to find that it works with :include, only :joins. Is there any reason for this?
- To me it makes more intuitive sense that the symbols are the same as the :joins, so it would be :conditions => {:bookshelf => instead of :conditions => :bookshelves as is currently the case.

But overall, very useful.
ben

Actually, sorry to be a pain, but the above seems to actually illustrate a bug. Say I’m trying to get the names of all the books on a particular shelf (which are book1 and book2), and all I have is the name of one of the books (book1).

Using your method, I would


Book.all(
  :joins => {:bookshelf => :books},
  :conditions => {:bookshelves => {:books => {:title => 'book1'}}}
).collect{|b| b.title}

I end up getting

=> ["book1", "book1"]

I suppose this is a bug, but I’m too lazy to file it on lighthouse.

bug report:

http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/710-condition-hashes-through-joins-gives-fails-when-circular-references-are-used

Contributor

anildigital replied Aug 4, 2008

Thanks Pratik

Please sign in to comment.