diff options
Diffstat (limited to 'activerecord/lib/active_record')
12 files changed, 227 insertions, 93 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index a5d3a50ef1..9eee7f2d98 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -10,10 +10,10 @@ module ActiveRecord end unless self.new_record? end - # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes + # 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) + # 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). Example: # # class Customer < ActiveRecord::Base @@ -30,10 +30,10 @@ module ActiveRecord # class Money # include Comparable # attr_reader :amount, :currency - # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } - # - # def initialize(amount, currency = "USD") - # @amount, @currency = amount, currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency # end # # def exchange_to(other_currency) @@ -56,19 +56,19 @@ module ActiveRecord # # class Address # attr_reader :street, :city - # def initialize(street, city) - # @street, @city = street, city + # def initialize(street, city) + # @street, @city = street, city # end # - # def close_to?(other_address) - # city == other_address.city + # 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 any other attribute, though: @@ -87,8 +87,8 @@ module ActiveRecord # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") # customer.address = Address.new("May Street", "Chicago") - # customer.address_street # => "May Street" - # customer.address_city # => "Chicago" + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" # # == Writing value objects # @@ -103,12 +103,51 @@ module ActiveRecord # 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 + # 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 @@ -122,47 +161,71 @@ module ActiveRecord # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. # # Options are: - # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred + # * <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 a number of mapping arrays (attribute, parameter) that bind an attribute name - # to a constructor parameter on the value class. - # * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped - # attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes. + # * <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 the attribute in the value object. The order in which mappings are defined determine 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+. - # - # An optional block can be passed to convert the argument that is passed to the writer method into an instance of - # <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>. + # * <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>. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) - # composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money } + # 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 = {}, &block) - options.assert_valid_keys(:class_name, :mapping, :allow_nil) + 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 ] + 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 + allow_nil = options[:allow_nil] || false + constructor = options[:constructor] || :new + converter = options[:converter] || block + + ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given? + + reader_method(name, class_name, mapping, allow_nil, constructor) + writer_method(name, class_name, mapping, allow_nil, converter) - reader_method(name, class_name, mapping, allow_nil) - writer_method(name, class_name, mapping, allow_nil, block) - create_reflection(:composed_of, part_id, options, self) end private - def reader_method(name, class_name, mapping, allow_nil) + def reader_method(name, class_name, mapping, allow_nil, constructor) module_eval do define_method(name) do |*args| force_reload = args.first || false if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)})) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = case constructor + when Symbol + class_name.constantize.send(constructor, *attrs) + when Proc, Method + constructor.call(*attrs) + else + raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' + end + instance_variable_set("@#{name}", object) end instance_variable_get("@#{name}") end @@ -170,14 +233,23 @@ module ActiveRecord end - def writer_method(name, class_name, mapping, allow_nil, conversion) + def writer_method(name, class_name, mapping, allow_nil, converter) module_eval do define_method("#{name}=") do |part| if part.nil? && allow_nil mapping.each { |pair| self[pair.first] = nil } instance_variable_set("@#{name}", nil) else - part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil? + unless part.is_a?(class_name.constantize) || converter.nil? + part = case converter + when Symbol + class_name.constantize.send(converter, part) + when Proc, Method + converter.call(part) + else + raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' + end + end mapping.each { |pair| self[pair.first] = part.send(pair.last) } instance_variable_set("@#{name}", part.freeze) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6405071354..aca2d770fc 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -739,9 +739,6 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). - # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -855,8 +852,6 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -973,8 +968,6 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -1190,8 +1183,6 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. - # [:accessible<] - # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # has_and_belongs_to_many :projects @@ -1266,8 +1257,6 @@ module ActiveRecord association = association_proxy_class.new(self, reflection) end - new_value = reflection.klass.new(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) - if association_proxy_class == HasOneThroughAssociation association.create_through_record(new_value) self.send(reflection.name, new_value) @@ -1509,29 +1498,35 @@ module ActiveRecord end end - def create_has_many_reflection(association_id, options, &extension) - options.assert_valid_keys( - :class_name, :table_name, :foreign_key, :primary_key, - :dependent, - :select, :conditions, :include, :order, :group, :limit, :offset, - :as, :through, :source, :source_type, - :uniq, - :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate, :accessible - ) + mattr_accessor :valid_keys_for_has_many_association + @@valid_keys_for_has_many_association = [ + :class_name, :table_name, :foreign_key, :primary_key, + :dependent, + :select, :conditions, :include, :order, :group, :limit, :offset, + :as, :through, :source, :source_type, + :uniq, + :finder_sql, :counter_sql, + :before_add, :after_add, :before_remove, :after_remove, + :extend, :readonly, + :validate + ] + def create_has_many_reflection(association_id, options, &extension) + options.assert_valid_keys(valid_keys_for_has_many_association) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) create_reflection(:has_many, association_id, options, self) end - def create_has_one_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key, :accessible - ) + mattr_accessor :valid_keys_for_has_one_association + @@valid_keys_for_has_one_association = [ + :class_name, :foreign_key, :remote, :select, :conditions, :order, + :include, :dependent, :counter_cache, :extend, :as, :readonly, + :validate, :primary_key + ] + def create_has_one_reflection(association_id, options) + options.assert_valid_keys(valid_keys_for_has_one_association) create_reflection(:has_one, association_id, options, self) end @@ -1542,12 +1537,15 @@ module ActiveRecord create_reflection(:has_one, association_id, options, self) end - def create_belongs_to_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, - :counter_cache, :extend, :polymorphic, :readonly, :validate, :accessible - ) + mattr_accessor :valid_keys_for_belongs_to_association + @@valid_keys_for_belongs_to_association = [ + :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, + :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, + :validate + ] + def create_belongs_to_reflection(association_id, options) + options.assert_valid_keys(valid_keys_for_belongs_to_association) reflection = create_reflection(:belongs_to, association_id, options, self) if options[:polymorphic] @@ -1565,7 +1563,7 @@ module ActiveRecord :finder_sql, :delete_sql, :insert_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate, :accessible + :validate ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 168443e092..8de528f638 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -110,8 +110,6 @@ module ActiveRecord @owner.transaction do flatten_deeper(records).each do |record| - record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash) - raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| result &&= insert_record(record) unless @owner.new_record? @@ -286,10 +284,6 @@ module ActiveRecord # Replace this collection with +other_array+ # This will perform a diff and delete/add only records that have changed. def replace(other_array) - other_array.map! do |val| - val.is_a?(Hash) ? @reflection.klass.new(val) : val - end if @reflection.options[:accessible] - other_array.each { |val| raise_on_type_mismatch(val) } load_target @@ -377,7 +371,9 @@ module ActiveRecord def create_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) ensure_owner_is_not_new - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + @reflection.build_association(attrs) + end if block_given? add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } else @@ -387,7 +383,7 @@ module ActiveRecord def build_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.klass.new(attrs) + record = @reflection.build_association(attrs) if block_given? add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } else diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 7c28cbdd07..f05c6be075 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,11 +2,11 @@ module ActiveRecord module Associations class BelongsToAssociation < AssociationProxy #:nodoc: def create(attributes = {}) - replace(@reflection.klass.create(attributes)) + replace(@reflection.create_association(attributes)) end def build(attributes = {}) - replace(@reflection.klass.new(attributes)) + replace(@reflection.build_association(attributes)) end def replace(record) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 84fa900f46..ebd2bf768c 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -10,14 +10,14 @@ module ActiveRecord def create!(attrs = nil) @reflection.klass.transaction do - self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!) + self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!) object end end def create(attrs = nil) @reflection.klass.transaction do - self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create) + self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association) object end end @@ -47,8 +47,9 @@ module ActiveRecord return false unless record.save end end - klass = @reflection.through_reflection.klass - @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! } + through_reflection = @reflection.through_reflection + klass = through_reflection.klass + @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! } end # TODO - add dependent option support diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 18733255d2..c92ef5c2c9 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -7,15 +7,21 @@ module ActiveRecord end def create(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.create(attrs) } + new_record(replace_existing) do |reflection| + reflection.create_association(attrs) + end end def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.create!(attrs) } + new_record(replace_existing) do |reflection| + reflection.create_association!(attrs) + end end def build(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.new(attrs) } + new_record(replace_existing) do |reflection| + reflection.build_association(attrs) + end end def replace(obj, dont_save = false) @@ -91,7 +97,9 @@ module ActiveRecord # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + yield @reflection + end if replace_existing replace(record, true) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 907495a416..fc6d762fcd 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -274,7 +274,7 @@ module ActiveRecord #:nodoc: # == Dynamic attribute-based finders # # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by - # appending the name of an attribute to <tt>find_by_</tt> or <tt>find_all_by_</tt>, so you get finders like <tt>Person.find_by_user_name</tt>, + # appending the name of an attribute to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt>, so you get finders like <tt>Person.find_by_user_name</tt>, # <tt>Person.find_all_by_last_name</tt>, and <tt>Payment.find_by_transaction_id</tt>. So instead of writing # <tt>Person.find(:first, :conditions => ["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. # And instead of writing <tt>Person.find(:all, :conditions => ["last_name = ?", last_name])</tt>, you just do <tt>Person.find_all_by_last_name(last_name)</tt>. @@ -287,6 +287,7 @@ module ActiveRecord #:nodoc: # It's even possible to use all the additional parameters to find. For example, the full interface for <tt>Payment.find_all_by_amount</tt> # is actually <tt>Payment.find_all_by_amount(amount, options)</tt>. And the full interface to <tt>Person.find_by_user_name</tt> is # actually <tt>Person.find_by_user_name(user_name, options)</tt>. So you could call <tt>Payment.find_all_by_amount(50, :order => "created_on")</tt>. + # Also you may call <tt>Payment.find_last_by_amount(amount, options)</tt> returning the last record matching that amount and options. # # The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with # <tt>find_or_create_by_</tt> and will return the object if it already exists and otherwise creates it, then returns it. Protected attributes won't be set unless they are given in a block. For example: @@ -768,10 +769,24 @@ module ActiveRecord #:nodoc: # :order => 'created_at', :limit => 5 ) def update_all(updates, conditions = nil, options = {}) sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} " + scope = scope(:find) - add_conditions!(sql, conditions, scope) - add_order!(sql, options[:order], nil) - add_limit!(sql, options, nil) + + select_sql = "" + add_conditions!(select_sql, conditions, scope) + + if options.has_key?(:limit) || (scope && scope[:limit]) + # Only take order from scope if limit is also provided by scope, this + # is useful for updating a has_many association with a limit. + add_order!(select_sql, options[:order], scope) + + add_limit!(select_sql, options, scope) + sql.concat(connection.limited_update_conditions(select_sql, quoted_table_name, connection.quote_column_name(primary_key))) + else + add_order!(select_sql, options[:order], nil) + sql.concat(select_sql) + end + connection.update(sql, "#{name} Update") end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index a675af4787..80992dd34b 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -217,7 +217,7 @@ module ActiveRecord sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) - sql << ') AS #{aggregate_alias}_subquery' if use_workaround + sql << ") AS #{aggregate_alias}_subquery" if use_workaround sql end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index aaf9e2e73f..8fc89de22b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -153,6 +153,10 @@ module ActiveRecord "=" end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index f1d13698c3..c2a0fb72bf 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -523,6 +523,10 @@ module ActiveRecord "= BINARY" end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + private def connect @connection.reconnect = true if @connection.respond_to?(:reconnect=) diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index b105b919f5..f4a5712981 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -8,7 +8,8 @@ module ActiveRecord def initialize(method) @finder = :find_initial case method.to_s - when /^find_(all_by|by)_([_a-zA-Z]\w*)$/ + when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/ + @finder = :find_last if $1 == 'last_by' @finder = :find_every if $1 == 'all_by' names = $2 when /^find_by_([_a-zA-Z]\w*)\!$/ diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 935b1939d8..a1b498eceb 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -129,10 +129,45 @@ module ActiveRecord # Holds all the meta-data about an association as it was specified in the Active Record class. class AssociationReflection < MacroReflection #:nodoc: + # Returns the target association's class: + # + # class Author < ActiveRecord::Base + # has_many :books + # end + # + # Author.reflect_on_association(:books).klass + # # => Book + # + # <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate + # a new association object. Use +build_association+ or +create_association+ + # instead. This allows plugins to hook into association object creation. def klass @klass ||= active_record.send(:compute_type, class_name) end + # Returns a new, unsaved instance of the associated class. +options+ will + # be passed to the class's constructor. + def build_association(*options) + klass.new(*options) + end + + # Creates a new instance of the associated class, and immediates saves it + # with ActiveRecord::Base#save. +options+ will be passed to the class's + # creation method. Returns the newly created object. + def create_association(*options) + klass.create(*options) + end + + # Creates a new instance of the associated class, and immediates saves it + # with ActiveRecord::Base#save!. +options+ will be passed to the class's + # creation method. If the created record doesn't pass validations, then an + # exception will be raised. + # + # Returns the newly created object. + def create_association!(*options) + klass.create!(*options) + end + def table_name @table_name ||= klass.table_name end |