Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 8671

Show
Ignore:
Timestamp:
01/19/08 03:45:24 (4 months ago)
Author:
bitsweat
Message:

Support aggregations in finder conditions. Closes #10572.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/CHANGELOG

    r8662 r8671  
    11*SVN* 
     2 
     3* Support aggregations in finder conditions.  #10572 [Ryan Kinderman] 
    24 
    35* Organize and clean up the Active Record test suite.  #10742 [John Barnette] 
  • trunk/activerecord/lib/active_record/aggregations.rb

    r8510 r8671  
    109109    # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects 
    110110    # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable 
     111    # 
     112    # == Finding records by a value object 
     113    # 
     114    # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance 
     115    # of the value object in the conditions hash. The following example finds all customers with +balance_amount+ equal to 20 and 
     116    # +balance_currency+ equal to "USD": 
     117    # 
     118    #   Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")}) 
     119    # 
    111120    module ClassMethods 
    112121      # Adds reader and writer methods for manipulating a value object: 
  • trunk/activerecord/lib/active_record/base.rb

    r8571 r8671  
    15611561        end 
    15621562 
     1563        # Similar in purpose to +expand_hash_conditions_for_aggregates+. 
     1564        def expand_attribute_names_for_aggregates(attribute_names) 
     1565          expanded_attribute_names = [] 
     1566          attribute_names.each do |attribute_name| 
     1567            unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? 
     1568              aggregate_mapping(aggregation).each do |field_attr, aggregate_attr| 
     1569                expanded_attribute_names << field_attr 
     1570              end 
     1571            else 
     1572              expanded_attribute_names << attribute_name 
     1573            end 
     1574          end 
     1575          expanded_attribute_names 
     1576        end 
     1577 
    15631578        def all_attributes_exists?(attribute_names) 
     1579          attribute_names = expand_attribute_names_for_aggregates(attribute_names) 
    15641580          attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } 
    15651581        end 
     
    18021818        end 
    18031819 
     1820        def aggregate_mapping(reflection) 
     1821          mapping = reflection.options[:mapping] || [reflection.name, reflection.name] 
     1822          mapping.first.is_a?(Array) ? mapping : [mapping] 
     1823        end 
     1824 
     1825        # Accepts a hash of sql conditions and replaces those attributes 
     1826        # that correspond to a +composed_of+ relationship with their expanded 
     1827        # aggregate attribute values. 
     1828        # Given: 
     1829        #     class Person < ActiveRecord::Base 
     1830        #       composed_of :address, :class_name => "Address", 
     1831        #         :mapping => [%w(address_street street), %w(address_city city)] 
     1832        #     end 
     1833        # Then: 
     1834        #     { :address => Address.new("813 abc st.", "chicago") } 
     1835        #       # => { :address_street => "813 abc st.", :address_city => "chicago" } 
     1836        def expand_hash_conditions_for_aggregates(attrs) 
     1837          expanded_attrs = {} 
     1838          attrs.each do |attr, value| 
     1839            unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? 
     1840              mapping = aggregate_mapping(aggregation) 
     1841              mapping.each do |field_attr, aggregate_attr| 
     1842                if mapping.size == 1 && !value.respond_to?(aggregate_attr) 
     1843                  expanded_attrs[field_attr] = value 
     1844                else 
     1845                  expanded_attrs[field_attr] = value.send(aggregate_attr) 
     1846                end 
     1847              end 
     1848            else 
     1849              expanded_attrs[attr] = value 
     1850            end 
     1851          end 
     1852          expanded_attrs 
     1853        end 
     1854 
    18041855        # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. 
    18051856        #   { :name => "foo'bar", :group_id => 4 } 
     
    18111862        #   { 'other_records.id' => 7 } 
    18121863        #     # => "`other_records`.`id` = 7" 
     1864        # And for value objects on a composed_of relationship: 
     1865        #   { :address => Address.new("123 abc st.", "chicago") } 
     1866        #     # => "address_street='123 abc st.' and address_city='chicago'" 
    18131867        def sanitize_sql_hash_for_conditions(attrs) 
     1868          attrs = expand_hash_conditions_for_aggregates(attrs) 
     1869 
    18141870          conditions = attrs.map do |attr, value| 
    18151871            attr = attr.to_s 
  • trunk/activerecord/test/cases/finder_test.rb

    r8661 r8671  
    88require 'models/developer' 
    99require 'models/post' 
     10require 'models/customer' 
    1011 
    1112class FinderTest < ActiveSupport::TestCase 
    12   fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors 
     13  fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers 
    1314 
    1415  def test_find 
     
    3940 
    4041    assert_raise(NoMethodError) { Topic.exists?([1,2]) } 
     42  end 
     43 
     44  def test_exists_with_aggregate_having_three_mappings 
     45    existing_address = customers(:david).address 
     46    assert Customer.exists?(:address => existing_address) 
     47  end 
     48 
     49  def test_exists_with_aggregate_having_three_mappings_with_one_difference 
     50    existing_address = customers(:david).address 
     51    assert !Customer.exists?(:address => 
     52      Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) 
     53    assert !Customer.exists?(:address => 
     54      Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) 
     55    assert !Customer.exists?(:address => 
     56      Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) 
    4157  end 
    4258 
     
    174190  end 
    175191 
     192  def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate 
     193    david = customers(:david) 
     194    assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address }) 
     195    assert_raises(ActiveRecord::RecordNotFound) { 
     196      Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address }) 
     197    } 
     198  end 
     199 
    176200  def test_find_on_association_proxy_conditions 
    177201    assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort 
     
    237261    assert_not_nil topic 
    238262    assert_nil topic.last_read 
     263  end 
     264 
     265  def test_hash_condition_find_with_aggregate_having_one_mapping 
     266    balance = customers(:david).balance 
     267    assert_kind_of Money, balance 
     268    found_customer = Customer.find(:first, :conditions => {:balance => balance}) 
     269    assert_equal customers(:david), found_customer 
     270  end 
     271 
     272  def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate 
     273    gps_location = customers(:david).gps_location 
     274    assert_kind_of GpsLocation, gps_location 
     275    found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location}) 
     276    assert_equal customers(:david), found_customer 
     277  end 
     278 
     279  def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value 
     280    balance = customers(:david).balance 
     281    assert_kind_of Money, balance 
     282    found_customer = Customer.find(:first, :conditions => {:balance => balance.amount}) 
     283    assert_equal customers(:david), found_customer 
     284  end 
     285 
     286  def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value 
     287    gps_location = customers(:david).gps_location 
     288    assert_kind_of GpsLocation, gps_location 
     289    found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location.gps_location}) 
     290    assert_equal customers(:david), found_customer 
     291  end 
     292 
     293  def test_hash_condition_find_with_aggregate_having_three_mappings 
     294    address = customers(:david).address 
     295    assert_kind_of Address, address 
     296    found_customer = Customer.find(:first, :conditions => {:address => address}) 
     297    assert_equal customers(:david), found_customer 
     298  end 
     299 
     300  def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not 
     301    address = customers(:david).address 
     302    assert_kind_of Address, address 
     303    found_customer = Customer.find(:first, :conditions => {:address => address, :name => customers(:david).name}) 
     304    assert_equal customers(:david), found_customer 
    239305  end 
    240306 
     
    363429  end 
    364430 
     431  def test_find_by_one_attribute_that_is_an_aggregate 
     432    address = customers(:david).address 
     433    assert_kind_of Address, address 
     434    found_customer = Customer.find_by_address(address) 
     435    assert_equal customers(:david), found_customer 
     436  end 
     437 
     438  def test_find_by_one_attribute_that_is_an_aggregate_with_one_attribute_difference 
     439    address = customers(:david).address 
     440    assert_kind_of Address, address 
     441    missing_address = Address.new(address.street, address.city, address.country + "1") 
     442    assert_nil Customer.find_by_address(missing_address) 
     443    missing_address = Address.new(address.street, address.city + "1", address.country) 
     444    assert_nil Customer.find_by_address(missing_address) 
     445    missing_address = Address.new(address.street + "1", address.city, address.country) 
     446    assert_nil Customer.find_by_address(missing_address) 
     447  end 
     448 
     449  def test_find_by_two_attributes_that_are_both_aggregates 
     450    balance = customers(:david).balance 
     451    address = customers(:david).address 
     452    assert_kind_of Money, balance 
     453    assert_kind_of Address, address 
     454    found_customer = Customer.find_by_balance_and_address(balance, address) 
     455    assert_equal customers(:david), found_customer 
     456  end 
     457 
     458  def test_find_by_two_attributes_with_one_being_an_aggregate 
     459    balance = customers(:david).balance 
     460    assert_kind_of Money, balance 
     461    found_customer = Customer.find_by_balance_and_name(balance, customers(:david).name) 
     462    assert_equal customers(:david), found_customer 
     463  end 
     464 
    365465  def test_dynamic_finder_on_one_attribute_with_conditions_caches_method 
    366466    # ensure this test can run independently of order 
     
    406506  end 
    407507 
     508  def test_find_all_by_one_attribute_that_is_an_aggregate 
     509    balance = customers(:david).balance 
     510    assert_kind_of Money, balance 
     511    found_customers = Customer.find_all_by_balance(balance) 
     512    assert_equal 1, found_customers.size 
     513    assert_equal customers(:david), found_customers.first 
     514  end 
     515 
     516  def test_find_all_by_two_attributes_that_are_both_aggregates 
     517    balance = customers(:david).balance 
     518    address = customers(:david).address 
     519    assert_kind_of Money, balance 
     520    assert_kind_of Address, address 
     521    found_customers = Customer.find_all_by_balance_and_address(balance, address) 
     522    assert_equal 1, found_customers.size 
     523    assert_equal customers(:david), found_customers.first 
     524  end 
     525 
     526  def test_find_all_by_two_attributes_with_one_being_an_aggregate 
     527    balance = customers(:david).balance 
     528    assert_kind_of Money, balance 
     529    found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name) 
     530    assert_equal 1, found_customers.size 
     531    assert_equal customers(:david), found_customers.first 
     532  end 
     533 
    408534  def test_find_all_by_one_attribute_with_options 
    409535    topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC") 
     
    465591    assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") 
    466592    assert !another.new_record? 
     593  end 
     594 
     595  def test_find_or_create_from_two_attributes_with_one_being_an_aggregate 
     596    number_of_customers = Customer.count 
     597    created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth") 
     598    assert_equal number_of_customers + 1, Customer.count 
     599    assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth") 
     600    assert !created_customer.new_record? 
    467601  end 
    468602 
     
    478612  end 
    479613 
     614  def test_find_or_create_from_one_aggregate_attribute 
     615    number_of_customers = Customer.count 
     616    created_customer = Customer.find_or_create_by_balance(Money.new(123)) 
     617    assert_equal number_of_customers + 1, Customer.count 
     618    assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123)) 
     619    assert !created_customer.new_record? 
     620  end 
     621 
     622  def test_find_or_create_from_one_aggregate_attribute_and_hash 
     623    number_of_customers = Customer.count 
     624    balance = Money.new(123) 
     625    name = "Elizabeth" 
     626    created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name}) 
     627    assert_equal number_of_customers + 1, Customer.count 
     628    assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name}) 
     629    assert !created_customer.new_record? 
     630    assert_equal balance, created_customer.balance 
     631    assert_equal name, created_customer.name 
     632  end 
     633 
    480634  def test_find_or_initialize_from_one_attribute 
    481635    sig38 = Company.find_or_initialize_by_name("38signals") 
    482636    assert_equal "38signals", sig38.name 
    483637    assert sig38.new_record? 
     638  end 
     639 
     640  def test_find_or_initialize_from_one_aggregate_attribute 
     641    new_customer = Customer.find_or_initialize_by_balance(Money.new(123)) 
     642    assert_equal 123, new_customer.balance.amount 
     643    assert new_customer.new_record? 
    484644  end 
    485645 
     
    514674  end 
    515675 
     676  def test_find_or_initialize_from_one_aggregate_attribute_and_one_not 
     677    new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth") 
     678    assert_equal 123, new_customer.balance.amount 
     679    assert_equal "Elizabeth", new_customer.name 
     680    assert new_customer.new_record? 
     681  end 
     682 
    516683  def test_find_or_initialize_from_one_attribute_and_hash 
    517684    sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) 
     
    520687    assert_equal 23, sig38.client_of 
    521688    assert sig38.new_record? 
     689  end 
     690 
     691  def test_find_or_initialize_from_one_aggregate_attribute_and_hash 
     692    balance = Money.new(123) 
     693    name = "Elizabeth" 
     694    new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name}) 
     695    assert_equal balance, new_customer.balance 
     696    assert_equal name, new_customer.name 
     697    assert new_customer.new_record? 
    522698  end 
    523699