mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Revert "Removing composed_of from ActiveRecord."
This reverts commit 14fc8b3452
.
Reason: we need to discuss a better path from this removal.
Conflicts:
activerecord/lib/active_record/reflection.rb
activerecord/test/cases/base_test.rb
activerecord/test/models/developer.rb
This commit is contained in:
parent
c01810dbec
commit
f4d818d51e
19 changed files with 790 additions and 32 deletions
|
@ -179,29 +179,6 @@
|
|||
|
||||
*Joost Baaij & Carlos Antonio da Silva*
|
||||
|
||||
* `composed_of` was removed. You'll have to write your own accessor
|
||||
and mutator methods if you'd like to use value objects to represent some
|
||||
portion of your models. So, instead of:
|
||||
|
||||
class Person < ActiveRecord::Base
|
||||
composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
||||
end
|
||||
|
||||
you could write something like this:
|
||||
|
||||
def address
|
||||
@address ||= Address.new(address_street, address_city)
|
||||
end
|
||||
|
||||
def address=(address)
|
||||
self[:address_street] = @address.street
|
||||
self[:address_city] = @address.city
|
||||
|
||||
@address = address
|
||||
end
|
||||
|
||||
*Steve Klabnik*
|
||||
|
||||
* PostgreSQL default log level is now 'warning', to bypass the noisy notice
|
||||
messages. You can change the log level using the `min_messages` option
|
||||
available in your config/database.yml.
|
||||
|
|
|
@ -46,6 +46,18 @@ A short rundown of some of the major features:
|
|||
{Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html]
|
||||
|
||||
|
||||
* Aggregations of value objects.
|
||||
|
||||
class Account < ActiveRecord::Base
|
||||
composed_of :balance, :class_name => "Money",
|
||||
:mapping => %w(balance amount)
|
||||
composed_of :address,
|
||||
:mapping => [%w(address_street street), %w(address_city city)]
|
||||
end
|
||||
|
||||
{Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html]
|
||||
|
||||
|
||||
* Validation rules that can differ for new or existing objects.
|
||||
|
||||
class Account < ActiveRecord::Base
|
||||
|
|
|
@ -36,6 +36,7 @@ module ActiveRecord
|
|||
autoload :ConnectionNotEstablished, 'active_record/errors'
|
||||
autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter'
|
||||
|
||||
autoload :Aggregations
|
||||
autoload :Associations
|
||||
autoload :AttributeMethods
|
||||
autoload :AttributeAssignment
|
||||
|
|
261
activerecord/lib/active_record/aggregations.rb
Normal file
261
activerecord/lib/active_record/aggregations.rb
Normal file
|
@ -0,0 +1,261 @@
|
|||
module ActiveRecord
|
||||
# = Active Record Aggregations
|
||||
module Aggregations # :nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def clear_aggregation_cache #:nodoc:
|
||||
@aggregation_cache.clear if persisted?
|
||||
end
|
||||
|
||||
# Active Record implements aggregation through a macro-like class method called +composed_of+
|
||||
# for representing attributes as value objects. It expresses relationships like "Account [is]
|
||||
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
|
||||
# to the macro adds a description of how the value objects are created from the attributes of
|
||||
# the entity object (when the entity is initialized either as a new object or from finding an
|
||||
# existing object) and how it can be turned back into attributes (when the entity is saved to
|
||||
# the database).
|
||||
#
|
||||
# class Customer < ActiveRecord::Base
|
||||
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
|
||||
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
||||
# end
|
||||
#
|
||||
# The customer class now has the following methods to manipulate the value objects:
|
||||
# * <tt>Customer#balance, Customer#balance=(money)</tt>
|
||||
# * <tt>Customer#address, Customer#address=(address)</tt>
|
||||
#
|
||||
# These methods will operate with value objects like the ones described below:
|
||||
#
|
||||
# class Money
|
||||
# include Comparable
|
||||
# attr_reader :amount, :currency
|
||||
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
|
||||
#
|
||||
# def initialize(amount, currency = "USD")
|
||||
# @amount, @currency = amount, currency
|
||||
# end
|
||||
#
|
||||
# def exchange_to(other_currency)
|
||||
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
|
||||
# Money.new(exchanged_amount, other_currency)
|
||||
# end
|
||||
#
|
||||
# def ==(other_money)
|
||||
# amount == other_money.amount && currency == other_money.currency
|
||||
# end
|
||||
#
|
||||
# def <=>(other_money)
|
||||
# if currency == other_money.currency
|
||||
# amount <=> other_money.amount
|
||||
# else
|
||||
# amount <=> other_money.exchange_to(currency).amount
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Address
|
||||
# attr_reader :street, :city
|
||||
# def initialize(street, city)
|
||||
# @street, @city = street, city
|
||||
# end
|
||||
#
|
||||
# def close_to?(other_address)
|
||||
# city == other_address.city
|
||||
# end
|
||||
#
|
||||
# def ==(other_address)
|
||||
# city == other_address.city && street == other_address.street
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Now it's possible to access attributes from the database through the value objects instead. If
|
||||
# you choose to name the composition the same as the attribute's name, it will be the only way to
|
||||
# access that attribute. That's the case with our +balance+ attribute. You interact with the value
|
||||
# objects just like you would with any other attribute:
|
||||
#
|
||||
# customer.balance = Money.new(20) # sets the Money value object and the attribute
|
||||
# customer.balance # => Money value object
|
||||
# customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
|
||||
# customer.balance > Money.new(10) # => true
|
||||
# customer.balance == Money.new(20) # => true
|
||||
# customer.balance < Money.new(5) # => false
|
||||
#
|
||||
# Value objects can also be composed of multiple attributes, such as the case of Address. The order
|
||||
# of the mappings will determine the order of the parameters.
|
||||
#
|
||||
# customer.address_street = "Hyancintvej"
|
||||
# customer.address_city = "Copenhagen"
|
||||
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
|
||||
#
|
||||
# customer.address_street = "Vesterbrogade"
|
||||
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
|
||||
# customer.clear_aggregation_cache
|
||||
# customer.address # => Address.new("Vesterbrogade", "Copenhagen")
|
||||
#
|
||||
# customer.address = Address.new("May Street", "Chicago")
|
||||
# customer.address_street # => "May Street"
|
||||
# customer.address_city # => "Chicago"
|
||||
#
|
||||
# == Writing value objects
|
||||
#
|
||||
# Value objects are immutable and interchangeable objects that represent a given value, such as
|
||||
# a Money object representing $5. Two Money objects both representing $5 should be equal (through
|
||||
# methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
|
||||
# unlike entity objects where equality is determined by identity. An entity class such as Customer can
|
||||
# easily have two different objects that both have an address on Hyancintvej. Entity identity is
|
||||
# determined by object or relational unique identifiers (such as primary keys). Normal
|
||||
# ActiveRecord::Base classes are entity objects.
|
||||
#
|
||||
# It's also important to treat the value objects as immutable. Don't allow the Money object to have
|
||||
# its amount changed after creation. Create a new Money object with the new value instead. The
|
||||
# Money#exchange_to method is an example of this. It returns a new value object instead of changing
|
||||
# its own values. Active Record won't persist value objects that have been changed through means
|
||||
# other than the writer method.
|
||||
#
|
||||
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
|
||||
# object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
|
||||
#
|
||||
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
|
||||
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
|
||||
#
|
||||
# == Custom constructors and converters
|
||||
#
|
||||
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value
|
||||
# class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
|
||||
# option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
|
||||
# a custom constructor to be specified.
|
||||
#
|
||||
# When a new value is assigned to the value object, the default assumption is that the new value
|
||||
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
|
||||
# converted to an instance of value class if necessary.
|
||||
#
|
||||
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
|
||||
# should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
|
||||
# for the value class is called +create+ and it expects a CIDR address string as a parameter. New
|
||||
# values can be assigned to the value object using either another NetAddr::CIDR object, a string
|
||||
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
|
||||
# these requirements:
|
||||
#
|
||||
# class NetworkResource < ActiveRecord::Base
|
||||
# composed_of :cidr,
|
||||
# :class_name => 'NetAddr::CIDR',
|
||||
# :mapping => [ %w(network_address network), %w(cidr_range bits) ],
|
||||
# :allow_nil => true,
|
||||
# :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
|
||||
# :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
|
||||
# end
|
||||
#
|
||||
# # This calls the :constructor
|
||||
# network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
|
||||
#
|
||||
# # These assignments will both use the :converter
|
||||
# network_resource.cidr = [ '192.168.2.1', 8 ]
|
||||
# network_resource.cidr = '192.168.0.1/24'
|
||||
#
|
||||
# # This assignment won't use the :converter as the value is already an instance of the value class
|
||||
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
|
||||
#
|
||||
# # Saving and then reloading will use the :constructor on reload
|
||||
# network_resource.save
|
||||
# network_resource.reload
|
||||
#
|
||||
# == Finding records by a value object
|
||||
#
|
||||
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
|
||||
# by specifying an instance of the value object in the conditions hash. The following example
|
||||
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
|
||||
#
|
||||
# Customer.where(:balance => Money.new(20, "USD")).all
|
||||
#
|
||||
module ClassMethods
|
||||
# Adds reader and writer methods for manipulating a value object:
|
||||
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
|
||||
#
|
||||
# Options are:
|
||||
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
|
||||
# can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
|
||||
# to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
|
||||
# with this option.
|
||||
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
|
||||
# object. Each mapping is represented as an array where the first item is the name of the
|
||||
# entity attribute and the second item is the name of the attribute in the value object. The
|
||||
# order in which mappings are defined determines the order in which attributes are sent to the
|
||||
# value class constructor.
|
||||
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
|
||||
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
|
||||
# mapped attributes.
|
||||
# This defaults to +false+.
|
||||
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
|
||||
# is called to initialize the value object. The constructor is passed all of the mapped attributes,
|
||||
# in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
|
||||
# to instantiate a <tt>:class_name</tt> object.
|
||||
# The default is <tt>:new</tt>.
|
||||
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
|
||||
# or a Proc that is called when a new value is assigned to the value object. The converter is
|
||||
# passed the single value that is used in the assignment and is only called if the new value is
|
||||
# not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
|
||||
# can return nil to skip the assignment.
|
||||
#
|
||||
# Option examples:
|
||||
# composed_of :temperature, :mapping => %w(reading celsius)
|
||||
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount),
|
||||
# :converter => Proc.new { |balance| balance.to_money }
|
||||
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
||||
# composed_of :gps_location
|
||||
# composed_of :gps_location, :allow_nil => true
|
||||
# composed_of :ip_address,
|
||||
# :class_name => 'IPAddr',
|
||||
# :mapping => %w(ip to_i),
|
||||
# :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
|
||||
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
|
||||
#
|
||||
def composed_of(part_id, options = {})
|
||||
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
|
||||
|
||||
name = part_id.id2name
|
||||
class_name = options[:class_name] || name.camelize
|
||||
mapping = options[:mapping] || [ name, name ]
|
||||
mapping = [ mapping ] unless mapping.first.is_a?(Array)
|
||||
allow_nil = options[:allow_nil] || false
|
||||
constructor = options[:constructor] || :new
|
||||
converter = options[:converter]
|
||||
|
||||
reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
|
||||
create_reflection(:composed_of, part_id, nil, options, self)
|
||||
end
|
||||
|
||||
private
|
||||
def reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
define_method(name) do
|
||||
if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
|
||||
attrs = mapping.collect {|pair| read_attribute(pair.first)}
|
||||
object = constructor.respond_to?(:call) ?
|
||||
constructor.call(*attrs) :
|
||||
class_name.constantize.send(constructor, *attrs)
|
||||
@aggregation_cache[name] = object
|
||||
end
|
||||
@aggregation_cache[name]
|
||||
end
|
||||
end
|
||||
|
||||
def writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
define_method("#{name}=") do |part|
|
||||
klass = class_name.constantize
|
||||
unless part.is_a?(klass) || converter.nil? || part.nil?
|
||||
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
|
||||
end
|
||||
|
||||
if part.nil? && allow_nil
|
||||
mapping.each { |pair| self[pair.first] = nil }
|
||||
@aggregation_cache[name] = nil
|
||||
else
|
||||
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
||||
@aggregation_cache[name] = part.freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -132,7 +132,7 @@ module ActiveRecord
|
|||
private
|
||||
|
||||
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
||||
# by calling new on the column type or aggregation type object with these parameters.
|
||||
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
||||
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
||||
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
||||
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
|
||||
|
@ -168,7 +168,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def read_value_from_parameter(name, values_hash_from_param)
|
||||
klass = column_for_attribute(name).klass
|
||||
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
|
||||
if values_hash_from_param.values.all?{|v|v.nil?}
|
||||
nil
|
||||
elsif klass == Time
|
||||
|
|
|
@ -266,6 +266,7 @@ module ActiveRecord
|
|||
@changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
|
||||
end
|
||||
|
||||
@aggregation_cache = {}
|
||||
@association_cache = {}
|
||||
@attributes_cache = {}
|
||||
|
||||
|
@ -390,6 +391,7 @@ module ActiveRecord
|
|||
|
||||
@attributes[pk] = nil unless @attributes.key?(pk)
|
||||
|
||||
@aggregation_cache = {}
|
||||
@association_cache = {}
|
||||
@attributes_cache = {}
|
||||
@previously_changed = {}
|
||||
|
|
|
@ -57,7 +57,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def valid?
|
||||
attribute_names.all? { |name| model.columns_hash[name] }
|
||||
attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
|
||||
end
|
||||
|
||||
def define
|
||||
|
|
|
@ -89,6 +89,7 @@ module ActiveRecord
|
|||
include ActiveModel::SecurePassword
|
||||
include AutosaveAssociation
|
||||
include NestedAttributes
|
||||
include Aggregations
|
||||
include Transactions
|
||||
include Reflection
|
||||
include Serialization
|
||||
|
|
|
@ -290,6 +290,7 @@ module ActiveRecord
|
|||
# may do e.g. record.reload(:lock => true) to reload the same record with
|
||||
# an exclusive row lock.
|
||||
def reload(options = nil)
|
||||
clear_aggregation_cache
|
||||
clear_association_cache
|
||||
|
||||
fresh_object =
|
||||
|
|
|
@ -17,17 +17,36 @@ module ActiveRecord
|
|||
# and creates input fields for all of the attributes depending on their type
|
||||
# and displays the associations to other objects.
|
||||
#
|
||||
# MacroReflection class has info for the AssociationReflection
|
||||
# class.
|
||||
# MacroReflection class has info for AggregateReflection and AssociationReflection
|
||||
# classes.
|
||||
module ClassMethods
|
||||
def create_reflection(macro, name, scope, options, active_record)
|
||||
klass = options[:through] ? ThroughReflection : AssociationReflection
|
||||
reflection = klass.new(macro, name, scope, options, active_record)
|
||||
case macro
|
||||
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
||||
klass = options[:through] ? ThroughReflection : AssociationReflection
|
||||
reflection = klass.new(macro, name, scope, options, active_record)
|
||||
when :composed_of
|
||||
reflection = AggregateReflection.new(macro, name, scope, options, active_record)
|
||||
end
|
||||
|
||||
self.reflections = self.reflections.merge(name => reflection)
|
||||
reflection
|
||||
end
|
||||
|
||||
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
||||
def reflect_on_all_aggregations
|
||||
reflections.values.grep(AggregateReflection)
|
||||
end
|
||||
|
||||
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
|
||||
#
|
||||
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
|
||||
#
|
||||
def reflect_on_aggregation(aggregation)
|
||||
reflection = reflections[aggregation]
|
||||
reflection if reflection.is_a?(AggregateReflection)
|
||||
end
|
||||
|
||||
# Returns an array of AssociationReflection objects for all the
|
||||
# associations in the class. If you only want to reflect on a certain
|
||||
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
|
||||
|
@ -59,15 +78,18 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Abstract base class for AssociationReflection. Objects of AssociationReflection are returned by the Reflection::ClassMethods.
|
||||
# Abstract base class for AggregateReflection and AssociationReflection. Objects of
|
||||
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
||||
class MacroReflection
|
||||
# Returns the name of the macro.
|
||||
#
|
||||
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
|
||||
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
|
||||
attr_reader :name
|
||||
|
||||
# Returns the macro type.
|
||||
#
|
||||
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt>
|
||||
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
|
||||
attr_reader :macro
|
||||
|
||||
|
@ -75,6 +97,7 @@ module ActiveRecord
|
|||
|
||||
# Returns the hash of options used for the macro.
|
||||
#
|
||||
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
|
||||
# <tt>has_many :clients</tt> returns +{}+
|
||||
attr_reader :options
|
||||
|
||||
|
@ -94,6 +117,7 @@ module ActiveRecord
|
|||
|
||||
# Returns the class for the macro.
|
||||
#
|
||||
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
|
||||
# <tt>has_many :clients</tt> returns the Client class
|
||||
def klass
|
||||
@klass ||= class_name.constantize
|
||||
|
@ -101,6 +125,7 @@ module ActiveRecord
|
|||
|
||||
# Returns the class name for the macro.
|
||||
#
|
||||
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
|
||||
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
|
||||
def class_name
|
||||
@class_name ||= (options[:class_name] || derive_class_name).to_s
|
||||
|
@ -122,6 +147,16 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
# Holds all the meta-data about an aggregation as it was specified in the
|
||||
# Active Record class.
|
||||
class AggregateReflection < MacroReflection #:nodoc:
|
||||
def mapping
|
||||
mapping = options[:mapping] || [name, name]
|
||||
mapping.first.is_a?(Array) ? mapping : [mapping]
|
||||
end
|
||||
end
|
||||
|
||||
# Holds all the meta-data about an association as it was specified in the
|
||||
# Active Record class.
|
||||
class AssociationReflection < MacroReflection #:nodoc:
|
||||
|
|
|
@ -689,7 +689,8 @@ module ActiveRecord
|
|||
when String, Array
|
||||
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
|
||||
when Hash
|
||||
PredicateBuilder.build_from_hash(table.engine, opts, table)
|
||||
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
|
||||
PredicateBuilder.build_from_hash(table.engine, attributes, table)
|
||||
else
|
||||
[opts]
|
||||
end
|
||||
|
|
|
@ -43,6 +43,36 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Accepts a hash of SQL conditions and replaces those attributes
|
||||
# that correspond to a +composed_of+ relationship with their expanded
|
||||
# aggregate attribute values.
|
||||
# Given:
|
||||
# class Person < ActiveRecord::Base
|
||||
# composed_of :address, :class_name => "Address",
|
||||
# :mapping => [%w(address_street street), %w(address_city city)]
|
||||
# end
|
||||
# Then:
|
||||
# { :address => Address.new("813 abc st.", "chicago") }
|
||||
# # => { :address_street => "813 abc st.", :address_city => "chicago" }
|
||||
def expand_hash_conditions_for_aggregates(attrs)
|
||||
expanded_attrs = {}
|
||||
attrs.each do |attr, value|
|
||||
if aggregation = reflect_on_aggregation(attr.to_sym)
|
||||
mapping = aggregation.mapping
|
||||
mapping.each do |field_attr, aggregate_attr|
|
||||
if mapping.size == 1 && !value.respond_to?(aggregate_attr)
|
||||
expanded_attrs[field_attr] = value
|
||||
else
|
||||
expanded_attrs[field_attr] = value.send(aggregate_attr)
|
||||
end
|
||||
end
|
||||
else
|
||||
expanded_attrs[attr] = value
|
||||
end
|
||||
end
|
||||
expanded_attrs
|
||||
end
|
||||
|
||||
# Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
|
||||
# { :name => "foo'bar", :group_id => 4 }
|
||||
# # => "name='foo''bar' and group_id= 4"
|
||||
|
@ -58,6 +88,8 @@ module ActiveRecord
|
|||
# { :address => Address.new("123 abc st.", "chicago") }
|
||||
# # => "address_street='123 abc st.' and address_city='chicago'"
|
||||
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
|
||||
attrs = expand_hash_conditions_for_aggregates(attrs)
|
||||
|
||||
table = Arel::Table.new(table_name).alias(default_table_name)
|
||||
PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
|
||||
connection.visitor.accept b
|
||||
|
|
158
activerecord/test/cases/aggregations_test.rb
Normal file
158
activerecord/test/cases/aggregations_test.rb
Normal file
|
@ -0,0 +1,158 @@
|
|||
require "cases/helper"
|
||||
require 'models/customer'
|
||||
require 'active_support/core_ext/exception'
|
||||
|
||||
class AggregationsTest < ActiveRecord::TestCase
|
||||
fixtures :customers
|
||||
|
||||
def test_find_single_value_object
|
||||
assert_equal 50, customers(:david).balance.amount
|
||||
assert_kind_of Money, customers(:david).balance
|
||||
assert_equal 300, customers(:david).balance.exchange_to("DKK").amount
|
||||
end
|
||||
|
||||
def test_find_multiple_value_object
|
||||
assert_equal customers(:david).address_street, customers(:david).address.street
|
||||
assert(
|
||||
customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country))
|
||||
)
|
||||
end
|
||||
|
||||
def test_change_single_value_object
|
||||
customers(:david).balance = Money.new(100)
|
||||
customers(:david).save
|
||||
assert_equal 100, customers(:david).reload.balance.amount
|
||||
end
|
||||
|
||||
def test_immutable_value_objects
|
||||
customers(:david).balance = Money.new(100)
|
||||
assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } }
|
||||
end
|
||||
|
||||
def test_inferred_mapping
|
||||
assert_equal "35.544623640962634", customers(:david).gps_location.latitude
|
||||
assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
|
||||
|
||||
customers(:david).gps_location = GpsLocation.new("39x-110")
|
||||
|
||||
assert_equal "39", customers(:david).gps_location.latitude
|
||||
assert_equal "-110", customers(:david).gps_location.longitude
|
||||
|
||||
customers(:david).save
|
||||
|
||||
customers(:david).reload
|
||||
|
||||
assert_equal "39", customers(:david).gps_location.latitude
|
||||
assert_equal "-110", customers(:david).gps_location.longitude
|
||||
end
|
||||
|
||||
def test_reloaded_instance_refreshes_aggregations
|
||||
assert_equal "35.544623640962634", customers(:david).gps_location.latitude
|
||||
assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
|
||||
|
||||
Customer.update_all("gps_location = '24x113'")
|
||||
customers(:david).reload
|
||||
assert_equal '24x113', customers(:david)['gps_location']
|
||||
|
||||
assert_equal GpsLocation.new('24x113'), customers(:david).gps_location
|
||||
end
|
||||
|
||||
def test_gps_equality
|
||||
assert_equal GpsLocation.new('39x110'), GpsLocation.new('39x110')
|
||||
end
|
||||
|
||||
def test_gps_inequality
|
||||
assert_not_equal GpsLocation.new('39x110'), GpsLocation.new('39x111')
|
||||
end
|
||||
|
||||
def test_allow_nil_gps_is_nil
|
||||
assert_nil customers(:zaphod).gps_location
|
||||
end
|
||||
|
||||
def test_allow_nil_gps_set_to_nil
|
||||
customers(:david).gps_location = nil
|
||||
customers(:david).save
|
||||
customers(:david).reload
|
||||
assert_nil customers(:david).gps_location
|
||||
end
|
||||
|
||||
def test_allow_nil_set_address_attributes_to_nil
|
||||
customers(:zaphod).address = nil
|
||||
assert_nil customers(:zaphod).attributes[:address_street]
|
||||
assert_nil customers(:zaphod).attributes[:address_city]
|
||||
assert_nil customers(:zaphod).attributes[:address_country]
|
||||
end
|
||||
|
||||
def test_allow_nil_address_set_to_nil
|
||||
customers(:zaphod).address = nil
|
||||
customers(:zaphod).save
|
||||
customers(:zaphod).reload
|
||||
assert_nil customers(:zaphod).address
|
||||
end
|
||||
|
||||
def test_nil_raises_error_when_allow_nil_is_false
|
||||
assert_raise(NoMethodError) { customers(:david).balance = nil }
|
||||
end
|
||||
|
||||
def test_allow_nil_address_loaded_when_only_some_attributes_are_nil
|
||||
customers(:zaphod).address_street = nil
|
||||
customers(:zaphod).save
|
||||
customers(:zaphod).reload
|
||||
assert_kind_of Address, customers(:zaphod).address
|
||||
assert_nil customers(:zaphod).address.street
|
||||
end
|
||||
|
||||
def test_nil_assignment_results_in_nil
|
||||
customers(:david).gps_location = GpsLocation.new('39x111')
|
||||
assert_not_nil customers(:david).gps_location
|
||||
customers(:david).gps_location = nil
|
||||
assert_nil customers(:david).gps_location
|
||||
end
|
||||
|
||||
def test_nil_return_from_converter_is_respected_when_allow_nil_is_true
|
||||
customers(:david).non_blank_gps_location = ""
|
||||
customers(:david).save
|
||||
customers(:david).reload
|
||||
assert_nil customers(:david).non_blank_gps_location
|
||||
end
|
||||
|
||||
def test_nil_return_from_converter_results_in_failure_when_allow_nil_is_false
|
||||
assert_raises(NoMethodError) do
|
||||
customers(:barney).gps_location = ""
|
||||
end
|
||||
end
|
||||
|
||||
def test_do_not_run_the_converter_when_nil_was_set
|
||||
customers(:david).non_blank_gps_location = nil
|
||||
assert_nil Customer.gps_conversion_was_run
|
||||
end
|
||||
|
||||
def test_custom_constructor
|
||||
assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s
|
||||
assert_kind_of Fullname, customers(:barney).fullname
|
||||
end
|
||||
|
||||
def test_custom_converter
|
||||
customers(:barney).fullname = 'Barnoit Gumbleau'
|
||||
assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s
|
||||
assert_kind_of Fullname, customers(:barney).fullname
|
||||
end
|
||||
end
|
||||
|
||||
class OverridingAggregationsTest < ActiveRecord::TestCase
|
||||
class Name; end
|
||||
class DifferentName; end
|
||||
|
||||
class Person < ActiveRecord::Base
|
||||
composed_of :composed_of, :mapping => %w(person_first_name first_name)
|
||||
end
|
||||
|
||||
class DifferentPerson < Person
|
||||
composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name)
|
||||
end
|
||||
|
||||
def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited
|
||||
assert_not_equal Person.reflect_on_aggregation(:composed_of),
|
||||
DifferentPerson.reflect_on_aggregation(:composed_of)
|
||||
end
|
||||
end
|
|
@ -907,6 +907,51 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_multiparameter_assignment_of_aggregation
|
||||
customer = Customer.new
|
||||
address = Address.new("The Street", "The City", "The Country")
|
||||
attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country }
|
||||
customer.attributes = attributes
|
||||
assert_equal address, customer.address
|
||||
end
|
||||
|
||||
def test_multiparameter_assignment_of_aggregation_out_of_order
|
||||
customer = Customer.new
|
||||
address = Address.new("The Street", "The City", "The Country")
|
||||
attributes = { "address(3)" => address.country, "address(2)" => address.city, "address(1)" => address.street }
|
||||
customer.attributes = attributes
|
||||
assert_equal address, customer.address
|
||||
end
|
||||
|
||||
def test_multiparameter_assignment_of_aggregation_with_missing_values
|
||||
ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do
|
||||
customer = Customer.new
|
||||
address = Address.new("The Street", "The City", "The Country")
|
||||
attributes = { "address(2)" => address.city, "address(3)" => address.country }
|
||||
customer.attributes = attributes
|
||||
end
|
||||
assert_equal("address", ex.errors[0].attribute)
|
||||
end
|
||||
|
||||
def test_multiparameter_assignment_of_aggregation_with_blank_values
|
||||
customer = Customer.new
|
||||
address = Address.new("The Street", "The City", "The Country")
|
||||
attributes = { "address(1)" => "", "address(2)" => address.city, "address(3)" => address.country }
|
||||
customer.attributes = attributes
|
||||
assert_equal Address.new(nil, "The City", "The Country"), customer.address
|
||||
end
|
||||
|
||||
def test_multiparameter_assignment_of_aggregation_with_large_index
|
||||
ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do
|
||||
customer = Customer.new
|
||||
address = Address.new("The Street", "The City", "The Country")
|
||||
attributes = { "address(1)" => "The Street", "address(2)" => address.city, "address(3000)" => address.country }
|
||||
customer.attributes = attributes
|
||||
end
|
||||
|
||||
assert_equal("address", ex.errors[0].attribute)
|
||||
end
|
||||
|
||||
def test_attributes_on_dummy_time
|
||||
# Oracle, and Sybase do not have a TIME datatype.
|
||||
return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
|
||||
|
@ -996,6 +1041,26 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_equal("c", duped_topic.title)
|
||||
end
|
||||
|
||||
def test_dup_with_aggregate_of_same_name_as_attribute
|
||||
dev = DeveloperWithAggregate.find(1)
|
||||
assert_kind_of DeveloperSalary, dev.salary
|
||||
|
||||
dup = nil
|
||||
assert_nothing_raised { dup = dev.dup }
|
||||
assert_kind_of DeveloperSalary, dup.salary
|
||||
assert_equal dev.salary.amount, dup.salary.amount
|
||||
assert !dup.persisted?
|
||||
|
||||
# test if the attributes have been dupd
|
||||
original_amount = dup.salary.amount
|
||||
dev.salary.amount = 1
|
||||
assert_equal original_amount, dup.salary.amount
|
||||
|
||||
assert dup.save
|
||||
assert dup.persisted?
|
||||
assert_not_equal dup.id, dev.id
|
||||
end
|
||||
|
||||
def test_dup_does_not_copy_associations
|
||||
author = authors(:david)
|
||||
assert_not_equal [], author.posts
|
||||
|
|
|
@ -45,6 +45,32 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
|
|||
assert_equal [], Topic.find_all_by_title("The First Topic!!")
|
||||
end
|
||||
|
||||
def test_find_all_by_one_attribute_that_is_an_aggregate
|
||||
balance = customers(:david).balance
|
||||
assert_kind_of Money, balance
|
||||
found_customers = Customer.find_all_by_balance(balance)
|
||||
assert_equal 1, found_customers.size
|
||||
assert_equal customers(:david), found_customers.first
|
||||
end
|
||||
|
||||
def test_find_all_by_two_attributes_that_are_both_aggregates
|
||||
balance = customers(:david).balance
|
||||
address = customers(:david).address
|
||||
assert_kind_of Money, balance
|
||||
assert_kind_of Address, address
|
||||
found_customers = Customer.find_all_by_balance_and_address(balance, address)
|
||||
assert_equal 1, found_customers.size
|
||||
assert_equal customers(:david), found_customers.first
|
||||
end
|
||||
|
||||
def test_find_all_by_two_attributes_with_one_being_an_aggregate
|
||||
balance = customers(:david).balance
|
||||
assert_kind_of Money, balance
|
||||
found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name)
|
||||
assert_equal 1, found_customers.size
|
||||
assert_equal customers(:david), found_customers.first
|
||||
end
|
||||
|
||||
def test_find_all_by_one_attribute_with_options
|
||||
topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC")
|
||||
assert_equal topics(:first), topics.last
|
||||
|
@ -111,6 +137,14 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
|
|||
assert_equal 17, sig38.firm_id
|
||||
end
|
||||
|
||||
def test_find_or_create_from_two_attributes_with_one_being_an_aggregate
|
||||
number_of_customers = Customer.count
|
||||
created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth")
|
||||
assert_equal number_of_customers + 1, Customer.count
|
||||
assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth")
|
||||
assert created_customer.persisted?
|
||||
end
|
||||
|
||||
def test_find_or_create_from_one_attribute_and_hash
|
||||
number_of_companies = Company.count
|
||||
sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
|
||||
|
@ -133,12 +167,38 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
|
|||
assert_equal 23, sig38.client_of
|
||||
end
|
||||
|
||||
def test_find_or_create_from_one_aggregate_attribute
|
||||
number_of_customers = Customer.count
|
||||
created_customer = Customer.find_or_create_by_balance(Money.new(123))
|
||||
assert_equal number_of_customers + 1, Customer.count
|
||||
assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123))
|
||||
assert created_customer.persisted?
|
||||
end
|
||||
|
||||
def test_find_or_create_from_one_aggregate_attribute_and_hash
|
||||
number_of_customers = Customer.count
|
||||
balance = Money.new(123)
|
||||
name = "Elizabeth"
|
||||
created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name})
|
||||
assert_equal number_of_customers + 1, Customer.count
|
||||
assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name})
|
||||
assert created_customer.persisted?
|
||||
assert_equal balance, created_customer.balance
|
||||
assert_equal name, created_customer.name
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_attribute
|
||||
sig38 = Company.find_or_initialize_by_name("38signals")
|
||||
assert_equal "38signals", sig38.name
|
||||
assert !sig38.persisted?
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_aggregate_attribute
|
||||
new_customer = Customer.find_or_initialize_by_balance(Money.new(123))
|
||||
assert_equal 123, new_customer.balance.amount
|
||||
assert !new_customer.persisted?
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected
|
||||
c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000})
|
||||
assert_equal "Fortune 1000", c.name
|
||||
|
@ -225,6 +285,13 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
|
|||
assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") }
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_aggregate_attribute_and_one_not
|
||||
new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth")
|
||||
assert_equal 123, new_customer.balance.amount
|
||||
assert_equal "Elizabeth", new_customer.name
|
||||
assert !new_customer.persisted?
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_attribute_and_hash
|
||||
sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
|
||||
assert_equal "38signals", sig38.name
|
||||
|
@ -233,6 +300,15 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
|
|||
assert !sig38.persisted?
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_aggregate_attribute_and_hash
|
||||
balance = Money.new(123)
|
||||
name = "Elizabeth"
|
||||
new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name})
|
||||
assert_equal balance, new_customer.balance
|
||||
assert_equal name, new_customer.name
|
||||
assert !new_customer.persisted?
|
||||
end
|
||||
|
||||
def test_find_last_by_one_attribute
|
||||
assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title)
|
||||
assert_nil Topic.find_last_by_title("A title with no matches")
|
||||
|
|
|
@ -83,6 +83,21 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert !Topic.exists?
|
||||
end
|
||||
|
||||
def test_exists_with_aggregate_having_three_mappings
|
||||
existing_address = customers(:david).address
|
||||
assert Customer.exists?(:address => existing_address)
|
||||
end
|
||||
|
||||
def test_exists_with_aggregate_having_three_mappings_with_one_difference
|
||||
existing_address = customers(:david).address
|
||||
assert !Customer.exists?(:address =>
|
||||
Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
|
||||
assert !Customer.exists?(:address =>
|
||||
Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
|
||||
assert !Customer.exists?(:address =>
|
||||
Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
|
||||
end
|
||||
|
||||
def test_exists_does_not_instantiate_records
|
||||
Developer.expects(:instantiate).never
|
||||
Developer.exists?
|
||||
|
@ -301,6 +316,14 @@ class FinderTest < ActiveRecord::TestCase
|
|||
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.where('customers.name' => david.name, :address => david.address).find(david.id)
|
||||
assert_raise(ActiveRecord::RecordNotFound) {
|
||||
Customer.where('customers.name' => david.name + "1", :address => david.address).find(david.id)
|
||||
}
|
||||
end
|
||||
|
||||
def test_find_on_association_proxy_conditions
|
||||
assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.where(post_id: authors(:david).posts).map(&:id).sort
|
||||
end
|
||||
|
@ -375,6 +398,48 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_nil topic.last_read
|
||||
end
|
||||
|
||||
def test_hash_condition_find_with_aggregate_having_one_mapping
|
||||
balance = customers(:david).balance
|
||||
assert_kind_of Money, balance
|
||||
found_customer = Customer.where(:balance => balance).first
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate
|
||||
gps_location = customers(:david).gps_location
|
||||
assert_kind_of GpsLocation, gps_location
|
||||
found_customer = Customer.where(:gps_location => gps_location).first
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value
|
||||
balance = customers(:david).balance
|
||||
assert_kind_of Money, balance
|
||||
found_customer = Customer.where(:balance => balance.amount).first
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value
|
||||
gps_location = customers(:david).gps_location
|
||||
assert_kind_of GpsLocation, gps_location
|
||||
found_customer = Customer.where(:gps_location => gps_location.gps_location).first
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_hash_condition_find_with_aggregate_having_three_mappings
|
||||
address = customers(:david).address
|
||||
assert_kind_of Address, address
|
||||
found_customer = Customer.where(:address => address).first
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not
|
||||
address = customers(:david).address
|
||||
assert_kind_of Address, address
|
||||
found_customer = Customer.where(:address => address, :name => customers(:david).name).first
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_condition_utc_time_interpolation_with_default_timezone_local
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :local do
|
||||
|
@ -548,6 +613,40 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50)
|
||||
end
|
||||
|
||||
def test_find_by_one_attribute_that_is_an_aggregate
|
||||
address = customers(:david).address
|
||||
assert_kind_of Address, address
|
||||
found_customer = Customer.find_by_address(address)
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_find_by_one_attribute_that_is_an_aggregate_with_one_attribute_difference
|
||||
address = customers(:david).address
|
||||
assert_kind_of Address, address
|
||||
missing_address = Address.new(address.street, address.city, address.country + "1")
|
||||
assert_nil Customer.find_by_address(missing_address)
|
||||
missing_address = Address.new(address.street, address.city + "1", address.country)
|
||||
assert_nil Customer.find_by_address(missing_address)
|
||||
missing_address = Address.new(address.street + "1", address.city, address.country)
|
||||
assert_nil Customer.find_by_address(missing_address)
|
||||
end
|
||||
|
||||
def test_find_by_two_attributes_that_are_both_aggregates
|
||||
balance = customers(:david).balance
|
||||
address = customers(:david).address
|
||||
assert_kind_of Money, balance
|
||||
assert_kind_of Address, address
|
||||
found_customer = Customer.find_by_balance_and_address(balance, address)
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_find_by_two_attributes_with_one_being_an_aggregate
|
||||
balance = customers(:david).balance
|
||||
assert_kind_of Money, balance
|
||||
found_customer = Customer.find_by_balance_and_name(balance, customers(:david).name)
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching
|
||||
# ensure this test can run independently of order
|
||||
class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit)
|
||||
|
|
|
@ -83,6 +83,30 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_aggregation_reflection
|
||||
reflection_for_address = AggregateReflection.new(
|
||||
:composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
|
||||
)
|
||||
|
||||
reflection_for_balance = AggregateReflection.new(
|
||||
:composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
|
||||
)
|
||||
|
||||
reflection_for_gps_location = AggregateReflection.new(
|
||||
:composed_of, :gps_location, nil, { }, Customer
|
||||
)
|
||||
|
||||
assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
|
||||
assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance)
|
||||
assert Customer.reflect_on_all_aggregations.include?(reflection_for_address)
|
||||
|
||||
assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address)
|
||||
|
||||
assert_equal Address, Customer.reflect_on_aggregation(:address).klass
|
||||
|
||||
assert_equal Money, Customer.reflect_on_aggregation(:balance).klass
|
||||
end
|
||||
|
||||
def test_reflect_on_all_autosave_associations
|
||||
expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] }
|
||||
received = Pirate.reflect_on_all_autosave_associations
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
class Customer < ActiveRecord::Base
|
||||
cattr_accessor :gps_conversion_was_run
|
||||
|
||||
composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
|
||||
composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
|
||||
composed_of :gps_location, :allow_nil => true
|
||||
composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location),
|
||||
:converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)}
|
||||
composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse
|
||||
end
|
||||
|
||||
class Address
|
||||
|
|
|
@ -64,6 +64,12 @@ class AuditLog < ActiveRecord::Base
|
|||
belongs_to :unvalidated_developer, :class_name => 'Developer'
|
||||
end
|
||||
|
||||
DeveloperSalary = Struct.new(:amount)
|
||||
class DeveloperWithAggregate < ActiveRecord::Base
|
||||
self.table_name = 'developers'
|
||||
composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)]
|
||||
end
|
||||
|
||||
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
|
||||
self.table_name = 'developers'
|
||||
has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id'
|
||||
|
|
Loading…
Reference in a new issue