diff options
Diffstat (limited to 'activerecord/lib/active_record')
108 files changed, 7399 insertions, 5530 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 83a9ab46c5..90d3b58c78 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -4,17 +4,15 @@ module ActiveRecord extend ActiveSupport::Concern def clear_aggregation_cache #:nodoc: - self.class.reflect_on_all_aggregations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end unless self.new_record? + @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 + # 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 @@ -70,9 +68,9 @@ module ActiveRecord # 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 + # 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: # # customer.balance = Money.new(20) # sets the Money value object and the attribute @@ -82,8 +80,8 @@ module ActiveRecord # 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. + # 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" @@ -94,42 +92,42 @@ module ActiveRecord # # == 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 + # 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 + # 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. This - # is exemplified by the Money#exchange_to method that returns a new value object instead of changing - # its own values. Active Record won't persist value objects that have been changed through means + # 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. This + # is exemplified by the Money#exchange_to method that 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 + # 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 + # 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 + # 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 + # 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 + # 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 @@ -157,38 +155,38 @@ module ActiveRecord # # == 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 + # 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.find(:all, :conditions => {:balance => Money.new(20, "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 + # * <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 the attribute in the value object. The - # order in which mappings are defined determine the order in which attributes are sent to the + # * <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 + # 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 + # * <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 + # * <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: @@ -222,53 +220,32 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) - module_eval do - define_method(name) do |*args| - force_reload = args.first || false - - unless instance_variable_defined?("@#{name}") - instance_variable_set("@#{name}", nil) - end - - if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - 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}") + 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) - 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 - 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) + define_method("#{name}=") do |part| + if part.nil? && allow_nil + mapping.each { |pair| self[pair.first] = nil } + @aggregation_cache[name] = nil + else + unless part.is_a?(class_name.constantize) || converter.nil? + part = converter.respond_to?(:call) ? + converter.call(part) : + class_name.constantize.send(converter, part) end + + mapping.each { |pair| self[pair.first] = part.send(pair.last) } + @aggregation_cache[name] = part.freeze end end end diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb deleted file mode 100644 index 0f0fdc2e21..0000000000 --- a/activerecord/lib/active_record/association_preload.rb +++ /dev/null @@ -1,403 +0,0 @@ -require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/enumerable' - -module ActiveRecord - # See ActiveRecord::AssociationPreload::ClassMethods for documentation. - module AssociationPreload #:nodoc: - extend ActiveSupport::Concern - - # Implements the details of eager loading of Active Record associations. - # Application developers should not use this module directly. - # - # <tt>ActiveRecord::Base</tt> is extended with this module. The source code in - # <tt>ActiveRecord::Base</tt> references methods defined in this module. - # - # Note that 'eager loading' and 'preloading' are actually the same thing. - # However, there are two different eager loading strategies. - # - # The first one is by using table joins. This was only strategy available - # prior to Rails 2.1. Suppose that you have an Author model with columns - # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using - # this strategy, Active Record would try to retrieve all data for an author - # and all of its books via a single query: - # - # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.id - # WHERE authors.name = 'Ken Akamatsu' - # - # However, this could result in many rows that contain redundant data. After - # having received the first row, we already have enough data to instantiate - # the Author object. In all subsequent rows, only the data for the joined - # 'books' table is useful; the joined 'authors' data is just redundant, and - # processing this redundant data takes memory and CPU time. The problem - # quickly becomes worse and worse as the level of eager loading increases - # (i.e. if Active Record is to eager load the associations' associations as - # well). - # - # The second strategy is to use multiple database queries, one for each - # level of association. Since Rails 2.1, this is the default strategy. In - # situations where a table join is necessary (e.g. when the +:conditions+ - # option references an association's column), it will fallback to the table - # join strategy. - # - # See also ActiveRecord::Associations::ClassMethods, which explains eager - # loading in a more high-level (application developer-friendly) manner. - module ClassMethods - protected - - # Eager loads the named associations for the given Active Record record(s). - # - # In this description, 'association name' shall refer to the name passed - # to an association creation method. For example, a model that specifies - # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association - # names +:author+ and +:buyers+. - # - # == Parameters - # +records+ is an array of ActiveRecord::Base. This array needs not be flat, - # i.e. +records+ itself may also contain arrays of records. In any case, - # +preload_associations+ will preload the all associations records by - # flattening +records+. - # - # +associations+ specifies one or more associations that you want to - # preload. It may be: - # - a Symbol or a String which specifies a single association name. For - # example, specifying +:books+ allows this method to preload all books - # for an Author. - # - an Array which specifies multiple association names. This array - # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt> - # allows this method to preload an author's avatar as well as all of his - # books. - # - a Hash which specifies multiple association names, as well as - # association names for the to-be-preloaded association objects. For - # example, specifying <tt>{ :author => :avatar }</tt> will preload a - # book's author, as well as that author's avatar. - # - # +:associations+ has the same format as the +:include+ option for - # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this: - # - # :books - # [ :books, :author ] - # { :author => :avatar } - # [ :books, { :author => :avatar } ] - # - # +preload_options+ contains options that will be passed to ActiveRecord::Base#find - # (which is called under the hood for preloading records). But it is passed - # only one level deep in the +associations+ argument, i.e. it's not passed - # to the child associations when +associations+ is a Hash. - def preload_associations(records, associations, preload_options={}) - records = Array.wrap(records).compact.uniq - return if records.empty? - case associations - when Array then associations.each {|association| preload_associations(records, association, preload_options)} - when Symbol, String then preload_one_association(records, associations.to_sym, preload_options) - when Hash then - associations.each do |parent, child| - raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol) - preload_associations(records, parent, preload_options) - reflection = reflections[parent] - parents = records.sum { |record| Array.wrap(record.send(reflection.name)) } - unless parents.empty? - parents.first.class.preload_associations(parents, child) - end - end - end - end - - private - - # Preloads a specific named association for the given records. This is - # called by +preload_associations+ as its base case. - def preload_one_association(records, association, preload_options={}) - class_to_reflection = {} - # Not all records have the same class, so group then preload - # group on the reflection itself so that if various subclass share the same association then - # we do not split them unnecessarily - records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records| - raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection - - # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus, - # the following could call 'preload_belongs_to_association', - # 'preload_has_many_association', etc. - send("preload_#{reflection.macro}_association", _records, reflection, preload_options) - end - end - - def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) - parent_records.each do |parent_record| - association_proxy = parent_record.send(reflection_name) - association_proxy.loaded - association_proxy.target.push(*Array.wrap(associated_record)) - - association_proxy.__send__(:set_inverse_instance, associated_record, parent_record) - end - end - - def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) - parent_records.each do |parent_record| - parent_record.send("set_#{reflection_name}_target", associated_record) - end - end - - def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) - associated_records.each do |associated_record| - mapped_records = id_to_record_map[associated_record[key].to_s] - add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record) - end - end - - def set_association_single_records(id_to_record_map, reflection_name, associated_records, key) - seen_keys = {} - associated_records.each do |associated_record| - #this is a has_one or belongs_to: there should only be one record. - #Unfortunately we can't (in portable way) ask the database for - #'all records where foo_id in (x,y,z), but please - # only one row per distinct foo_id' so this where we enforce that - next if seen_keys[associated_record[key].to_s] - seen_keys[associated_record[key].to_s] = true - mapped_records = id_to_record_map[associated_record[key].to_s] - mapped_records.each do |mapped_record| - association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) - association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record) - end - end - - id_to_record_map.each do |id, records| - next if seen_keys.include?(id.to_s) - records.each {|record| record.send("set_#{reflection_name}_target", nil) } - end - end - - # Given a collection of Active Record objects, constructs a Hash which maps - # the objects' IDs to the relevant objects. Returns a 2-tuple - # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash, - # and +ids+ is an Array of record IDs. - def construct_id_map(records, primary_key=nil) - id_to_record_map = {} - ids = [] - records.each do |record| - primary_key ||= record.class.primary_key - ids << record[primary_key] - mapped_records = (id_to_record_map[ids.last.to_s] ||= []) - mapped_records << record - end - ids.uniq! - return id_to_record_map, ids - end - - def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) - table_name = reflection.klass.quoted_table_name - id_to_record_map, ids = construct_id_map(records) - records.each {|record| record.send(reflection.name).loaded} - options = reflection.options - - conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(reflection, preload_options) - - associated_records = reflection.klass.unscoped.where([conditions, ids]). - includes(options[:include]). - joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). - select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). - order(options[:order]).to_a - - set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') - end - - def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) - options = reflection.options - records.each {|record| record.send("set_#{reflection.name}_target", nil)} - if options[:through] - through_records = preload_through_records(records, reflection, options[:through]) - through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name - unless through_records.empty? - source = reflection.source_reflection.name - through_records.first.class.preload_associations(through_records, source) - if through_reflection.macro == :belongs_to - rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key) - rev_primary_key = through_reflection.klass.primary_key - through_records.each do |through_record| - add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s], - reflection.name, through_record.send(source)) - end - else - through_records.each do |through_record| - add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s], - reflection.name, through_record.send(source)) - end - end - end - else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) - end - end - - def preload_has_many_association(records, reflection, preload_options={}) - return if records.first.send(reflection.name).loaded? - options = reflection.options - - primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) - records.each {|record| record.send(reflection.name).loaded} - - if options[:through] - through_records = preload_through_records(records, reflection, options[:through]) - through_reflection = reflections[options[:through]] - unless through_records.empty? - source = reflection.source_reflection.name - through_records.first.class.preload_associations(through_records, source, options) - through_records.each do |through_record| - through_record_id = through_record[reflection.through_reflection_primary_key].to_s - add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source)) - end - end - - else - set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), - reflection.primary_key_name) - end - end - - def preload_through_records(records, reflection, through_association) - through_reflection = reflections[through_association] - through_primary_key = through_reflection.primary_key_name - - through_records = [] - if reflection.options[:source_type] - interface = reflection.source_reflection.options[:foreign_type] - preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} - - records.compact! - records.first.class.preload_associations(records, through_association, preload_options) - - # Dont cache the association - we would only be caching a subset - records.each do |record| - proxy = record.send(through_association) - - if proxy.respond_to?(:target) - through_records.concat Array.wrap(proxy.target) - proxy.reset - else # this is a has_one :through reflection - through_records << proxy if proxy - end - end - else - options = {} - options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] - options[:order] = reflection.options[:order] - options[:conditions] = reflection.options[:conditions] - records.first.class.preload_associations(records, through_association, options) - - records.each do |record| - through_records.concat Array.wrap(record.send(through_association)) - end - end - through_records - end - - def preload_belongs_to_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") - options = reflection.options - primary_key_name = reflection.primary_key_name - - if options[:polymorphic] - polymorph_type = options[:foreign_type] - klasses_and_ids = {} - - # Construct a mapping from klass to a list of ids to load and a mapping of those ids back - # to their parent_records - records.each do |record| - if klass = record.send(polymorph_type) - klass_id = record.send(primary_key_name) - if klass_id - id_map = klasses_and_ids[klass] ||= {} - id_list_for_klass_id = (id_map[klass_id.to_s] ||= []) - id_list_for_klass_id << record - end - end - end - klasses_and_ids = klasses_and_ids.to_a - else - id_map = {} - records.each do |record| - key = record.send(primary_key_name) - if key - mapped_records = (id_map[key.to_s] ||= []) - mapped_records << record - end - end - klasses_and_ids = [[reflection.klass.name, id_map]] - end - - klasses_and_ids.each do |klass_and_id| - klass_name, id_map = *klass_and_id - next if id_map.empty? - klass = klass_name.constantize - - table_name = klass.quoted_table_name - primary_key = reflection.options[:primary_key] || klass.primary_key - column_type = klass.columns.detect{|c| c.name == primary_key}.type - - ids = id_map.keys.map do |id| - if column_type == :integer - id.to_i - elsif column_type == :float - id.to_f - else - id - end - end - - conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(reflection, preload_options) - - associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a - - set_association_single_records(id_map, reflection.name, associated_records, primary_key) - end - end - - def find_associated_records(ids, reflection, preload_options) - options = reflection.options - table_name = reflection.klass.quoted_table_name - - if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" - else - foreign_key = reflection.primary_key_name - conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" - end - - conditions << append_conditions(reflection, preload_options) - - find_options = { - :select => preload_options[:select] || options[:select] || "#{table_name}.*", - :include => preload_options[:include] || options[:include], - :conditions => [conditions, ids], - :joins => options[:joins], - :group => preload_options[:group] || options[:group], - :order => preload_options[:order] || options[:order] - } - - reflection.klass.scoped.apply_finder_options(find_options).to_a - end - - - def interpolate_sql_for_preload(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - - def append_conditions(reflection, preload_options) - sql = "" - sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions - sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions] - sql - end - - def in_or_equals_for_ids(ids) - ids.size > 1 ? "IN (?)" : "= ?" - end - end - end -end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 73c0900c8b..08fb6bf3c4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/class/attribute' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -18,32 +19,36 @@ module ActiveRecord end end - class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc: + class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.") end end + class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection) + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + end + end + class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") end end + class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection, through_reflection) + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + end + end + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") - end - end - - class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc: - def initialize(reflection) - through_reflection = reflection.through_reflection - source_reflection = reflection.source_reflection - super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.") + super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -65,6 +70,12 @@ module ActiveRecord end end + class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc + def initialize(owner, reflection) + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + end + end + class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc: def initialize(reflection) super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).") @@ -93,8 +104,8 @@ module ActiveRecord # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot delete record because of dependent #{reflection.name}") + def initialize(name) + super("Cannot delete record because of dependent #{name}") end end @@ -104,44 +115,75 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. - autoload :AssociationCollection, 'active_record/associations/association_collection' - autoload :AssociationProxy, 'active_record/associations/association_proxy' - autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' + autoload :Association, 'active_record/associations/association' + autoload :SingularAssociation, 'active_record/associations/singular_association' + autoload :CollectionAssociation, 'active_record/associations/collection_association' + autoload :CollectionProxy, 'active_record/associations/collection_proxy' + + autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' - autoload :HasManyAssociation, 'active_record/associations/has_many_association' - autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' - autoload :HasOneAssociation, 'active_record/associations/has_one_association' - autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' + autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' + autoload :HasManyAssociation, 'active_record/associations/has_many_association' + autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' + autoload :HasOneAssociation, 'active_record/associations/has_one_association' + autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' + autoload :ThroughAssociation, 'active_record/associations/through_association' + + module Builder #:nodoc: + autoload :Association, 'active_record/associations/builder/association' + autoload :SingularAssociation, 'active_record/associations/builder/singular_association' + autoload :CollectionAssociation, 'active_record/associations/builder/collection_association' + + autoload :BelongsTo, 'active_record/associations/builder/belongs_to' + autoload :HasOne, 'active_record/associations/builder/has_one' + autoload :HasMany, 'active_record/associations/builder/has_many' + autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many' + end + + autoload :Preloader, 'active_record/associations/preloader' + autoload :JoinDependency, 'active_record/associations/join_dependency' + autoload :AssociationScope, 'active_record/associations/association_scope' + autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :JoinHelper, 'active_record/associations/join_helper' # Clears out the association cache. def clear_association_cache #:nodoc: - self.class.reflect_on_all_associations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end unless self.new_record? + @association_cache.clear if persisted? + end + + # :nodoc: + attr_reader :association_cache + + # Returns the association instance for the given name, instantiating it if it doesn't already exist + def association(name) #:nodoc: + association = association_instance_get(name) + + if association.nil? + reflection = self.class.reflect_on_association(name) + association = reflection.association_class.new(self, reflection) + association_instance_set(name, association) + end + + association end private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - ivar = "@#{name}" - if instance_variable_defined?(ivar) - association = instance_variable_get(ivar) - association if association.respond_to?(:loaded?) - end + @association_cache[name.to_sym] end # Set the specified association instance. def association_instance_set(name, association) - instance_variable_set("@#{name}", association) + @association_cache[name] = association end - # Associations are a set of macro-like class methods for tying objects together through - # foreign keys. They express relationships like "Project has one Project Manager" - # or "Project belongs to a Portfolio". Each macro adds a number of methods to the - # class which are specialized according to the collection or association symbol and the + # Associations are a set of macro-like class methods for tying objects together through + # foreign keys. They express relationships like "Project has one Project Manager" + # or "Project belongs to a Portfolio". Each macro adds a number of methods to the + # class which are specialized according to the collection or association symbol and the # options hash. It works much the same way as Ruby's own <tt>attr*</tt> - # methods. + # methods. # # class Project < ActiveRecord::Base # belongs_to :portfolio @@ -150,7 +192,7 @@ module ActiveRecord # has_and_belongs_to_many :categories # end # - # The project class now has the following methods (and more) to ease the traversal and + # The project class now has the following methods (and more) to ease the traversal and # manipulation of its relationships: # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> @@ -162,8 +204,8 @@ module ActiveRecord # # === A word of warning # - # Don't create associations that have the same name as instance methods of - # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # Don't create associations that have the same name as instance methods of + # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to # its model, it will override the inherited method and break things. # For instance, +attributes+ and +connection+ would be bad choices for association names. # @@ -177,7 +219,7 @@ module ActiveRecord # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X - # other.create!(attributes={}) | | | X + # create_other!(attributes={}) | X | | X # # ===Collection associations (one-to-many / many-to-many) # | | | has_many @@ -200,10 +242,9 @@ module ActiveRecord # others.empty? | X | X | X # others.clear | X | X | X # others.delete(other,other,...) | X | X | X - # others.delete_all | X | X | + # others.delete_all | X | X | X # others.destroy_all | X | X | X # others.find(*args) | X | X | X - # others.find_first | X | | # others.exists? | X | X | X # others.uniq | X | X | X # others.reset | X | X | X @@ -274,7 +315,7 @@ module ActiveRecord # # == Is it a +belongs_to+ or +has_one+ association? # - # Both express a 1-1 relationship. The difference is mostly where to place the foreign + # Both express a 1-1 relationship. The difference is mostly where to place the foreign # key, which goes on the table for the class declaring the +belongs_to+ relationship. # # class User < ActiveRecord::Base @@ -304,43 +345,49 @@ module ActiveRecord # # == Unsaved objects and associations # - # You can manipulate objects and associations before they are saved to the database, but - # there is some special behavior you should be aware of, mostly involving the saving of + # You can manipulate objects and associations before they are saved to the database, but + # there is some special behavior you should be aware of, mostly involving the saving of # associated objects. # # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>, # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it # to +true+ will _always_ save the members, whereas setting it to +false+ will - # _never_ save the members. + # _never_ save the members. More details about :autosave option is available at + # autosave_association.rb . # # === One-to-one associations # - # * Assigning an object to a +has_one+ association automatically saves that object and - # the object being replaced (if there is one), in order to update their primary - # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). - # * If either of these saves fail (due to one of the objects being invalid) the assignment - # statement returns +false+ and the assignment is cancelled. - # * If you wish to assign an object to a +has_one+ association without saving it, - # use the <tt>association.build</tt> method (documented below). - # * Assigning an object to a +belongs_to+ association does not save the object, since - # the foreign key field belongs on the parent. It does not save the parent either. + # * Assigning an object to a +has_one+ association automatically saves that object and + # the object being replaced (if there is one), in order to update their foreign + # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). + # * If either of these saves fail (due to one of the objects being invalid), an + # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # cancelled. + # * If you wish to assign an object to a +has_one+ association without saving it, + # use the <tt>build_association</tt> method (documented below). The object being + # replaced will still be saved to update its foreign key. + # * Assigning an object to a +belongs_to+ association does not save the object, since + # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # - # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically - # saves that object, except if the parent object (the owner of the collection) is not yet - # stored in the database. - # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) - # fails, then <tt>push</tt> returns +false+. - # * You can add an object to a collection without automatically saving it by using the - # <tt>collection.build</tt> method (documented below). - # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically - # saved when the parent is saved. + # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. + # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) + # fails, then <tt>push</tt> returns +false+. + # * If saving fails while replacing the collection (via <tt>association=</tt>), an + # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # cancelled. + # * You can add an object to a collection without automatically saving it by using the + # <tt>collection.build</tt> method (documented below). + # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically + # saved when the parent is saved. # # === Association callbacks # - # Similar to the normal callbacks that hook into the lifecycle of an Active Record object, - # you can also define callbacks that get triggered when you add an object to or remove an + # Similar to the normal callbacks that hook into the life cycle of an Active Record object, + # you can also define callbacks that get triggered when you add an object to or remove an # object from an association collection. # # class Project @@ -354,20 +401,20 @@ module ActiveRecord # It's possible to stack callbacks by passing them as an array. Example: # # class Project - # has_and_belongs_to_many :developers, + # has_and_belongs_to_many :developers, # :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] # end # # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. # - # Should any of the +before_add+ callbacks throw an exception, the object does not get - # added to the collection. Same with the +before_remove+ callbacks; if an exception is + # Should any of the +before_add+ callbacks throw an exception, the object does not get + # added to the collection. Same with the +before_remove+ callbacks; if an exception is # thrown the object doesn't get removed. # # === Association extensions # - # The proxy objects that control the access to associations can be extended through anonymous - # modules. This is especially beneficial for adding new finders, creators, and other + # The proxy objects that control the access to associations can be extended through anonymous + # modules. This is especially beneficial for adding new finders, creators, and other # factory-type methods that are only used as part of this association. # # class Account < ActiveRecord::Base @@ -383,8 +430,8 @@ module ActiveRecord # person.first_name # => "David" # person.last_name # => "Heinemeier Hansson" # - # If you need to share the same extensions between many associations, you can use a named - # extension module. + # If you need to share the same extensions between many associations, you can use a named + # extension module. # # module FindOrCreateByNameExtension # def find_or_create_by_name(name) @@ -401,10 +448,10 @@ module ActiveRecord # has_many :people, :extend => FindOrCreateByNameExtension # end # - # If you need to use multiple named extension modules, you can specify an array of modules + # If you need to use multiple named extension modules, you can specify an array of modules # with the <tt>:extend</tt> option. - # In the case of name conflicts between methods in the modules, methods in modules later - # in the array supercede those earlier in the array. + # In the case of name conflicts between methods in the modules, methods in modules later + # in the array supercede those earlier in the array. # # class Account < ActiveRecord::Base # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] @@ -415,13 +462,13 @@ module ActiveRecord # # * +proxy_owner+ - Returns the object the association is part of. # * +proxy_reflection+ - Returns the reflection object that describes the association. - # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or + # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. # # === Association Join Models # - # Has Many associations can be configured with the <tt>:through</tt> option to use an - # explicit join model to retrieve the data. This operates similarly to a + # Has Many associations can be configured with the <tt>:through</tt> option to use an + # explicit join model to retrieve the data. This operates similarly to a # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, # callbacks, and extra attributes on the join model. Consider the following schema: # @@ -479,17 +526,76 @@ module ActiveRecord # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # - # An important caveat with going through +has_one+ or +has_many+ associations on the - # join model is that these associations are *read-only*. For example, the following + # An important caveat with going through +has_one+ or +has_many+ associations on the + # join model is that these associations are *read-only*. For example, the following # would not work following the previous example: # # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around # @group.avatars.delete(@group.avatars.last) # so would this # + # If you are using a +belongs_to+ on the join model, it is a good idea to set the + # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example + # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association): + # + # @post = Post.first + # @tag = @post.tags.build :name => "ruby" + # @tag.save + # + # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the + # <tt>:inverse_of</tt> is set: + # + # class Taggable < ActiveRecord::Base + # belongs_to :post + # belongs_to :tag, :inverse_of => :taggings + # end + # + # === Nested Associations + # + # You can actually specify *any* association with the <tt>:through</tt> option, including an + # association which has a <tt>:through</tt> option itself. For example: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :comments, :through => :posts + # has_many :commenters, :through => :comments + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # @author = Author.first + # @author.commenters # => People who commented on posts written by the author + # + # An equivalent way of setting up this association this would be: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :commenters, :through => :posts + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # has_many :commenters, :through => :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # When using nested association, you will not be able to modify the association because there + # is not enough information to know what modification to make. For example, if you tried to + # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the + # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. + # # === Polymorphic Associations # - # Polymorphic associations on models are not restricted on what types of models they - # can be associated with. Rather, they specify an interface that a +has_many+ association + # Polymorphic associations on models are not restricted on what types of models they + # can be associated with. Rather, they specify an interface that a +has_many+ association # must adhere to. # # class Asset < ActiveRecord::Base @@ -502,15 +608,15 @@ module ActiveRecord # # @asset.attachable = @post # - # This works by using a type column in addition to a foreign key to specify the associated - # record. In the Asset example, you'd need an +attachable_id+ integer column and an + # This works by using a type column in addition to a foreign key to specify the associated + # record. In the Asset example, you'd need an +attachable_id+ integer column and an # +attachable_type+ string column. # - # Using polymorphic associations in combination with single table inheritance (STI) is - # a little tricky. In order for the associations to work as expected, ensure that you - # store the base model for the STI models in the type column of the polymorphic + # Using polymorphic associations in combination with single table inheritance (STI) is + # a little tricky. In order for the associations to work as expected, ensure that you + # store the base model for the STI models in the type column of the polymorphic # association. To continue with the asset example above, suppose there are guest posts - # and member posts that use the posts table for STI. In this case, there must be a +type+ + # and member posts that use the posts table for STI. In this case, there must be a +type+ # column in the posts table. # # class Asset < ActiveRecord::Base @@ -534,8 +640,8 @@ module ActiveRecord # # == Caching # - # All of the methods are built on a simple caching principle that will keep the result - # of the last query around unless specifically instructed not to. The cache is even + # All of the methods are built on a simple caching principle that will keep the result + # of the last query around unless specifically instructed not to. The cache is even # shared across methods to make it even cheaper to use the macro-added methods without # worrying too much about performance at the first go. # @@ -547,9 +653,9 @@ module ActiveRecord # # == Eager loading of associations # - # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 - # posts that each need to display their author triggers 101 database queries. Through the + # Eager loading is a way to find objects of a certain class and a number of named associations. + # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # posts that each need to display their author triggers 101 database queries. Through the # use of eager loading, the 101 queries can be reduced to 2. # # class Post < ActiveRecord::Base @@ -565,54 +671,54 @@ module ActiveRecord # puts "Last comment on: " + post.comments.first.created_on # end # - # To iterate over these one hundred posts, we'll generate 201 database queries. Let's + # To iterate over these one hundred posts, we'll generate 201 database queries. Let's # first just optimize it for retrieving the author: # # for post in Post.find(:all, :include => :author) # - # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> - # symbol. After loading the posts, find will collect the +author_id+ from each one and load - # all the referenced authors with one query. Doing so will cut down the number of queries + # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> + # symbol. After loading the posts, find will collect the +author_id+ from each one and load + # all the referenced authors with one query. Doing so will cut down the number of queries # from 201 to 102. # # We can improve upon the situation further by referencing both associations in the finder with: # # for post in Post.find(:all, :include => [ :author, :comments ]) # - # This will load all comments with a single query. This reduces the total number of queries - # to 3. More generally the number of queries will be 1 plus the number of associations + # This will load all comments with a single query. This reduces the total number of queries + # to 3. More generally the number of queries will be 1 plus the number of associations # named (except if some of the associations are polymorphic +belongs_to+ - see below). # # To include a deep hierarchy of associations, use a hash: # # for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ]) # - # That'll grab not only all the comments but all their authors and gravatar pictures. - # You can mix and match symbols, arrays and hashes in any combination to describe the + # That'll grab not only all the comments but all their authors and gravatar pictures. + # You can mix and match symbols, arrays and hashes in any combination to describe the # associations you want to load. # - # All of this power shouldn't fool you into thinking that you can pull out huge amounts - # of data with no performance penalty just because you've reduced the number of queries. - # The database still needs to send all the data to Active Record and it still needs to - # be processed. So it's no catch-all for performance problems, but it's a great way to + # All of this power shouldn't fool you into thinking that you can pull out huge amounts + # of data with no performance penalty just because you've reduced the number of queries. + # The database still needs to send all the data to Active Record and it still needs to + # be processed. So it's no catch-all for performance problems, but it's a great way to # cut down on the number of queries in a situation as the one described above. # - # Since only one table is loaded at a time, conditions or orders cannot reference tables - # other than the main one. If this is the case Active Record falls back to the previously + # Since only one table is loaded at a time, conditions or orders cannot reference tables + # other than the main one. If this is the case Active Record falls back to the previously # used LEFT OUTER JOIN based strategy. For example # - # Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true]) + # Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all # - # This will result in a single SQL query with joins along the lines of: + # This will result in a single SQL query with joins along the lines of: # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and - # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions + # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions # like this can have unintended consequences. - # In the above example posts with no approved comments are not returned at all, because - # the conditions apply to the SQL statement as a whole and not just to the association. + # In the above example posts with no approved comments are not returned at all, because + # the conditions apply to the SQL statement as a whole and not just to the association. # You must disambiguate column references for this fallback to happen, for example # <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not. # - # If you do want eager load only some members of an association it is usually more natural + # If you do want eager load only some members of an association it is usually more natural # to <tt>:include</tt> an association which has conditions defined on it: # # class Post < ActiveRecord::Base @@ -621,10 +727,10 @@ module ActiveRecord # # Post.find(:all, :include => :approved_comments) # - # This will load posts and eager load the +approved_comments+ association, which contains + # This will load posts and eager load the +approved_comments+ association, which contains # only those comments that have been approved. # - # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, + # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, # returning all the associated objects: # # class Picture < ActiveRecord::Base @@ -633,7 +739,7 @@ module ActiveRecord # # Picture.find(:first, :include => :most_recent_comments).most_recent_comments # => returns all associated comments. # - # When eager loaded, conditions are interpolated in the context of the model class, not + # When eager loaded, conditions are interpolated in the context of the model class, not # the model instance. Conditions are lazily interpolated before the actual model exists. # # Eager loading is supported with polymorphic associations. @@ -646,20 +752,20 @@ module ActiveRecord # # Address.find(:all, :include => :addressable) # - # This will execute one query to load the addresses and load the addressables with one + # This will execute one query to load the addresses and load the addressables with one # query per addressable type. - # For example if all the addressables are either of class Person or Company then a total - # of 3 queries will be executed. The list of addressable types to load is determined on + # For example if all the addressables are either of class Person or Company then a total + # of 3 queries will be executed. The list of addressable types to load is determined on # the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. - # The reason is that the parent model's type is a column value so its corresponding table + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. + # The reason is that the parent model's type is a column value so its corresponding table # name cannot be put in the +FROM+/+JOIN+ clauses of that query. # # == Table Aliasing # - # Active Record uses table aliasing in the case that a table is referenced multiple times - # in a join. If a table is referenced only once, the standard table name is used. The - # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. + # Active Record uses table aliasing in the case that a table is referenced multiple times + # in a join. If a table is referenced only once, the standard table name is used. The + # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. # Indexes are appended for any more successive uses of the table name. # # Post.find :all, :joins => :comments @@ -693,7 +799,7 @@ module ActiveRecord # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2 # - # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table + # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table # names will take precedence over the eager associations: # # Post.find :all, :joins => :comments, :joins => "inner join comments ..." @@ -703,7 +809,7 @@ module ActiveRecord # INNER JOIN comments special_comments_posts ... # INNER JOIN comments ... # - # Table aliases are automatically truncated according to the maximum length of table identifiers + # Table aliases are automatically truncated according to the maximum length of table identifiers # according to the specific database. # # == Modules @@ -720,9 +826,9 @@ module ActiveRecord # end # end # - # When <tt>Firm#clients</tt> is called, it will in turn call + # When <tt>Firm#clients</tt> is called, it will in turn call # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. - # If you want to associate with a class in another module scope, this can be done by + # If you want to associate with a class in another module scope, this can be done by # specifying the complete class name. # # module MyApplication @@ -739,7 +845,7 @@ module ActiveRecord # # == Bi-directional associations # - # When you specify an association there is usually an association on the associated model + # When you specify an association there is usually an association on the associated model # that specifies the same relationship in reverse. For example, with the following models: # # class Dungeon < ActiveRecord::Base @@ -755,10 +861,10 @@ module ActiveRecord # belongs_to :dungeon # end # - # The +traps+ association on +Dungeon+ and the the +dungeon+ association on +Trap+ are - # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ + # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are + # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record doesn't know anything about these inverse relationships and so no object + # Active Record doesn't know anything about these inverse relationships and so no object # loading optimisation is possible. For example: # # d = Dungeon.first @@ -767,10 +873,10 @@ module ActiveRecord # d.level = 10 # d.level == t.dungeon.level # => false # - # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to - # the same object data from the database, but are actually different in-memory copies + # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to + # the same object data from the database, but are actually different in-memory copies # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell - # Active Record about inverse relationships and it will optimise object loading. For + # Active Record about inverse relationships and it will optimise object loading. For # example, if we changed our model definitions to: # # class Dungeon < ActiveRecord::Base @@ -786,7 +892,7 @@ module ActiveRecord # belongs_to :dungeon, :inverse_of => :evil_wizard # end # - # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same + # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+. # # There are limitations to <tt>:inverse_of</tt> support: @@ -795,14 +901,81 @@ module ActiveRecord # * does not work with <tt>:polymorphic</tt> associations. # * for +belongs_to+ associations +has_many+ inverse associations are ignored. # + # == Deleting from associations + # + # === Dependent associations + # + # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option. + # This allows you to specify that associated records should be deleted when the owner is + # deleted. + # + # For example: + # + # class Author + # has_many :posts, :dependent => :destroy + # end + # Author.find(1).destroy # => Will destroy all of the author's posts, too + # + # The <tt>:dependent</tt> option can have different values which specify how the deletion + # is done. For more information, see the documentation for this option on the different + # specific association types. + # + # === Delete or destroy? + # + # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, + # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. + # + # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they + # cause the records in the join table to be removed. + # + # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the + # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either + # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or + # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. + # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for + # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete + # the join records, without running their callbacks). + # + # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that + # it returns the association rather than the records which have been deleted. + # + # === What gets deleted? + # + # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt> + # associations have records in join tables, as well as the associated records. So when we + # call one of these deletion methods, what exactly should be deleted? + # + # The answer is that it is assumed that deletion on an association is about removing the + # <i>link</i> between the owner and the associated object(s), rather than necessarily the + # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ + # <tt>:through</tt>, the join records will be deleted, but the associated records won't. + # + # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt> + # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself + # to be removed from the database. + # + # However, there are examples where this strategy doesn't make sense. For example, suppose + # a person has many projects, and each project has many tasks. If we deleted one of a person's + # tasks, we would probably not want the project to be deleted. In this scenario, the delete method + # won't actually work: it can only be used if the association on the join model is a + # +belongs_to+. In other situations you are expected to perform operations directly on + # either the associated records or the <tt>:through</tt> association. + # + # With a regular +has_many+ there is no distinction between the "associated records" + # and the "link", so there is only one choice for what gets deleted. + # + # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the + # associated records themselves, you can always do something along the lines of + # <tt>person.tasks.each(&:destroy)</tt>. + # # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt> # - # If you attempt to assign an object to an association that doesn't match the inferred + # If you attempt to assign an object to an association that doesn't match the inferred # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>. # # == Options # - # All of the association macros can be specialized through options. This makes cases + # All of the association macros can be specialized through options. This makes cases # more complex than the simple and guessable ones possible. module ClassMethods # Specifies a one-to-many association. The following methods for retrieval and query of @@ -813,12 +986,16 @@ module ActiveRecord # An empty array is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. - # Note that this operation instantly fires update sql without waiting for the save or update call on the + # Note that this operation instantly fires update sql without waiting for the save or update call on the # parent object. # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>, # and deleted if they're associated with <tt>:dependent => :delete_all</tt>. + # + # If the <tt>:through</tt> option is used, then the join records are deleted (rather than + # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or + # <tt>:dependent => :nullify</tt> to override this. # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is @@ -874,24 +1051,24 @@ module ActiveRecord # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # - # === Supported options + # === Options # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_many :products</tt> will by default be linked - # to the Product class, but if the real class name is SpecialProduct, you'll have to + # from the association name. So <tt>has_many :products</tt> will by default be linked + # to the Product class, but if the real class name is SpecialProduct, you'll have to # specify it with this option. # [:conditions] # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from - # the association are scoped if a hash is used. - # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published + # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from + # the association are scoped if a hash is used. + # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published # posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>. # [:order] # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt>. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ + # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ # association will use "person_id" as the default <tt>:foreign_key</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. @@ -902,15 +1079,17 @@ module ActiveRecord # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to # <tt>:restrict</tt> this object cannot be deleted if it has any associated object. # - # *Warning:* This option is ignored when used with <tt>:through</tt> option. + # If using with the <tt>:through</tt> option, the association on the join model must be + # a +belongs_to+, and the records which get deleted are the join records, rather than + # the associated records. # # [:finder_sql] # Specify a complete SQL statement to fetch the association. This is a good way to go for complex - # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ + # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ # is _not_ added. # [:counter_sql] # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is - # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. # [:extend] # Specify a named module for extending the proxy. See "Association extensions". @@ -919,30 +1098,38 @@ module ActiveRecord # [:group] # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> + # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> # returns. Uses the <tt>HAVING</tt> SQL-clause. # [:limit] # An integer determining the limit on the number of rows that should be returned. # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, + # An integer determining the offset from where the rows should be fetched. So at 5, # it would skip the first 4 rows. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if - # you, for example, want to do a join but not include the joined columns. Do not forget + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you, for example, want to do a join but not include the joined columns. Do not forget # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] - # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt> - # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You - # can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, <tt>has_one</tt> - # or <tt>has_many</tt> association on the join model. The collection of join models - # can be managed via the collection API. For example, new join models are created for - # newly associated objects, and if some are gone their rows are deleted (directly, - # no destroy callbacks are triggered). + # Specifies an association through which to perform the query. This can be any other type + # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. + # + # If the association on the join model is a +belongs_to+, the collection can be modified + # and the records on the <tt>:through</tt> model will be automatically created and removed + # as appropriate. Otherwise, the collection is read-only, so you should manipulate the + # <tt>:through</tt> association directly. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option on the source association on the + # join model. This allows associated records to be built which will automatically create + # the appropriate join model records when they are saved. (See the 'Association Join Models' + # section above.) # [:source] - # Specifies the source association name used by <tt>has_many :through</tt> queries. - # Only use it if the name cannot be inferred from the association. + # Specifies the source association name used by <tt>has_many :through</tt> queries. + # Only use it if the name cannot be inferred from the association. # <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. # [:source_type] @@ -953,15 +1140,14 @@ module ActiveRecord # [:readonly] # 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. + # If +false+, don't validate the associated objects when saving the parent object. true by default. # [:autosave] - # If true, always save the associated objects or destroy them if marked for destruction, - # when saving the parent object. - # If false, never save or destroy the associated objects. + # If true, always save the associated objects or destroy them if marked for destruction, + # when saving the parent object. If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_many</tt> association. Does not work in combination + # Specifies the name of the <tt>belongs_to</tt> association on the associated object + # that is the inverse of this <tt>has_many</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # @@ -979,16 +1165,8 @@ module ActiveRecord # 'FROM people p, post_subscriptions ps ' + # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + # 'ORDER BY p.first_name' - def has_many(association_id, options = {}, &extension) - reflection = create_has_many_reflection(association_id, options, &extension) - configure_dependency_for_has_many(reflection) - add_association_callbacks(reflection.name, reflection.options) - - if options[:through] - collection_accessor_methods(reflection, HasManyThroughAssociation) - else - collection_accessor_methods(reflection, HasManyAssociation) - end + def has_many(name, options = {}, &extension) + Builder::HasMany.build(self, name, options, &extension) end # Specifies a one-to-one association with another class. This method should only be used @@ -1006,12 +1184,14 @@ module ActiveRecord # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not - # yet been saved. <b>Note:</b> This ONLY works if an association already exists. - # It will NOT work if the association is +nil+. + # yet been saved. # [create_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.) @@ -1023,6 +1203,7 @@ module ActiveRecord # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) + # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) # # === Options # @@ -1036,19 +1217,19 @@ module ActiveRecord # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash - # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create + # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create # an enabled account with <tt>@company.create_account</tt> or <tt>@company.build_account</tt>. # [:order] # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt>. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to - # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. - # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+. + # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. + # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+. # Also, association is assigned. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association + # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association # will use "person_id" as the default <tt>:foreign_key</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. @@ -1057,17 +1238,17 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, - # you want to do a join but not include the joined columns. Do not forget to include the + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, + # you want to do a join but not include the joined columns. Do not forget to include the # primary and foreign keys, otherwise it will raise an error. # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> - # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You - # can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt> - # association on the join model. + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> + # or <tt>belongs_to</tt> association on the join model. # [:source] - # Specifies the source association name used by <tt>has_one :through</tt> queries. - # Only use it if the name cannot be inferred from the association. + # Specifies the source association name used by <tt>has_one :through</tt> queries. + # Only use it if the name cannot be inferred from the association. # <tt>has_one :favorite, :through => :favorites</tt> will look for a # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. # [:source_type] @@ -1076,20 +1257,20 @@ module ActiveRecord # [:readonly] # 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. + # If +false+, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, + # If true, always save the associated object or destroy it if marked for destruction, # when saving the parent object. If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_one</tt> association. Does not work in combination + # Specifies the name of the <tt>belongs_to</tt> association on the associated object + # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card - # has_one :credit_card, :dependent => :nullify # updates the associated records foreign + # has_one :credit_card, :dependent => :nullify # updates the associated records foreign # # key value to NULL rather than destroying it # has_one :last_comment, :class_name => "Comment", :order => "posted_on" # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" @@ -1097,17 +1278,8 @@ module ActiveRecord # has_one :boss, :readonly => :true # has_one :club, :through => :membership # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable - def has_one(association_id, options = {}) - if options[:through] - reflection = create_has_one_through_reflection(association_id, options) - association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) - else - reflection = create_has_one_reflection(association_id, options) - association_accessor_methods(reflection, HasOneAssociation) - association_constructor_method(:build, reflection, HasOneAssociation) - association_constructor_method(:create, reflection, HasOneAssociation) - configure_dependency_for_has_one(reflection) - end + def has_one(name, options = {}) + Builder::HasOne.build(self, name, options) end # Specifies a one-to-one association with another class. This method should only be used @@ -1129,6 +1301,9 @@ module ActiveRecord # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.) @@ -1140,6 +1315,7 @@ module ActiveRecord # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>) # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) + # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options @@ -1152,33 +1328,38 @@ module ActiveRecord # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>authorized = 1</tt>. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed - # if, for example, you want to do a join but not include the joined columns. Do not + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed + # if, for example, you want to do a join but not include the joined columns. Do not # forget to include the primary and foreign keys, otherwise it will raise an error. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> - # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, - # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key + # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> + # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, + # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key # of "favorite_person_id". + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the association with a "_type" + # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt> + # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] - # Specify the method that returns the primary key of associated object used for the association. + # Specify the method that returns the primary key of associated object used for the association. # By default this is id. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to - # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. - # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with + # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. + # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with # a <tt>has_many</tt> relationship on another class because of the potential to leave # orphaned records behind. # [:counter_cache] # Caches the number of belonging objects on the associate class through the use of +increment_counter+ - # and +decrement_counter+. The counter cache is incremented when an object of this - # class is created and decremented when it's destroyed. This requires that a column + # and +decrement_counter+. The counter cache is incremented when an object of this + # class is created and decremented when it's destroyed. This requires that a column # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) - # is used on the associate class (such as a Post class). You can also specify a custom counter - # cache column by providing a column name instead of a +true+/+false+ value to this + # is used on the associate class (such as a Post class). You can also specify a custom counter + # cache column by providing a column name instead of a +true+/+false+ value to this # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) - # Note: Specifying a counter cache will add it to that model's list of readonly attributes + # Note: Specifying a counter cache will add it to that model's list of readonly attributes # using +attr_readonly+. # [:include] # Specify second-order associations that should be eager loaded when this object is loaded. @@ -1189,19 +1370,19 @@ module ActiveRecord # [:readonly] # 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. + # If +false+, don't validate the associated objects when saving the parent object. +false+ by default. # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, when + # If true, always save the associated object or destroy it if marked for destruction, when # saving the parent object. # If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) - # when this record is either saved or destroyed. If you specify a symbol, that attribute + # If true, the associated object will be touched (the updated_at/on attributes set to now) + # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time instead of the updated_at/on attribute. # [:inverse_of] - # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated - # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in + # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated + # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # @@ -1216,29 +1397,16 @@ module ActiveRecord # belongs_to :post, :counter_cache => true # belongs_to :company, :touch => true # belongs_to :company, :touch => :employees_last_updated_at - def belongs_to(association_id, options = {}) - reflection = create_belongs_to_reflection(association_id, options) - - if reflection.options[:polymorphic] - association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - else - association_accessor_methods(reflection, BelongsToAssociation) - association_constructor_method(:build, reflection, BelongsToAssociation) - association_constructor_method(:create, reflection, BelongsToAssociation) - end - - add_counter_cache_callbacks(reflection) if options[:counter_cache] - add_touch_callbacks(reflection, options[:touch]) if options[:touch] - - configure_dependency_for_belongs_to(reflection) + def belongs_to(name, options = {}) + Builder::BelongsTo.build(self, name, options) end # Specifies a many-to-many relationship with another class. This associates two classes via an # intermediate join table. Unless the join table is explicitly specified as an option, it is # guessed using the lexical order of the class names. So a join between Developer and Project - # will give the default join table name of "developers_projects" because "D" outranks "P". - # Note that this precedence is calculated using the <tt><</tt> operator for String. This - # means that if the strings are of different lengths, and the strings are equal when compared + # will give the default join table name of "developers_projects" because "D" outranks "P". + # Note that this precedence is calculated using the <tt><</tt> operator for String. This + # means that if the strings are of different lengths, and the strings are equal when compared # up to the shortest length, then the longer string is considered of higher # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", @@ -1261,12 +1429,6 @@ module ActiveRecord # end # end # - # Deprecated: Any additional fields added to the join table will be placed as attributes when - # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join - # tables with additional attributes will be marked as readonly (because we can't save changes - # to the additional attributes). It's strongly recommended that you upgrade any - # associations with attributes to a real join model (see introduction). - # # Adds the following methods for retrieval and query: # # [collection(force_reload = false)] @@ -1275,7 +1437,7 @@ module ActiveRecord # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). - # Note that this operation instantly fires update sql without waiting for the save or update call on the + # Note that this operation instantly fires update sql without waiting for the save or update call on the # parent object. # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. @@ -1304,7 +1466,7 @@ module ActiveRecord # with +attributes+ and linked to this object through the join table, but has not yet been saved. # [collection.create(attributes = {})] # Returns a new object of the collection type that has been instantiated - # with +attributes+, linked to this object through the join table, and that has already been + # with +attributes+, linked to this object through the join table, and that has already been # saved (if it passed the validation). # # (+collection+ is replaced with the symbol passed as the first argument, so @@ -1340,8 +1502,8 @@ module ActiveRecord # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes - # a +has_and_belongs_to_many+ association to Project will use "person_id" as the + # of this class in lower-case and "_id" suffixed. So a Person class that makes + # a +has_and_belongs_to_many+ association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. @@ -1350,7 +1512,7 @@ module ActiveRecord # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are + # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are # scoped if a hash is used. # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> # or <tt>@blog.posts.build</tt>. @@ -1363,7 +1525,7 @@ module ActiveRecord # Overwrite the default generated SQL statement used to fetch the association with a manual statement # [:counter_sql] # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is - # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. # [:delete_sql] # Overwrite the default generated SQL statement used to remove links between the associated @@ -1378,23 +1540,23 @@ module ActiveRecord # [:group] # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. + # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. # Uses the <tt>HAVING</tt> SQL-clause. # [:limit] # An integer determining the limit on the number of rows that should be returned. # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, + # An integer determining the offset from where the rows should be fetched. So at 5, # it would skip the first 4 rows. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, - # you want to do a join but not include the joined columns. Do not forget to include the primary + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, + # you want to do a join but not include the joined columns. Do not forget to include the primary # and foreign keys, otherwise it will raise an error. # [:readonly] # 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. + # If +false+, don't validate the associated objects when saving the parent object. +true+ by default. # [:autosave] - # If true, always save the associated objects or destroy them if marked for destruction, when + # If true, always save the associated objects or destroy them if marked for destruction, when # saving the parent object. # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. @@ -1407,845 +1569,9 @@ module ActiveRecord # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' - def has_and_belongs_to_many(association_id, options = {}, &extension) - reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) - - # Don't use a before_destroy callback since users' before_destroy - # callbacks will be executed after the association is wiped out. - include Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy # def destroy - super # super - #{reflection.name}.clear # posts.clear - end # end - RUBY - } - - add_association_callbacks(reflection.name, options) + def has_and_belongs_to_many(name, options = {}, &extension) + Builder::HasAndBelongsToMany.build(self, name, options, &extension) end - - private - # Generates a join table name from two provided table names. - # The names in the join table names end up in lexicographic order. - # - # join_table_name("members", "clubs") # => "clubs_members" - # join_table_name("members", "special_clubs") # => "members_special_clubs" - def join_table_name(first_table_name, second_table_name) - if first_table_name < second_table_name - join_table = "#{first_table_name}_#{second_table_name}" - else - join_table = "#{second_table_name}_#{first_table_name}" - end - - table_name_prefix + join_table + table_name_suffix - end - - def association_accessor_methods(reflection, association_proxy_class) - redefine_method(reflection.name) do |*params| - force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - if association.nil? || force_reload - association = association_proxy_class.new(self, reflection) - retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload - if retval.nil? and association_proxy_class == BelongsToAssociation - association_instance_set(reflection.name, nil) - return nil - end - association_instance_set(reflection.name, association) - end - - association.target.nil? ? nil : association - end - - redefine_method("loaded_#{reflection.name}?") do - association = association_instance_get(reflection.name) - association && association.loaded? - end - - redefine_method("#{reflection.name}=") do |new_value| - association = association_instance_get(reflection.name) - - if association.nil? || association.target != new_value - association = association_proxy_class.new(self, reflection) - end - - association.replace(new_value) - association_instance_set(reflection.name, new_value.nil? ? nil : association) - end - - redefine_method("set_#{reflection.name}_target") do |target| - return if target.nil? and association_proxy_class == BelongsToAssociation - association = association_proxy_class.new(self, reflection) - association.target = target - association_instance_set(reflection.name, association) - end - end - - def collection_reader_method(reflection, association_proxy_class) - redefine_method(reflection.name) do |*params| - force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end - - reflection.klass.uncached { association.reload } if force_reload - - association - end - - redefine_method("#{reflection.name.to_s.singularize}_ids") do - if send(reflection.name).loaded? || reflection.options[:finder_sql] - send(reflection.name).map(&:id) - else - if reflection.through_reflection && reflection.source_reflection.belongs_to? - through = reflection.through_reflection - primary_key = reflection.source_reflection.primary_key_name - send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}") - else - send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map!(&:id) - end - end - end - - end - - def collection_accessor_methods(reflection, association_proxy_class, writer = true) - collection_reader_method(reflection, association_proxy_class) - - if writer - redefine_method("#{reflection.name}=") do |new_value| - # Loads proxy class instance (defined in collection_reader_method) if not already loaded - association = send(reflection.name) - association.replace(new_value) - association - end - - redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| - pk_column = reflection.primary_key_column - ids = (new_value || []).reject { |nid| nid.blank? } - ids.map!{ |i| pk_column.type_cast(i) } - send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids)) - end - end - end - - def association_constructor_method(constructor, reflection, association_proxy_class) - redefine_method("#{constructor}_#{reflection.name}") do |*params| - attributees = params.first unless params.empty? - replace_existing = params[1].nil? ? true : params[1] - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end - - if association_proxy_class == HasOneAssociation - association.send(constructor, attributees, replace_existing) - else - association.send(constructor, attributees) - end - end - end - - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - - method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.increment_counter(cache_column, association.id) unless association.nil? - end - after_create(method_name) - - method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.decrement_counter(cache_column, association.id) unless association.nil? - end - before_destroy(method_name) - - module_eval( - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ - ) - end - - def add_touch_callbacks(reflection, touch_attribute) - method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}" - redefine_method(method_name) do - association = send(reflection.name) - - if touch_attribute == true - association.touch unless association.nil? - else - association.touch(touch_attribute) unless association.nil? - end - end - after_save(method_name) - after_touch(method_name) - after_destroy(method_name) - end - - # Creates before_destroy callback methods that nullify, delete or destroy - # has_many associated objects, according to the defined :dependent rule. - # - # See HasManyAssociation#delete_records for more information. In general - # - delete children if the option is set to :destroy or :delete_all - # - set the foreign key to NULL if the option is set to :nullify - # - do not delete the parent record if there is any child record if the - # option is set to :restrict - # - # The +extra_conditions+ parameter, which is not used within the main - # Active Record codebase, is meant to allow plugins to define extra - # finder conditions. - def configure_dependency_for_has_many(reflection, extra_conditions = nil) - if reflection.options.include?(:dependent) - case reflection.options[:dependent] - when :destroy - method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do - send(reflection.name).each do |o| - # No point in executing the counter update since we're going to destroy the parent anyway - counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if(o.respond_to? counter_method) then - class << o - self - end.send(:define_method, counter_method, Proc.new {}) - end - o.destroy - end - end - before_destroy method_name - when :delete_all - before_destroy do |record| - self.class.send(:delete_all_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :nullify - before_destroy do |record| - self.class.send(:nullify_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.primary_key_name, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :restrict - method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).empty? - raise DeleteRestrictionError.new(reflection) - end - end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" - end - end - end - - # Creates before_destroy callback methods that nullify, delete or destroy - # has_one associated objects, according to the defined :dependent rule. - # If the association is marked as :dependent => :restrict, create a callback - # that prevents deleting entirely. - def configure_dependency_for_has_one(reflection) - if reflection.options.include?(:dependent) - name = reflection.options[:dependent] - method_name = :"has_one_dependent_#{name}_for_#{reflection.name}" - - case name - when :destroy, :delete - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.#{name} if association - end - eoruby - when :nullify - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association - end - eoruby - when :restrict - method_name = "has_one_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).nil? - raise DeleteRestrictionError.new(reflection) - end - end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete, :nullify or :restrict (#{reflection.options[:dependent].inspect})" - end - - before_destroy method_name - end - end - - def configure_dependency_for_belongs_to(reflection) - if reflection.options.include?(:dependent) - name = reflection.options[:dependent] - - unless [:destroy, :delete].include?(name) - raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})" - end - - method_name = :"belongs_to_dependent_#{name}_for_#{reflection.name}" - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.#{name} if association - end - eoruby - after_destroy method_name - end - end - - def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions) - association_class.delete_all(dependent_conditions) - end - - def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions) - association_class.update_all("#{primary_key_name} = NULL", dependent_conditions) - end - - 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, :having, :limit, :offset, - :as, :through, :source, :source_type, - :uniq, - :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate, :inverse_of - ] - - 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 - - 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, :inverse_of - ] - - 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 - - def create_has_one_through_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate - ) - create_reflection(:has_one, association_id, options, self) - end - - mattr_accessor :valid_keys_for_belongs_to_association - @@valid_keys_for_belongs_to_association = [ - :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions, - :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, - :validate, :touch, :inverse_of - ] - - 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] - reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type" - end - - reflection - end - - mattr_accessor :valid_keys_for_has_and_belongs_to_many_association - @@valid_keys_for_has_and_belongs_to_many_association = [ - :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key, - :select, :conditions, :include, :order, :group, :having, :limit, :offset, - :uniq, - :finder_sql, :counter_sql, :delete_sql, :insert_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate - ] - - def create_has_and_belongs_to_many_reflection(association_id, options, &extension) - options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association) - options[:extend] = create_extension_modules(association_id, extension, options[:extend]) - - reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - - if reflection.association_foreign_key == reflection.primary_key_name - raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) - end - - reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name)) - if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false) - raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection) - end - - reflection - end - - def add_association_callbacks(association_name, options) - callbacks = %w(before_add after_add before_remove after_remove) - callbacks.each do |callback_name| - full_callback_name = "#{callback_name}_for_#{association_name}" - defined_callbacks = options[callback_name.to_sym] - if options.has_key?(callback_name.to_sym) - class_inheritable_reader full_callback_name.to_sym - write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten) - else - write_inheritable_attribute(full_callback_name.to_sym, []) - end - end - end - - def create_extension_modules(association_id, block_extension, extensions) - if block_extension - extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension" - - silence_warnings do - self.parent.const_set(extension_module_name, Module.new(&block_extension)) - end - Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize) - else - Array.wrap(extensions) - end - end - - class JoinDependency # :nodoc: - attr_reader :joins, :reflections, :table_aliases - - def initialize(base, associations, joins) - @joins = [JoinBase.new(base, joins)] - @associations = associations - @reflections = [] - @base_records_hash = {} - @base_records_in_order = [] - @table_aliases = Hash.new { |aliases, table| aliases[table] = 0 } - @table_aliases[base.table_name] = 1 - build(associations) - end - - def graft(*associations) - associations.each do |association| - join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class) - end - self - end - - def join_associations - @joins[1..-1].to_a - end - - def join_base - @joins[0] - end - - def count_aliases_from_table_joins(name) - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase - join_sql = join_base.table_joins.to_s.downcase - join_sql.blank? ? 0 : - # Table names - join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + - # Table aliases - join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size - end - - def instantiate(rows) - rows.each_with_index do |row, i| - primary_id = join_base.record_id(row) - unless @base_records_hash[primary_id] - @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) - end - construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) - end - remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) - return @base_records_in_order - end - - def remove_duplicate_results!(base, records, associations) - case associations - when Symbol, String - reflection = base.reflections[associations] - remove_uniq_by_reflection(reflection, records) - when Array - associations.each do |association| - remove_duplicate_results!(base, records, association) - end - when Hash - associations.keys.each do |name| - reflection = base.reflections[name] - remove_uniq_by_reflection(reflection, records) - - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end - end - end - - remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? - end - end - end - - protected - - def build(associations, parent = nil, join_class = Arel::InnerJoin) - parent ||= @joins.last - case associations - when Symbol, String - reflection = parent.reflections[associations.to_s.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" - @reflections << reflection - @joins << build_join_association(reflection, parent).with_join_class(join_class) - when Array - associations.each do |association| - build(association, parent, join_class) - end - when Hash - associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent, join_class) - build(associations[name], nil, join_class) - end - else - raise ConfigurationError, associations.inspect - end - end - - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end - end - - def build_join_association(reflection, parent) - JoinAssociation.new(reflection, self, parent) - end - - def construct(parent, associations, joins, row) - case associations - when Symbol, String - join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? - - joins.delete(join) - construct_association(parent, join, row) - when Array - associations.each do |association| - construct(parent, association, joins, row) - end - when Hash - associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join.nil? - - association = construct_association(parent, join, row) - joins.delete(join) - construct(association, associations[name], joins, row) if association - end - else - raise ConfigurationError, associations.inspect - end - end - - def construct_association(record, join, row) - case join.reflection.macro - when :has_many, :has_and_belongs_to_many - collection = record.send(join.reflection.name) - collection.loaded - - return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil? - association = join.instantiate(row) - collection.target.push(association) - collection.__send__(:set_inverse_instance, association, record) - when :has_one - return if record.id.to_s != join.parent.record_id(row).to_s - return if record.instance_variable_defined?("@#{join.reflection.name}") - association = join.instantiate(row) unless row[join.aliased_primary_key].nil? - set_target_and_inverse(join, association, record) - when :belongs_to - return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil? - association = join.instantiate(row) - set_target_and_inverse(join, association, record) - else - raise ConfigurationError, "unknown macro: #{join.reflection.macro}" - end - return association - end - - def set_target_and_inverse(join, association, record) - association_proxy = record.send("set_#{join.reflection.name}_target", association) - association_proxy.__send__(:set_inverse_instance, association, record) - end - - class JoinBase # :nodoc: - attr_reader :active_record, :table_joins - delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record - - def initialize(active_record, joins = nil) - @active_record = active_record - @cached_record = {} - @table_joins = joins - end - - def ==(other) - other.class == self.class && - other.active_record == active_record && - other.table_joins == table_joins - end - - def aliased_prefix - "t0" - end - - def aliased_primary_key - "#{aliased_prefix}_r0" - end - - def aliased_table_name - active_record.table_name - end - - def column_names_with_alias - unless defined?(@column_names_with_alias) - @column_names_with_alias = [] - - ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] - end - end - - @column_names_with_alias - end - - def extract_record(row) - column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record} - end - - def record_id(row) - row[aliased_primary_key] - end - - def instantiate(row) - @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) - end - end - - class JoinAssociation < JoinBase # :nodoc: - attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class - delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection - - def initialize(reflection, join_dependency, parent = nil) - reflection.check_validity! - if reflection.options[:polymorphic] - raise EagerLoadPolymorphicError.new(reflection) - end - - super(reflection.klass) - @join_dependency = join_dependency - @parent = parent - @reflection = reflection - @aliased_prefix = "t#{ join_dependency.joins.size }" - @parent_table_name = parent.active_record.table_name - @aliased_table_name = aliased_table_name_for(table_name) - @join = nil - @join_class = Arel::InnerJoin - - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - end - - if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] - @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") - end - end - - def ==(other) - other.class == self.class && - other.reflection == reflection && - other.parent == parent - end - - def find_parent_in(other_join_dependency) - other_join_dependency.joins.detect do |join| - self.parent == join - end - end - - def with_join_class(join_class) - @join_class = join_class - self - end - - def association_join - return @join if @join - - aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine) - parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => arel_engine) - - @join = case reflection.macro - when :has_and_belongs_to_many - join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine) - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key - - [ - join_table[fk].eq(parent_table[reflection.active_record.primary_key]), - aliased_table[klass.primary_key].eq(join_table[klass_fk]) - ] - when :has_many, :has_one - if reflection.options[:through] - join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine) - jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil - first_key = second_key = as_extra = nil - - if through_reflection.options[:as] # has_many :through against a polymorphic join - jt_foreign_key = through_reflection.options[:as].to_s + '_id' - jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].eq(parent.active_record.base_class.name) - else - jt_foreign_key = through_reflection.primary_key_name - end - - case source_reflection.macro - when :has_many - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - second_key = options[:foreign_key] || primary_key - as_extra = aliased_table["#{source_reflection.options[:as]}_type"].eq(source_reflection.active_record.base_class.name) - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - second_key = options[:foreign_key] || primary_key - end - - unless through_reflection.klass.descends_from_active_record? - jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type]) - else - second_key = source_reflection.primary_key_name - end - end - - [ - [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].reject{|x| x.blank? }, - aliased_table[first_key].eq(join_table[second_key]) - ] - elsif reflection.options[:as] - id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key]) - type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name) - [id_rel, type_rel] - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])] - end - when :belongs_to - [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])] - end - - unless klass.descends_from_active_record? - sti_column = aliased_table[klass.inheritance_column] - sti_condition = sti_column.eq(klass.sti_name) - klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } - - @join << sti_condition - end - - [through_reflection, reflection].each do |ref| - if ref && ref.options[:conditions] - @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name)) - end - end - - @join - end - - def relation - aliased = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine) - - if reflection.macro == :has_and_belongs_to_many - [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased] - elsif reflection.options[:through] - [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased] - else - aliased - end - end - - def join_relation(joining_relation, join = nil) - joining_relation.joins(self.with_join_class(Arel::OuterJoin)) - end - - protected - - def aliased_table_name_for(name, suffix = nil) - if @join_dependency.table_aliases[name].zero? - @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) - end - - if !@join_dependency.table_aliases[name].zero? # We need an alias - name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - @join_dependency.table_aliases[name] += 1 - if @join_dependency.table_aliases[name] == 1 # First time we've seen this name - # Also need to count the aliases from the table_aliases to avoid incorrect count - @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) - end - table_index = @join_dependency.table_aliases[name] - name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 - else - @join_dependency.table_aliases[name] += 1 - end - - name - end - - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name - end - - def table_alias_for(table_name, table_alias) - "#{table_name} #{table_alias if table_name != table_alias}".strip - end - - def table_name_and_alias - table_alias_for table_name, @aliased_table_name - end - - def interpolate_sql(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - end - end end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb new file mode 100644 index 0000000000..634dee2289 --- /dev/null +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -0,0 +1,85 @@ +require 'active_support/core_ext/string/conversions' + +module ActiveRecord + module Associations + # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and + # ActiveRecord::Associations::ThroughAssociationScope + class AliasTracker # :nodoc: + attr_reader :aliases, :table_joins + + # table_joins is an array of arel joins which might conflict with the aliases we assign here + def initialize(table_joins = []) + @aliases = Hash.new + @table_joins = table_joins + end + + def aliased_table_for(table_name, aliased_name = nil) + table_alias = aliased_name_for(table_name, aliased_name) + + if table_alias == table_name + Arel::Table.new(table_name) + else + Arel::Table.new(table_name).alias(table_alias) + end + end + + def aliased_name_for(table_name, aliased_name = nil) + aliased_name ||= table_name + + initialize_count_for(table_name) if aliases[table_name].nil? + + if aliases[table_name].zero? + # If it's zero, we can have our table_name + aliases[table_name] = 1 + table_name + else + # Otherwise, we need to use an alias + aliased_name = connection.table_alias_for(aliased_name) + + initialize_count_for(aliased_name) if aliases[aliased_name].nil? + + # Update the count + aliases[aliased_name] += 1 + + if aliases[aliased_name] > 1 + "#{truncate(aliased_name)}_#{aliases[aliased_name]}" + else + aliased_name + end + end + end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + private + + def initialize_count_for(name) + aliases[name] = 0 + + unless Arel::Table === table_joins + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name).downcase + + aliases[name] += table_joins.map { |join| + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + }.sum + end + + aliases[name] + end + + def truncate(name) + name[0..connection.table_alias_length-3] + end + + def connection + ActiveRecord::Base.connection + end + end + end +end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb new file mode 100644 index 0000000000..27c446b12c --- /dev/null +++ b/activerecord/lib/active_record/associations/association.rb @@ -0,0 +1,230 @@ +require 'active_support/core_ext/array/wrap' + +module ActiveRecord + module Associations + # = Active Record Associations + # + # This is the root class of all associations ('+ Foo' signifies an included module Foo): + # + # Association + # SingularAssociation + # HasOneAssociation + # HasOneThroughAssociation + ThroughAssociation + # BelongsToAssociation + # BelongsToPolymorphicAssociation + # CollectionAssociation + # HasAndBelongsToManyAssociation + # HasManyAssociation + # HasManyThroughAssociation + ThroughAssociation + class Association #:nodoc: + attr_reader :owner, :target, :reflection + + delegate :options, :to => :reflection + + def initialize(owner, reflection) + reflection.check_validity! + + @target = nil + @owner, @reflection = owner, reflection + @updated = false + + reset + construct_scope + end + + # Returns the name of the table of the related class: + # + # post.comments.aliased_table_name # => "comments" + # + def aliased_table_name + reflection.klass.table_name + end + + # Resets the \loaded flag to +false+ and sets the \target to +nil+. + def reset + @loaded = false + IdentityMap.remove(target) if IdentityMap.enabled? && target + @target = nil + end + + # Reloads the \target and returns +self+ on success. + def reload + reset + construct_scope + load_target + self unless target.nil? + end + + # Has the \target been already \loaded? + def loaded? + @loaded + end + + # Asserts the \target has been loaded setting the \loaded flag to +true+. + def loaded! + @loaded = true + @stale_state = stale_state + end + + # The target is stale if the target no longer points to the record(s) that the + # relevant foreign_key(s) refers to. If stale, the association accessor method + # on the owner will reload the target. It's up to subclasses to implement the + # state_state method if relevant. + # + # Note that if the target has not been loaded, it is not considered stale. + def stale_target? + loaded? && @stale_state != stale_state + end + + # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. + def target=(target) + @target = target + loaded! + end + + def scoped + target_scope.merge(@association_scope) + end + + # Construct the scope for this association. + # + # Note that the association_scope is merged into the target_scope only when the + # scoped method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which + # actually gets built. + def construct_scope + if klass + @association_scope = AssociationScope.new(self).scope + end + end + + # Set the inverse association, if possible + def set_inverse_instance(record) + if record && invertible_for?(record) + inverse = record.association(inverse_reflection_for(record).name) + inverse.target = owner + end + end + + # This class of the target. belongs_to polymorphic overrides this to look at the + # polymorphic_type field on the owner. + def klass + reflection.klass + end + + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + klass.scoped + end + + # Loads the \target if needed and returns it. + # + # This method is abstract in the sense that it relies on +find_target+, + # which is expected to be provided by descendants. + # + # If the \target is already \loaded it is just returned. Thus, you can call + # +load_target+ unconditionally to get the \target. + # + # ActiveRecord::RecordNotFound is rescued within the method, and it is + # not reraised. The proxy is \reset and +nil+ is the return value. + def load_target + if find_target? + begin + if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) + @target = IdentityMap.get(association_class, owner[reflection.foreign_key]) + end + rescue NameError + nil + ensure + @target ||= find_target + end + end + loaded! + target + rescue ActiveRecord::RecordNotFound + reset + end + + private + + def find_target? + !loaded? && (!owner.new_record? || foreign_key_present?) && klass + end + + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + owner.send(:instance_exec, record, &sql) + else + sql + end + end + + def creation_attributes + attributes = {} + + if [:has_one, :has_many].include?(reflection.macro) && !options[:through] + attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] + + if reflection.options[:as] + attributes[reflection.type] = owner.class.base_class.name + end + end + + attributes + end + + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if owner.persisted? + creation_attributes.each { |key, value| record[key] = value } + end + end + + # Should be true if there is a foreign key present on the owner which + # references the target. This is used to determine whether we can load + # the target if the owner is currently a new record (and therefore + # without a key). + # + # Currently implemented by belongs_to (vanilla and polymorphic) and + # has_one/has_many :through associations which go through a belongs_to + def foreign_key_present? + false + end + + # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of + # the kind of the class of the associated objects. Meant to be used as + # a sanity check when you are about to assign an associated record. + def raise_on_type_mismatch(record) + unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + raise ActiveRecord::AssociationTypeMismatch, message + end + end + + # Can be redefined by subclasses, notably polymorphic belongs_to + # The record parameter is necessary to support polymorphic inverses as we must check for + # the association in the specific class of the record. + def inverse_reflection_for(record) + reflection.inverse_of + end + + # Is this association invertible? Can be redefined by subclasses. + def invertible_for?(record) + inverse_reflection_for(record) + end + + # This should be implemented to return the values of the relevant key(s) on the owner, + # so that when state_state is different from the value stored on the last find_target, + # the target is stale. + # + # This is only relevant to certain associations, which is why it returns nil by default. + def stale_state + end + + def association_class + @reflection.klass + end + end + end +end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb deleted file mode 100644 index b5159eead3..0000000000 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ /dev/null @@ -1,557 +0,0 @@ -require 'set' -require 'active_support/core_ext/array/wrap' - -module ActiveRecord - module Associations - # = Active Record Association Collection - # - # AssociationCollection is an abstract class that provides common stuff to - # ease the implementation of association proxies that represent - # collections. See the class hierarchy in AssociationProxy. - # - # You need to be careful with assumptions regarding the target: The proxy - # does not fetch records from the database until it needs them, but new - # ones created with +build+ are added to the target. So, the target may be - # non-empty and still lack children waiting to be read from the database. - # If you look directly to the database you cannot assume that's the entire - # collection because new records may have been added to the target, etc. - # - # If you need to work on all current children, new and existing records, - # +load_target+ and the +loaded+ flag are your friends. - class AssociationCollection < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped - - def select(select = nil) - if block_given? - load_target - @target.select.each { |e| yield e } - else - scoped.select(select) - end - end - - def scoped - with_scope(construct_scope) { @reflection.klass.scoped } - end - - def find(*args) - options = args.extract_options! - - # If using a custom finder_sql, scan the entire collection. - if @reflection.options[:finder_sql] - expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } - - if ids.size == 1 - id = ids.first - record = load_target.detect { |r| id == r.id } - expects_array ? [ record ] : record - else - load_target.select { |r| ids.include?(r.id) } - end - else - merge_options_from_reflection!(options) - construct_find_options!(options) - - find_scope = construct_scope[:find].slice(:conditions, :order) - - with_scope(:find => find_scope) do - relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) - - case args.first - when :first, :last - relation.send(args.first) - when :all - records = relation.all - @reflection.options[:uniq] ? uniq(records) : records - else - relation.find(*args) - end - end - end - end - - # Fetches the first one using SQL if possible. - def first(*args) - if fetch_first_or_last_using_find?(args) - find(:first, *args) - else - load_target unless loaded? - @target.first(*args) - end - end - - # Fetches the last one using SQL if possible. - def last(*args) - if fetch_first_or_last_using_find?(args) - find(:last, *args) - else - load_target unless loaded? - @target.last(*args) - end - end - - def to_ary - load_target - if @target.is_a?(Array) - @target.to_ary - else - Array.wrap(@target) - end - end - alias_method :to_a, :to_ary - - def reset - reset_target! - reset_named_scopes_cache! - @loaded = false - end - - def build(attributes = {}, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| build(attr, &block) } - else - build_record(attributes) do |record| - block.call(record) if block_given? - set_belongs_to_association_for(record) - end - end - end - - # Add +records+ to this association. Returns +self+ so method calls may be chained. - # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. - def <<(*records) - result = true - load_target if @owner.new_record? - - transaction do - flatten_deeper(records).each do |record| - raise_on_type_mismatch(record) - add_record_to_target_with_callbacks(record) do |r| - result &&= insert_record(record) unless @owner.new_record? - end - end - end - - result && self - end - - alias_method :push, :<< - alias_method :concat, :<< - - # Starts a transaction in the association class's database connection. - # - # class Author < ActiveRecord::Base - # has_many :books - # end - # - # Author.first.books.transaction do - # # same effect as calling Book.transaction - # end - def transaction(*args) - @reflection.klass.transaction(*args) do - yield - end - end - - # Remove all records from this association - # - # See delete for more info. - def delete_all - load_target - delete(@target) - reset_target! - reset_named_scopes_cache! - end - - # Calculate sum using SQL, not Enumerable - def sum(*args) - if block_given? - calculate(:sum, *args) { |*block_args| yield(*block_args) } - else - calculate(:sum, *args) - end - end - - # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will - # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the - # descendant's +construct_sql+ method will have set :counter_sql automatically. - # Otherwise, construct options and pass them with scope to the target class's +count+. - def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - - if @reflection.options[:counter_sql] && !options.blank? - raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) - else - - if @reflection.options[:uniq] - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name - options.merge!(:distinct => true) - end - - value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } - - limit = @reflection.options[:limit] - offset = @reflection.options[:offset] - - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end - end - end - - # Removes +records+ from this association calling +before_remove+ and - # +after_remove+ callbacks. - # - # This method is abstract in the sense that +delete_records+ has to be - # provided by descendants. Note this method does not imply the records - # are actually removed from the database, that depends precisely on - # +delete_records+. They are in any case removed from the collection. - def delete(*records) - remove_records(records) do |_records, old_records| - delete_records(old_records) if old_records.any? - _records.each { |record| @target.delete(record) } - end - end - - # Destroy +records+ and remove them from this association calling - # +before_remove+ and +after_remove+ callbacks. - # - # Note that this method will _always_ remove records from the database - # ignoring the +:dependent+ option. - def destroy(*records) - records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)} - remove_records(records) do |_records, old_records| - old_records.each { |record| record.destroy } - end - - load_target - end - - # Removes all records from this association. Returns +self+ so method calls may be chained. - def clear - return self if length.zero? # forces load_target if it hasn't happened already - - if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy - destroy_all - else - delete_all - end - - self - end - - # Destroy all the records from this association. - # - # See destroy for more info. - def destroy_all - load_target - destroy(@target).tap do - reset_target! - reset_named_scopes_cache! - end - end - - def create(attrs = {}) - if attrs.is_a?(Array) - attrs.collect { |attr| create(attr) } - else - create_record(attrs) do |record| - yield(record) if block_given? - record.save - end - end - end - - def create!(attrs = {}) - create_record(attrs) do |record| - yield(record) if block_given? - record.save! - end - end - - # Returns the size of the collection by executing a SELECT COUNT(*) - # query if the collection hasn't been loaded, and calling - # <tt>collection.size</tt> if it has. - # - # If the collection has been already loaded +size+ and +length+ are - # equivalent. If not and you are going to need the records anyway - # +length+ will take one less query. Otherwise +size+ is more efficient. - # - # This method is abstract in the sense that it relies on - # +count_records+, which is a method descendants have to provide. - def size - if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) - @target.size - elsif !loaded? && @reflection.options[:group] - load_target.size - elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) - unsaved_records = @target.select { |r| r.new_record? } - unsaved_records.size + count_records - else - count_records - end - end - - # Returns the size of the collection calling +size+ on the target. - # - # If the collection has been already loaded +length+ and +size+ are - # equivalent. If not and you are going to need the records anyway this - # method will take one less query. Otherwise +size+ is more efficient. - def length - load_target.size - end - - # Equivalent to <tt>collection.size.zero?</tt>. If the collection has - # not been already loaded and you are going to fetch the records anyway - # it is better to check <tt>collection.length.zero?</tt>. - def empty? - size.zero? - end - - def any? - if block_given? - method_missing(:any?) { |*block_args| yield(*block_args) } - else - !empty? - end - end - - # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. - def many? - if block_given? - method_missing(:many?) { |*block_args| yield(*block_args) } - else - size > 1 - end - end - - def uniq(collection = self) - seen = Set.new - collection.inject([]) do |kept, record| - unless seen.include?(record.id) - kept << record - seen << record.id - end - kept - end - end - - # 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.each { |val| raise_on_type_mismatch(val) } - - load_target - other = other_array.size < 100 ? other_array : other_array.to_set - current = @target.size < 100 ? @target : @target.to_set - - transaction do - delete(@target.select { |v| !other.include?(v) }) - concat(other_array.select { |v| !current.include?(v) }) - end - end - - def include?(record) - return false unless record.is_a?(@reflection.klass) - load_target if @reflection.options[:finder_sql] && !loaded? - return @target.include?(record) if loaded? - exists?(record) - end - - def proxy_respond_to?(method, include_private = false) - super || @reflection.klass.respond_to?(method, include_private) - end - - protected - def construct_find_options!(options) - end - - def construct_counter_sql - if @reflection.options[:counter_sql] - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - elsif @reflection.options[:finder_sql] - # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - else - @counter_sql = @finder_sql - end - end - - def load_target - if !@owner.new_record? || foreign_key_present - begin - if !loaded? - if @target.is_a?(Array) && @target.any? - @target = find_target.map do |f| - i = @target.index(f) - if i - @target.delete_at(i).tap do |t| - keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - t.attributes = f.attributes.except(*keys) - end - else - f - end - end + @target - else - @target = find_target - end - end - rescue ActiveRecord::RecordNotFound - reset - end - end - - loaded if target - target - end - - def method_missing(method, *args) - match = DynamicFinderMatch.match(method) - if match && match.creator? - attributes = match.attribute_names - return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) - end - - if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - if block_given? - super { |*block_args| yield(*block_args) } - else - super - end - elsif @reflection.klass.scopes[method] - @_named_scopes_cache ||= {} - @_named_scopes_cache[method] ||= {} - @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) } - else - with_scope(construct_scope) do - if block_given? - @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } - else - @reflection.klass.send(method, *args) - end - end - end - end - - # overloaded in derived Association classes to provide useful scoping depending on association type. - def construct_scope - {} - end - - def reset_target! - @target = Array.new - end - - def reset_named_scopes_cache! - @_named_scopes_cache = {} - end - - def find_target - records = - if @reflection.options[:finder_sql] - @reflection.klass.find_by_sql(@finder_sql) - else - find(:all) - end - - records = @reflection.options[:uniq] ? uniq(records) : records - records.each do |record| - set_inverse_instance(record, @owner) - end - records - end - - def add_record_to_target_with_callbacks(record) - callback(:before_add, record) - yield(record) if block_given? - @target ||= [] unless loaded? - if index = @target.index(record) - @target[index] = record - else - @target << record - end - callback(:after_add, record) - set_inverse_instance(record, @owner) - record - end - - private - 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]) do - @reflection.build_association(attrs) - end - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) - end - end - - def build_record(attrs) - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.build_association(attrs) - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) - end - end - - def remove_records(*records) - records = flatten_deeper(records) - records.each { |record| raise_on_type_mismatch(record) } - - transaction do - records.each { |record| callback(:before_remove, record) } - old_records = records.reject { |r| r.new_record? } - yield(records, old_records) - records.each { |record| callback(:after_remove, record) } - end - end - - def callback(method, record) - callbacks_for(method).each do |callback| - case callback - when Symbol - @owner.send(callback, record) - when Proc - callback.call(@owner, record) - else - callback.send(method, @owner, record) - end - end - end - - def callbacks_for(callback_name) - full_callback_name = "#{callback_name}_for_#{@reflection.name}" - @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || [] - end - - def ensure_owner_is_not_new - if @owner.new_record? - raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" - end - end - - def fetch_first_or_last_using_find?(args) - args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) - end - end - end -end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb deleted file mode 100644 index f333f4d603..0000000000 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ /dev/null @@ -1,295 +0,0 @@ -require 'active_support/core_ext/array/wrap' - -module ActiveRecord - module Associations - # = Active Record Associations - # - # This is the root class of all association proxies: - # - # AssociationProxy - # BelongsToAssociation - # HasOneAssociation - # BelongsToPolymorphicAssociation - # AssociationCollection - # HasAndBelongsToManyAssociation - # HasManyAssociation - # HasManyThroughAssociation - # HasOneThroughAssociation - # - # Association proxies in Active Record are middlemen between the object that - # holds the association, known as the <tt>@owner</tt>, and the actual associated - # object, known as the <tt>@target</tt>. The kind of association any proxy is - # about is available in <tt>@reflection</tt>. That's an instance of the class - # ActiveRecord::Reflection::AssociationReflection. - # - # For example, given - # - # class Blog < ActiveRecord::Base - # has_many :posts - # end - # - # blog = Blog.find(:first) - # - # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as - # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and - # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. - # - # This class has most of the basic instance methods removed, and delegates - # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a - # corner case, it even removes the +class+ method and that's why you get - # - # blog.posts.class # => Array - # - # though the object behind <tt>blog.posts</tt> is not an Array, but an - # ActiveRecord::Associations::HasManyAssociation. - # - # The <tt>@target</tt> object is not \loaded until needed. For example, - # - # blog.posts.count - # - # is computed directly through SQL and does not trigger by itself the - # instantiation of the actual post records. - class AssociationProxy #:nodoc: - alias_method :proxy_respond_to?, :respond_to? - alias_method :proxy_extend, :extend - delegate :to_param, :to => :proxy_target - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|proxy_/ } - - def initialize(owner, reflection) - @owner, @reflection = owner, reflection - @updated = false - reflection.check_validity! - Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } - reset - end - - # Returns the owner of the proxy. - def proxy_owner - @owner - end - - # Returns the reflection object that represents the association handled - # by the proxy. - def proxy_reflection - @reflection - end - - # Returns the \target of the proxy, same as +target+. - def proxy_target - @target - end - - # Does the proxy or its \target respond to +symbol+? - def respond_to?(*args) - proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) - end - - # Forwards <tt>===</tt> explicitly to the \target because the instance method - # removal above doesn't catch it. Loads the \target if needed. - def ===(other) - load_target - other === @target - end - - # Returns the name of the table of the related class: - # - # post.comments.aliased_table_name # => "comments" - # - def aliased_table_name - @reflection.klass.table_name - end - - # Returns the SQL string that corresponds to the <tt>:conditions</tt> - # option of the macro, if given, or +nil+ otherwise. - def conditions - @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions - end - alias :sql_conditions :conditions - - # Resets the \loaded flag to +false+ and sets the \target to +nil+. - def reset - @loaded = false - @target = nil - end - - # Reloads the \target and returns +self+ on success. - def reload - reset - load_target - self unless @target.nil? - end - - # Has the \target been already \loaded? - def loaded? - @loaded - end - - # Asserts the \target has been loaded setting the \loaded flag to +true+. - def loaded - @loaded = true - end - - # Returns the target of this proxy, same as +proxy_target+. - def target - @target - end - - # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+. - def target=(target) - @target = target - loaded - end - - # Forwards the call to the target. Loads the \target if needed. - def inspect - load_target - @target.inspect - end - - def send(method, *args) - if proxy_respond_to?(method) - super - else - load_target - @target.send(method, *args) - end - end - - protected - # Does the association have a <tt>:dependent</tt> option? - def dependent? - @reflection.options[:dependent] - end - - def interpolate_sql(sql, record = nil) - @owner.send(:interpolate_sql, sql, record) - end - - # Forwards the call to the reflection class. - def sanitize_sql(sql, table_name = @reflection.klass.table_name) - @reflection.klass.send(:sanitize_sql, sql, table_name) - end - - # Assigns the ID of the owner to the corresponding foreign key in +record+. - # If the association is polymorphic the type of the owner is also set. - def set_belongs_to_association_for(record) - if @reflection.options[:as] - record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record? - record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s - else - unless @owner.new_record? - primary_key = @reflection.options[:primary_key] || :id - record[@reflection.primary_key_name] = @owner.send(primary_key) - end - end - end - - # Merges into +options+ the ones coming from the reflection. - def merge_options_from_reflection!(options) - options.reverse_merge!( - :group => @reflection.options[:group], - :having => @reflection.options[:having], - :limit => @reflection.options[:limit], - :offset => @reflection.options[:offset], - :joins => @reflection.options[:joins], - :include => @reflection.options[:include], - :select => @reflection.options[:select], - :readonly => @reflection.options[:readonly] - ) - end - - # Forwards +with_scope+ to the reflection. - def with_scope(*args, &block) - @reflection.klass.send :with_scope, *args, &block - end - - private - # Forwards any missing method call to the \target. - def method_missing(method, *args) - if load_target - unless @target.respond_to?(method) - message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" - raise NoMethodError, message - end - - if block_given? - @target.send(method, *args) { |*block_args| yield(*block_args) } - else - @target.send(method, *args) - end - end - end - - # Loads the \target if needed and returns it. - # - # This method is abstract in the sense that it relies on +find_target+, - # which is expected to be provided by descendants. - # - # If the \target is already \loaded it is just returned. Thus, you can call - # +load_target+ unconditionally to get the \target. - # - # ActiveRecord::RecordNotFound is rescued within the method, and it is - # not reraised. The proxy is \reset and +nil+ is the return value. - def load_target - return nil unless defined?(@loaded) - - if !loaded? and (!@owner.new_record? || foreign_key_present) - @target = find_target - end - - @loaded = true - @target - rescue ActiveRecord::RecordNotFound - reset - end - - # Can be overwritten by associations that might have the foreign key - # available for an association without having the object itself (and - # still being a new record). Currently, only +belongs_to+ presents - # this scenario (both vanilla and polymorphic). - def foreign_key_present - false - end - - # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of - # the kind of the class of the associated objects. Meant to be used as - # a sanity check when you are about to assign an associated record. - def raise_on_type_mismatch(record) - unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize) - message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" - raise ActiveRecord::AssociationTypeMismatch, message - end - end - - if RUBY_VERSION < '1.9.2' - # Array#flatten has problems with recursive arrays before Ruby 1.9.2. - # Going one level deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten - end - else - def flatten_deeper(array) - array.flatten - end - end - - # Returns the ID of the owner, quoted if needed. - def owner_quoted_id - @owner.quoted_id - end - - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.inverse_of - unless inverse_relationship.nil? - record.send(:"set_#{inverse_relationship.name}_target", instance) - end - end - - # Override in subclasses - def we_can_set_the_inverse_on_this?(record) - false - end - end - end -end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb new file mode 100644 index 0000000000..ab102b2b8f --- /dev/null +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -0,0 +1,120 @@ +module ActiveRecord + module Associations + class AssociationScope #:nodoc: + include JoinHelper + + attr_reader :association, :alias_tracker + + delegate :klass, :owner, :reflection, :interpolate, :to => :association + delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection + + def initialize(association) + @association = association + @alias_tracker = AliasTracker.new + end + + def scope + scope = klass.unscoped + scope = scope.extending(*Array.wrap(options[:extend])) + + # It's okay to just apply all these like this. The options will only be present if the + # association supports that option; this is enforced by the association builder. + scope = scope.apply_finder_options(options.slice( + :readonly, :include, :order, :limit, :joins, :group, :having, :offset)) + + if options[:through] && !options[:include] + scope = scope.includes(source_options[:include]) + end + + if select = select_value + scope = scope.select(select) + end + + add_constraints(scope) + end + + private + + def select_value + select_value = options[:select] + + if reflection.collection? + select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" + end + + if reflection.macro == :has_and_belongs_to_many + select_value ||= reflection.klass.arel_table[Arel.star] + end + + select_value + end + + def add_constraints(scope) + tables = construct_tables + + chain.each_with_index do |reflection, i| + table, foreign_table = tables.shift, tables.first + + if reflection.source_macro == :has_and_belongs_to_many + join_table = tables.shift + + scope = scope.joins(join( + join_table, + table[reflection.active_record_primary_key]. + eq(join_table[reflection.association_foreign_key]) + )) + + table, foreign_table = join_table, tables.first + end + + if reflection.source_macro == :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + end + + if reflection == chain.last + scope = scope.where(table[key].eq(owner[foreign_key])) + + conditions[i].each do |condition| + if options[:through] && condition.is_a?(Hash) + condition = { table.name => condition } + end + + scope = scope.where(interpolate(condition)) + end + else + constraint = table[key].eq(foreign_table[foreign_key]) + join = join(foreign_table, constraint) + + scope = scope.joins(join) + + unless conditions[i].empty? + scope = scope.where(sanitize(conditions[i], table)) + end + end + end + + scope + end + + def alias_suffix + reflection.name + end + + def table_name_for(reflection) + if reflection == self.reflection + # If this is a polymorphic belongs_to, we want to get the klass from the + # association because it depends on the polymorphic_type attribute of + # the owner + klass.table_name + else + reflection.table_name + end + end + + end + end +end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 4558872a2b..c263edd2c6 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,41 +1,17 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations - class BelongsToAssociation < AssociationProxy #:nodoc: - def create(attributes = {}) - replace(@reflection.create_association(attributes)) - end - - def build(attributes = {}) - replace(@reflection.build_association(attributes)) - end - + class BelongsToAssociation < SingularAssociation #:nodoc: def replace(record) - counter_cache_name = @reflection.counter_cache_column - - if record.nil? - if counter_cache_name && !@owner.new_record? - @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name] - end - - @target = @owner[@reflection.primary_key_name] = nil - else - raise_on_type_mismatch(record) + raise_on_type_mismatch(record) if record - if counter_cache_name && !@owner.new_record? && record.id != @owner[@reflection.primary_key_name] - @reflection.klass.increment_counter(counter_cache_name, record.id) - @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] - end - - @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record? - @updated = true - end + update_counters(record) + replace_keys(record) + set_inverse_instance(record) - set_inverse_instance(record, @owner) + @updated = true if record - loaded - record + self.target = record end def updated? @@ -43,44 +19,52 @@ module ActiveRecord end private - def find_target - find_method = if @reflection.options[:primary_key] - "find_by_#{@reflection.options[:primary_key]}" - else - "find" - end - the_target = @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :conditions => conditions, - :include => @reflection.options[:include], - :readonly => @reflection.options[:readonly] - ) if @owner[@reflection.primary_key_name] - set_inverse_instance(the_target, @owner) - the_target + + def update_counters(record) + counter_cache_name = reflection.counter_cache_column + + if counter_cache_name && owner.persisted? && different_target?(record) + if record + record.class.increment_counter(counter_cache_name, record.id) + end + + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) + end + end end - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + # Checks whether record is different to the current target, without loading it + def different_target?(record) + record.nil? && owner[reflection.foreign_key] || + record.id != owner[reflection.foreign_key] + end + + def replace_keys(record) + owner[reflection.foreign_key] = record && record[reflection.association_primary_key] + end + + def foreign_key_present? + owner[reflection.foreign_key] end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto # has_one associations. - def we_can_set_the_inverse_on_this?(record) - @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one + def invertible_for?(record) + inverse = inverse_reflection_for(record) + inverse && inverse.macro == :has_one end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def target_id + if options[:primary_key] + owner.send(reflection.name).try(:id) + else + owner[reflection.foreign_key] + end end - def previous_record_id - @previous_record_id ||= if @reflection.options[:primary_key] - previous_record = @owner.send(@reflection.name) - previous_record.nil? ? nil : previous_record.id - else - @owner[@reflection.primary_key_name] - end + def stale_state + owner[reflection.foreign_key].to_s end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 38454ec242..1ca448236e 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,77 +1,33 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations - class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: - def replace(record) - if record.nil? - @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil - else - @target = (AssociationProxy === record ? record.target : record) - - @owner[@reflection.primary_key_name] = record_id(record) - @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s - - @updated = true - end - - set_inverse_instance(record, @owner) - loaded - record - end - - def updated? - @updated - end - + class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: private - # NOTE - for now, we're only supporting inverse setting from belongs_to back onto - # has_one associations. - def we_can_set_the_inverse_on_this?(record) - if @reflection.has_inverse? - inverse_association = @reflection.polymorphic_inverse_of(record.class) - inverse_association && inverse_association.macro == :has_one - else - false - end + def replace_keys(record) + super + owner[reflection.foreign_type] = record && record.class.base_class.name end - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.polymorphic_inverse_of(record.class) - unless inverse_relationship.nil? - record.send(:"set_#{inverse_relationship.name}_target", instance) - end + def different_target?(record) + super || record.class != klass end - def find_target - return nil if association_class.nil? - - target = - if @reflection.options[:conditions] - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :conditions => conditions, - :include => @reflection.options[:include] - ) - else - association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) - end - set_inverse_instance(target, @owner) - target + def inverse_reflection_for(record) + reflection.polymorphic_inverse_of(record.class) end - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + def klass + type = owner[reflection.foreign_type] + type && type.constantize end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def raise_on_type_mismatch(record) + # A polymorphic association cannot have a type mismatch, by definition end - def association_class - @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil + def stale_state + [super, owner[reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb new file mode 100644 index 0000000000..96fca97440 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -0,0 +1,53 @@ +module ActiveRecord::Associations::Builder + class Association #:nodoc: + class_attribute :valid_options + self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate] + + # Set by subclasses + class_attribute :macro + + attr_reader :model, :name, :options, :reflection + + def self.build(model, name, options) + new(model, name, options).build + end + + def initialize(model, name, options) + @model, @name, @options = model, name, options + end + + def build + validate_options + reflection = model.create_reflection(self.class.macro, name, options, model) + define_accessors + reflection + end + + private + + def validate_options + options.assert_valid_keys(self.class.valid_options) + end + + def define_accessors + define_readers + define_writers + end + + def define_readers + name = self.name + + model.redefine_method(name) do |*params| + association(name).reader(*params) + end + end + + def define_writers + name = self.name + + model.redefine_method("#{name}=") do |value| + association(name).writer(value) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb new file mode 100644 index 0000000000..964e7fddc8 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -0,0 +1,83 @@ +module ActiveRecord::Associations::Builder + class BelongsTo < SingularAssociation #:nodoc: + self.macro = :belongs_to + + self.valid_options += [:foreign_type, :polymorphic, :touch] + + def constructable? + !options[:polymorphic] + end + + def build + reflection = super + add_counter_cache_callbacks(reflection) if options[:counter_cache] + add_touch_callbacks(reflection) if options[:touch] + configure_dependency + reflection + end + + private + + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column + name = self.name + + method_name = "belongs_to_counter_cache_after_create_for_#{name}" + model.redefine_method(method_name) do + record = send(name) + record.class.increment_counter(cache_column, record.id) unless record.nil? + end + model.after_create(method_name) + + method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" + model.redefine_method(method_name) do + record = send(name) + record.class.decrement_counter(cache_column, record.id) unless record.nil? + end + model.before_destroy(method_name) + + model.send(:module_eval, + "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ + ) + end + + def add_touch_callbacks(reflection) + name = self.name + method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" + touch = options[:touch] + + model.redefine_method(method_name) do + record = send(name) + + unless record.nil? + if touch == true + record.touch + else + record.touch(touch) + end + end + end + + model.after_save(method_name) + model.after_touch(method_name) + model.after_destroy(method_name) + end + + def configure_dependency + if options[:dependent] + unless [:destroy, :delete].include?(options[:dependent]) + raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" + end + + method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}" + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{method_name} + association = #{name} + association.#{options[:dependent]} if association + end + eoruby + model.after_destroy method_name + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb new file mode 100644 index 0000000000..f62209a226 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -0,0 +1,75 @@ +module ActiveRecord::Associations::Builder + class CollectionAssociation < Association #:nodoc: + CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] + + self.valid_options += [ + :table_name, :order, :group, :having, :limit, :offset, :uniq, :finder_sql, + :counter_sql, :before_add, :after_add, :before_remove, :after_remove + ] + + attr_reader :block_extension + + def self.build(model, name, options, &extension) + new(model, name, options, &extension).build + end + + def initialize(model, name, options, &extension) + super(model, name, options) + @block_extension = extension + end + + def build + wrap_block_extension + reflection = super + CALLBACKS.each { |callback_name| define_callback(callback_name) } + reflection + end + + def writable? + true + end + + private + + def wrap_block_extension + options[:extend] = Array.wrap(options[:extend]) + + if block_extension + silence_warnings do + model.parent.const_set(extension_module_name, Module.new(&block_extension)) + end + options[:extend].push("#{model.parent}::#{extension_module_name}".constantize) + end + end + + def extension_module_name + @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension" + end + + def define_callback(callback_name) + full_callback_name = "#{callback_name}_for_#{name}" + + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) + model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym])) + end + + def define_readers + super + + name = self.name + model.redefine_method("#{name.to_s.singularize}_ids") do + association(name).ids_reader + end + end + + def define_writers + super + + name = self.name + model.redefine_method("#{name.to_s.singularize}_ids=") do |ids| + association(name).ids_writer(ids) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb new file mode 100644 index 0000000000..4b48757da7 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -0,0 +1,63 @@ +module ActiveRecord::Associations::Builder + class HasAndBelongsToMany < CollectionAssociation #:nodoc: + self.macro = :has_and_belongs_to_many + + self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + + def build + reflection = super + check_validity(reflection) + define_after_destroy_method + reflection + end + + private + + def define_after_destroy_method + name = self.name + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{after_destroy_method_name} + association(#{name.to_sym.inspect}).delete_all + end + eoruby + model.after_destroy after_destroy_method_name + end + + def after_destroy_method_name + "has_and_belongs_to_many_after_destroy_for_#{name}" + end + + # TODO: These checks should probably be moved into the Reflection, and we should not be + # redefining the options[:join_table] value - instead we should define a + # reflection.join_table method. + def check_validity(reflection) + if reflection.association_foreign_key == reflection.foreign_key + raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) + end + + reflection.options[:join_table] ||= join_table_name( + model.send(:undecorated_table_name, model.to_s), + model.send(:undecorated_table_name, reflection.class_name) + ) + + if model.connection.supports_primary_key? && (model.connection.primary_key(reflection.options[:join_table]) rescue false) + raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection) + end + end + + # Generates a join table name from two provided table names. + # The names in the join table names end up in lexicographic order. + # + # join_table_name("members", "clubs") # => "clubs_members" + # join_table_name("members", "special_clubs") # => "members_special_clubs" + def join_table_name(first_table_name, second_table_name) + if first_table_name < second_table_name + join_table = "#{first_table_name}_#{second_table_name}" + else + join_table = "#{second_table_name}_#{first_table_name}" + end + + model.table_name_prefix + join_table + model.table_name_suffix + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb new file mode 100644 index 0000000000..77bb66228d --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -0,0 +1,63 @@ +module ActiveRecord::Associations::Builder + class HasMany < CollectionAssociation #:nodoc: + self.macro = :has_many + + self.valid_options += [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] + + def build + reflection = super + configure_dependency + reflection + end + + private + + def configure_dependency + if options[:dependent] + unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent]) + raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ + ":nullify or :restrict (#{options[:dependent].inspect})" + end + + send("define_#{options[:dependent]}_dependency_method") + model.before_destroy dependency_method_name + end + end + + def define_destroy_dependency_method + name = self.name + model.send(:define_method, dependency_method_name) do + send(name).each do |o| + # No point in executing the counter update since we're going to destroy the parent anyway + counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym + if o.respond_to?(counter_method) + class << o + self + end.send(:define_method, counter_method, Proc.new {}) + end + end + + send(name).delete_all + end + end + + def define_delete_all_dependency_method + name = self.name + model.send(:define_method, dependency_method_name) do + send(name).delete_all + end + end + alias :define_nullify_dependency_method :define_delete_all_dependency_method + + def define_restrict_dependency_method + name = self.name + model.send(:define_method, dependency_method_name) do + raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? + end + end + + def dependency_method_name + "has_many_dependent_for_#{name}" + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb new file mode 100644 index 0000000000..07ba5d088e --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -0,0 +1,61 @@ +module ActiveRecord::Associations::Builder + class HasOne < SingularAssociation #:nodoc: + self.macro = :has_one + + self.valid_options += [:order, :as] + + class_attribute :through_options + self.through_options = [:through, :source, :source_type] + + def constructable? + !options[:through] + end + + def build + reflection = super + configure_dependency unless options[:through] + reflection + end + + private + + def validate_options + valid_options = self.class.valid_options + valid_options += self.class.through_options if options[:through] + options.assert_valid_keys(valid_options) + end + + def configure_dependency + if options[:dependent] + unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent]) + raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ + ":nullify or :restrict (#{options[:dependent].inspect})" + end + + send("define_#{options[:dependent]}_dependency_method") + model.before_destroy dependency_method_name + end + end + + def dependency_method_name + "has_one_dependent_#{options[:dependent]}_for_#{name}" + end + + def define_destroy_dependency_method + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{dependency_method_name} + association(#{name.to_sym.inspect}).delete + end + eoruby + end + alias :define_delete_dependency_method :define_destroy_dependency_method + alias :define_nullify_dependency_method :define_destroy_dependency_method + + def define_restrict_dependency_method + name = self.name + model.redefine_method(dependency_method_name) do + raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? + end + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb new file mode 100644 index 0000000000..06a414b874 --- /dev/null +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -0,0 +1,32 @@ +module ActiveRecord::Associations::Builder + class SingularAssociation < Association #:nodoc: + self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + + def constructable? + true + end + + def define_accessors + super + define_constructors if constructable? + end + + private + + def define_constructors + name = self.name + + model.redefine_method("build_#{name}") do |*params| + association(name).build(*params) + end + + model.redefine_method("create_#{name}") do |*params| + association(name).create(*params) + end + + model.redefine_method("create_#{name}!") do |*params| + association(name).create!(*params) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb new file mode 100644 index 0000000000..9f4fc44cc6 --- /dev/null +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -0,0 +1,536 @@ +require 'active_support/core_ext/array/wrap' + +module ActiveRecord + module Associations + # = Active Record Association Collection + # + # AssociationCollection is an abstract class that provides common stuff to + # ease the implementation of association proxies that represent + # collections. See the class hierarchy in AssociationProxy. + # + # You need to be careful with assumptions regarding the target: The proxy + # does not fetch records from the database until it needs them, but new + # ones created with +build+ are added to the target. So, the target may be + # non-empty and still lack children waiting to be read from the database. + # If you look directly to the database you cannot assume that's the entire + # collection because new records may have been added to the target, etc. + # + # If you need to work on all current children, new and existing records, + # +load_target+ and the +loaded+ flag are your friends. + class CollectionAssociation < Association #:nodoc: + attr_reader :proxy + + def initialize(owner, reflection) + # When scopes are created via method_missing on the proxy, they are stored so that + # any records fetched from the database are kept around for future use. + @scopes_cache = Hash.new do |hash, method| + hash[method] = { } + end + + super + + @proxy = CollectionProxy.new(self) + end + + # Implements the reader method, e.g. foo.items for Foo.has_many :items + def reader(force_reload = false) + if force_reload + klass.uncached { reload } + elsif stale_target? + reload + end + + proxy + end + + # Implements the writer method, e.g. foo.items= for Foo.has_many :items + def writer(records) + replace(records) + end + + # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items + def ids_reader + if loaded? || options[:finder_sql] + load_target.map do |record| + record.send(reflection.association_primary_key) + end + else + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + + scoped.select(column).except(:includes).map! do |record| + record.send(reflection.association_primary_key) + end + end + end + + # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items + def ids_writer(ids) + pk_column = reflection.primary_key_column + ids = Array.wrap(ids).reject { |id| id.blank? } + ids.map! { |i| pk_column.type_cast(i) } + replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) + end + + def reset + @loaded = false + @target = [] + @scopes_cache.clear + end + + def select(select = nil) + if block_given? + load_target.select.each { |e| yield e } + else + scoped.select(select) + end + end + + def find(*args) + if options[:finder_sql] + find_by_scan(*args) + else + scoped.find(*args) + end + end + + def first(*args) + first_or_last(:first, *args) + end + + def last(*args) + first_or_last(:last, *args) + end + + def build(attributes = {}, &block) + build_or_create(attributes, :build, &block) + end + + def create(attributes = {}, &block) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + + build_or_create(attributes, :create, &block) + end + + def create!(attrs = {}, &block) + record = create(attrs, &block) + Array.wrap(record).each(&:save!) + record + end + + # Add +records+ to this association. Returns +self+ so method calls may be chained. + # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. + def concat(*records) + result = true + load_target if owner.new_record? + + transaction do + records.flatten.each do |record| + raise_on_type_mismatch(record) + add_to_target(record) do |r| + result &&= insert_record(record) unless owner.new_record? + end + end + end + + result && records + end + + # Starts a transaction in the association class's database connection. + # + # class Author < ActiveRecord::Base + # has_many :books + # end + # + # Author.first.books.transaction do + # # same effect as calling Book.transaction + # end + def transaction(*args) + reflection.klass.transaction(*args) do + yield + end + end + + # Remove all records from this association + # + # See delete for more info. + def delete_all + delete(load_target).tap do + reset + loaded! + end + end + + # Destroy all the records from this association. + # + # See destroy for more info. + def destroy_all + destroy(load_target).tap do + reset + loaded! + end + end + + # Calculate sum using SQL, not Enumerable + def sum(*args) + if block_given? + scoped.sum(*args) { |*block_args| yield(*block_args) } + else + scoped.sum(*args) + end + end + + # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the + # association, it will be used for the query. Otherwise, construct options and pass them with + # scope to the target class's +count+. + def count(column_name = nil, count_options = {}) + column_name, count_options = nil, column_name if column_name.is_a?(Hash) + + if options[:counter_sql] || options[:finder_sql] + unless count_options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + end + + reflection.klass.count_by_sql(custom_counter_sql) + else + if options[:uniq] + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name ||= reflection.klass.primary_key + count_options.merge!(:distinct => true) + end + + value = scoped.count(column_name, count_options) + + limit = options[:limit] + offset = options[:offset] + + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value + end + end + end + + # Removes +records+ from this association calling +before_remove+ and + # +after_remove+ callbacks. + # + # This method is abstract in the sense that +delete_records+ has to be + # provided by descendants. Note this method does not imply the records + # are actually removed from the database, that depends precisely on + # +delete_records+. They are in any case removed from the collection. + def delete(*records) + delete_or_destroy(records, options[:dependent]) + end + + # Destroy +records+ and remove them from this association calling + # +before_remove+ and +after_remove+ callbacks. + # + # Note that this method will _always_ remove records from the database + # ignoring the +:dependent+ option. + def destroy(*records) + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, :destroy) + end + + # Returns the size of the collection by executing a SELECT COUNT(*) + # query if the collection hasn't been loaded, and calling + # <tt>collection.size</tt> if it has. + # + # If the collection has been already loaded +size+ and +length+ are + # equivalent. If not and you are going to need the records anyway + # +length+ will take one less query. Otherwise +size+ is more efficient. + # + # This method is abstract in the sense that it relies on + # +count_records+, which is a method descendants have to provide. + def size + if owner.new_record? || (loaded? && !options[:uniq]) + target.size + elsif !loaded? && options[:group] + load_target.size + elsif !loaded? && !options[:uniq] && target.is_a?(Array) + unsaved_records = target.select { |r| r.new_record? } + unsaved_records.size + count_records + else + count_records + end + end + + # Returns the size of the collection calling +size+ on the target. + # + # If the collection has been already loaded +length+ and +size+ are + # equivalent. If not and you are going to need the records anyway this + # method will take one less query. Otherwise +size+ is more efficient. + def length + load_target.size + end + + # Equivalent to <tt>collection.size.zero?</tt>. If the collection has + # not been already loaded and you are going to fetch the records anyway + # it is better to check <tt>collection.length.zero?</tt>. + def empty? + size.zero? + end + + def any? + if block_given? + load_target.any? { |*block_args| yield(*block_args) } + else + !empty? + end + end + + # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. + def many? + if block_given? + load_target.many? { |*block_args| yield(*block_args) } + else + size > 1 + end + end + + def uniq(collection = load_target) + seen = {} + collection.find_all do |record| + seen[record.id] = true unless seen.key?(record.id) + end + end + + # 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.each { |val| raise_on_type_mismatch(val) } + original_target = load_target.dup + + transaction do + delete(target - other_array) + + unless concat(other_array - target) + @target = original_target + raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ + "new records could not be saved." + end + end + end + + def include?(record) + if record.is_a?(reflection.klass) + if record.new_record? + include_in_memory?(record) + else + load_target if options[:finder_sql] + loaded? ? target.include?(record) : scoped.exists?(record) + end + else + false + end + end + + def cached_scope(method, args) + @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) + end + + def load_target + if find_target? + targets = [] + + begin + targets = find_target + rescue ActiveRecord::RecordNotFound + reset + end + + @target = merge_target_lists(targets, target) + end + + loaded! + target + end + + def add_to_target(record) + transaction do + callback(:before_add, record) + yield(record) if block_given? + + if options[:uniq] && index = @target.index(record) + @target[index] = record + else + @target << record + end + + callback(:after_add, record) + set_inverse_instance(record) + end + + record + end + + private + + def custom_counter_sql + if options[:counter_sql] + interpolate(options[:counter_sql]) + else + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + end + end + + def custom_finder_sql + interpolate(options[:finder_sql]) + end + + def find_target + records = + if options[:finder_sql] + reflection.klass.find_by_sql(custom_finder_sql) + else + find(:all) + end + + records = options[:uniq] ? uniq(records) : records + records.each { |record| set_inverse_instance(record) } + records + end + + def merge_target_lists(loaded, existing) + return loaded if existing.empty? + return existing if loaded.empty? + + loaded.map do |f| + i = existing.index(f) + if i + existing.delete_at(i).tap do |t| + keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) + # FIXME: this call to attributes causes many NoMethodErrors + attributes = f.attributes + (attributes.keys - keys).each do |k| + t.send("#{k}=", attributes[k]) + end + end + else + f + end + end + existing + end + + def build_or_create(attributes, method) + records = Array.wrap(attributes).map do |attrs| + record = build_record(attrs) + + add_to_target(record) do + yield(record) if block_given? + insert_record(record) if method == :create + end + end + + attributes.is_a?(Array) ? records : records.first + end + + # Do the relevant stuff to insert the given record into the association collection. + def insert_record(record, validate = true) + raise NotImplementedError + end + + def build_record(attributes) + reflection.build_association(scoped.scope_for_create.merge(attributes)) + end + + def delete_or_destroy(records, method) + records = records.flatten + records.each { |record| raise_on_type_mismatch(record) } + existing_records = records.reject { |r| r.new_record? } + + transaction do + records.each { |record| callback(:before_remove, record) } + + delete_records(existing_records, method) if existing_records.any? + records.each { |record| target.delete(record) } + + records.each { |record| callback(:after_remove, record) } + end + end + + # Delete the given records from the association, using one of the methods :destroy, + # :delete_all or :nullify (or nil, in which case a default is used). + def delete_records(records, method) + raise NotImplementedError + end + + def callback(method, record) + callbacks_for(method).each do |callback| + case callback + when Symbol + owner.send(callback, record) + when Proc + callback.call(owner, record) + else + callback.send(method, owner, record) + end + end + end + + def callbacks_for(callback_name) + full_callback_name = "#{callback_name}_for_#{reflection.name}" + owner.class.send(full_callback_name.to_sym) || [] + end + + # Should we deal with assoc.first or assoc.last by issuing an independent query to + # the database, or by getting the target, and then taking the first/last item from that? + # + # If the args is just a non-empty options hash, go to the database. + # + # Otherwise, go to the database only if none of the following are true: + # * target already loaded + # * owner is new record + # * custom :finder_sql exists + # * target contains new or changed record(s) + # * the first arg is an integer (which indicates the number of records to be returned) + def fetch_first_or_last_using_find?(args) + if args.first.is_a?(Hash) + true + else + !(loaded? || + owner.new_record? || + options[:finder_sql] || + target.any? { |record| record.new_record? || record.changed? } || + args.first.kind_of?(Integer)) + end + end + + def include_in_memory?(record) + if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) + owner.send(reflection.through_reflection.name).any? { |source| + target = source.send(reflection.source_reflection.name) + target.respond_to?(:include?) ? target.include?(record) : target == record + } || target.include?(record) + else + target.include?(record) + end + end + + # If using a custom finder_sql, #find scans the entire collection. + def find_by_scan(*args) + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + end + + # Fetches the first/last using SQL if possible, otherwise from the target array. + def first_or_last(type, *args) + args.shift if args.first.is_a?(Hash) && args.first.empty? + + collection = fetch_first_or_last_using_find?(args) ? scoped : load_target + collection.send(type, *args) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb new file mode 100644 index 0000000000..cf77d770c9 --- /dev/null +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -0,0 +1,127 @@ +module ActiveRecord + module Associations + # Association proxies in Active Record are middlemen between the object that + # holds the association, known as the <tt>@owner</tt>, and the actual associated + # object, known as the <tt>@target</tt>. The kind of association any proxy is + # about is available in <tt>@reflection</tt>. That's an instance of the class + # ActiveRecord::Reflection::AssociationReflection. + # + # For example, given + # + # class Blog < ActiveRecord::Base + # has_many :posts + # end + # + # blog = Blog.find(:first) + # + # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as + # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and + # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. + # + # This class has most of the basic instance methods removed, and delegates + # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a + # corner case, it even removes the +class+ method and that's why you get + # + # blog.posts.class # => Array + # + # though the object behind <tt>blog.posts</tt> is not an Array, but an + # ActiveRecord::Associations::HasManyAssociation. + # + # The <tt>@target</tt> object is not \loaded until needed. For example, + # + # blog.posts.count + # + # is computed directly through SQL and does not trigger by itself the + # instantiation of the actual post records. + class CollectionProxy # :nodoc: + alias :proxy_extend :extend + + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, + :lock, :readonly, :having, :to => :scoped + + delegate :target, :load_target, :loaded?, :scoped, + :to => :@association + + delegate :select, :find, :first, :last, + :build, :create, :create!, + :concat, :delete_all, :destroy_all, :delete, :destroy, :uniq, + :sum, :count, :size, :length, :empty?, + :any?, :many?, :include?, + :to => :@association + + def initialize(association) + @association = association + Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) } + end + + def respond_to?(*args) + super || + (load_target && target.respond_to?(*args)) || + @association.klass.respond_to?(*args) + end + + def method_missing(method, *args, &block) + match = DynamicFinderMatch.match(method) + if match && match.creator? + attributes = match.attribute_names + return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) + end + + if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method)) + if load_target + if target.respond_to?(method) + target.send(method, *args, &block) + else + begin + super + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") + end + end + end + + elsif @association.klass.scopes[method] + @association.cached_scope(method, args) + else + scoped.readonly(nil).send(method, *args, &block) + end + end + + # Forwards <tt>===</tt> explicitly to the \target because the instance method + # removal above doesn't catch it. Loads the \target if needed. + def ===(other) + other === load_target + end + + def to_ary + load_target.dup + end + alias_method :to_a, :to_ary + + def <<(*records) + @association.concat(records) && self + end + alias_method :push, :<< + + def clear + delete_all + self + end + + def reload + @association.reload + self + end + + def new(*args, &block) + if @association.is_a?(HasManyThroughAssociation) + @association.build(*args, &block) + else + method_missing(:new, *args, &block) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index bec123e7a2..217213808b 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -1,136 +1,51 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations - class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: - def create(attributes = {}) - create_record(attributes) { |record| insert_record(record) } - end + class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc: + attr_reader :join_table - def create!(attributes = {}) - create_record(attributes) { |record| insert_record(record, true) } + def initialize(owner, reflection) + @join_table = Arel::Table.new(reflection.options[:join_table]) + super end - def columns - @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") - end + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) - def reset_column_information - @reflection.reset_column_information - end + if options[:insert_sql] + owner.connection.insert(interpolate(options[:insert_sql], record)) + else + stmt = join_table.compile_insert( + join_table[reflection.foreign_key] => owner.id, + join_table[reflection.association_foreign_key] => record.id + ) + + owner.connection.insert stmt.to_sql + end - def has_primary_key? - @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table]) + record end - protected - def construct_find_options!(options) - options[:joins] = @join_sql - options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) - options[:select] ||= (@reflection.options[:select] || '*') - end + private def count_records load_target.size end - def insert_record(record, force = true, validate = true) - if record.new_record? - if force - record.save! - else - return false unless record.save(:validate => validate) - end - end - - if @reflection.options[:insert_sql] - @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) + def delete_records(records, method) + if sql = options[:delete_sql] + records.each { |record| owner.connection.delete(interpolate(sql, record)) } else - relation = Arel::Table.new(@reflection.options[:join_table]) - timestamps = record_timestamp_columns(record) - timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? - - attributes = columns.inject({}) do |attrs, column| - name = column.name - case name.to_s - when @reflection.primary_key_name.to_s - attrs[relation[name]] = @owner.id - when @reflection.association_foreign_key.to_s - attrs[relation[name]] = record.id - when *timestamps - attrs[relation[name]] = timezone - else - if record.has_attribute?(name) - value = @owner.send(:quote_value, record[name], column) - attrs[relation[name]] = value unless value.nil? - end - end - attrs - end - - relation.insert(attributes) - end - - return true - end - - def delete_records(records) - if sql = @reflection.options[:delete_sql] - records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } - else - relation = Arel::Table.new(@reflection.options[:join_table]) - relation.where(relation[@reflection.primary_key_name].eq(@owner.id). - and(Arel::Predicates::In.new(relation[@reflection.association_foreign_key], records.map(&:id))) - ).delete + relation = join_table + stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). + and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) + ).compile_delete + owner.connection.delete stmt.to_sql end end - def construct_sql - if @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - else - @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " - @finder_sql << " AND (#{conditions})" if conditions - end - - @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" - - construct_counter_sql - end - - def construct_scope - { :find => { :conditions => @finder_sql, - :joins => @join_sql, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] } } - end - - # Join tables with additional columns on top of the two foreign keys must be considered - # ambiguous unless a select clause has been explicitly defined. Otherwise you can get - # broken records back, if, for example, the join column also has an id column. This will - # then overwrite the id column of the records coming back. - def finding_with_ambiguous_select?(select_clause) - !select_clause && columns.size != 2 - end - - private - def create_record(attributes, &block) - # Can't use Base.create because the foreign key may be a protected attribute. - ensure_owner_is_not_new - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr) } - else - build_record(attributes, &block) - end - end - - def record_timestamp_columns(record) - if record.record_timestamps - record.send(:all_timestamp_attributes).map(&:to_s) - else - [] - end + def invertible_for?(record) + false end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index c33bc6aa47..78c5c4b870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -5,19 +5,14 @@ module ActiveRecord # # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. - class HasManyAssociation < AssociationCollection #:nodoc: - def initialize(owner, reflection) - @finder_sql = nil - super + class HasManyAssociation < CollectionAssociation #:nodoc: + + def insert_record(record, validate = true) + set_owner_attributes(record) + record.save(:validate => validate) end - protected - def owner_quoted_id - if @reflection.options[:primary_key] - quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end - end + + private # Returns the number of records in this collection. # @@ -34,94 +29,70 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - @owner.send(:read_attribute, cached_counter_attribute_name) - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + owner.send(:read_attribute, cached_counter_attribute_name) + elsif options[:counter_sql] || options[:finder_sql] + reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include]) + scoped.count end # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - @target ||= [] and loaded if count == 0 - - if @reflection.options[:limit] - count = [ @reflection.options[:limit], count ].min - end - - return count - end + @target ||= [] and loaded! if count == 0 - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) + [options[:limit], count].compact.min end - def cached_counter_attribute_name - "#{@reflection.name}_count" + def has_cached_counter?(reflection = reflection) + owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def insert_record(record, force = false, validate = true) - set_belongs_to_association_for(record) - force ? record.save! : record.save(:validate => validate) + def cached_counter_attribute_name(reflection = reflection) + "#{reflection.name}_count" end - # Deletes the records according to the <tt>:dependent</tt> option. - def delete_records(records) - case @reflection.options[:dependent] - when :destroy - records.each { |r| r.destroy } - when :delete_all - @reflection.klass.delete(records.map { |record| record.id }) - else - relation = Arel::Table.new(@reflection.table_name) - relation.where(relation[@reflection.primary_key_name].eq(@owner.id). - and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id))) - ).update(relation[@reflection.primary_key_name] => nil) - - @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter? + def update_counter(difference, reflection = reflection) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) + owner.class.update_counters(owner.id, counter => difference) + owner[counter] += difference + owner.changed_attributes.delete(counter) # eww end end - def target_obsolete? - false + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_updates_counter_cache?(reflection = reflection) + counter_name = cached_counter_attribute_name(reflection) + reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + inverse_reflection.counter_cache_column == counter_name + } end - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - @finder_sql << " AND (#{conditions})" if conditions + # Deletes the records according to the <tt>:dependent</tt> option. + def delete_records(records, method) + if method == :destroy + records.each { |r| r.destroy } + update_counter(-records.length) unless inverse_updates_counter_cache? + else + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) + if method == :delete_all + update_counter(-scope.delete_all) else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions + update_counter(-scope.update_all(reflection.foreign_key => nil)) + end end - - construct_counter_sql - end - - def construct_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - { - :find => { :conditions => @finder_sql, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include]}, - :create => create_scoping - } - end - - def we_can_set_the_inverse_on_this?(record) - inverse = @reflection.inverse_of - return !inverse.nil? end end end 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 608b1c741a..9d2b29685b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,114 +1,144 @@ -require "active_record/associations/through_association_scope" require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: - include ThroughAssociationScope + include ThroughAssociation alias_method :new, :build - def create!(attrs = nil) - create_record(attrs, true) - end - - def create(attrs = nil) - create_record(attrs, false) + # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been + # loaded and calling collection.size if it has. If it's more likely than not that the collection does + # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer + # SELECT query if you use #length. + def size + if has_cached_counter? + owner.send(:read_attribute, cached_counter_attribute_name) + elsif loaded? + target.size + else + count + end end - def destroy(*records) - transaction do - delete_records(flatten_deeper(records)) - super + def concat(*records) + unless owner.new_record? + records.flatten.each do |record| + raise_on_type_mismatch(record) + record.save! if record.new_record? + end end + + super end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been - # loaded and calling collection.size if it has. If it's more likely than not that the collection does - # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer - # SELECT query if you use #length. - def size - return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? - return @target.size if loaded? - return count + def insert_record(record, validate = true) + ensure_not_nested + return if record.new_record? && !record.save(:validate => validate) + + through_record(record).save! + update_counter(1) + record end - protected - def create_record(attrs, force = true) - ensure_owner_is_not_new + private - transaction do - object = @reflection.klass.new(attrs) - add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) } - object + def through_record(record) + through_association = owner.association(through_reflection.name) + attributes = construct_join_attributes(record) + + through_record = Array.wrap(through_association.target).find { |candidate| + candidate.attributes.slice(*attributes.keys) == attributes + } + + unless through_record + through_record = through_association.build(attributes) + through_record.send("#{source_reflection.name}=", record) + end + + through_record + end + + def build_record(attributes) + ensure_not_nested + + record = super(attributes) + + inverse = source_reflection.inverse_of + if inverse + if inverse.macro == :has_many + record.send(inverse.name) << through_record(record) + elsif inverse.macro == :has_one + record.send("#{inverse.name}=", through_record(record)) + end end + + record end def target_reflection_has_associated_record? - if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? + if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank? false else true end end - def construct_find_options!(options) - options[:joins] = construct_joins(options[:joins]) - options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] + def update_through_counter?(method) + case method + when :destroy + !inverse_updates_counter_cache?(through_reflection) + when :nullify + false + else + true + end end - def insert_record(record, force = true, validate = true) - if record.new_record? - if force - record.save! - else - return false unless record.save(:validate => validate) - end - end + def delete_records(records, method) + ensure_not_nested - through_association = @owner.send(@reflection.through_reflection.name) - through_record = through_association.create!(construct_join_attributes(record)) - through_association.proxy_target << through_record - end + through = owner.association(through_reflection.name) + scope = through.scoped.where(construct_join_attributes(*records)) - # TODO - add dependent option support - def delete_records(records) - klass = @reflection.through_reflection.klass - records.each do |associate| - klass.delete_all(construct_join_attributes(associate)) + case method + when :destroy + count = scope.destroy_all.length + when :nullify + count = scope.update_all(source_reflection.foreign_key => nil) + else + count = scope.delete_all end - end - def find_target - return [] unless target_reflection_has_associated_record? - with_scope(construct_scope) { @reflection.klass.find(:all) } - end - - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) + delete_through_records(through, records) - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions - else - @finder_sql = construct_conditions + if through_reflection.macro == :has_many && update_through_counter?(method) + update_counter(-count, through_reflection) end - construct_counter_sql + update_counter(-count) end - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) + def delete_through_records(through, records) + if through_reflection.macro == :has_many + records.each do |record| + through.target.delete(through_record(record)) + end + else + records.each do |record| + through.target = nil if through.target == through_record(record) + end + end end - def cached_counter_attribute_name - "#{@reflection.name}_count" + def find_target + return [] unless target_reflection_has_associated_record? + scoped.all end # NOTE - not sure that we can actually cope with inverses here - def we_can_set_the_inverse_on_this?(record) + def invertible_for?(record) false end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 68b8b792ad..1d2e8667e4 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,142 +1,70 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations - class HasOneAssociation < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end + class HasOneAssociation < SingularAssociation #:nodoc: + def replace(record, save = true) + raise_on_type_mismatch(record) if record + load_target - def create(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.create_association(attrs) - end - end + reflection.klass.transaction do + if target && target != record + remove_target!(options[:dependent]) + end - def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.create_association!(attrs) - end - end + if record + set_inverse_instance(record) + set_owner_attributes(record) - def build(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.build_association(attrs) + if owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end + end end - end - def replace(obj, dont_save = false) - load_target + self.target = record + end - unless @target.nil? || @target == obj - if dependent? && !dont_save - case @reflection.options[:dependent] + def delete(method = options[:dependent]) + if load_target + case method when :delete - @target.delete unless @target.new_record? - @owner.clear_association_cache + target.delete when :destroy - @target.destroy unless @target.new_record? - @owner.clear_association_cache + target.destroy when :nullify - @target[@reflection.primary_key_name] = nil - @target.save unless @owner.new_record? || @target.new_record? - end - else - @target[@reflection.primary_key_name] = nil - @target.save unless @owner.new_record? || @target.new_record? + target.update_attribute(reflection.foreign_key, nil) end end - - if obj.nil? - @target = nil - else - raise_on_type_mismatch(obj) - set_belongs_to_association_for(obj) - @target = (AssociationProxy === obj ? obj.target : obj) - end - - set_inverse_instance(obj, @owner) - @loaded = true - - unless @owner.new_record? or obj.nil? or dont_save - return (obj.save ? self : false) - else - return (obj.nil? ? nil : self) - end end - protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end - end - private - def find_target - the_target = @reflection.klass.find(:first, - :conditions => @finder_sql, - :select => @reflection.options[:select], - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :readonly => @reflection.options[:readonly] - ) - set_inverse_instance(the_target, @owner) - the_target - end - def construct_sql - case - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - end - @finder_sql << " AND (#{conditions})" if conditions + # The reason that the save param for replace is false, if for create (not just build), + # is because the setting of the foreign keys is actually handled by the scoping when + # the record is instantiated, and so they are set straight away and do not need to be + # updated within replace. + def set_new_record(record) + replace(record, false) end - def construct_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - { :create => create_scoping } - end - - def new_record(replace_existing) - # Make sure we load the target first, if we plan on replacing the existing - # 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]) do - yield @reflection - end - - if replace_existing - replace(record, true) + def remove_target!(method) + if [:delete, :destroy].include?(method) + target.send(method) else - record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? - self.target = record - set_inverse_instance(record, @owner) - end + nullify_owner_attributes(target) - record - end - - def we_can_set_the_inverse_on_this?(record) - inverse = @reflection.inverse_of - return !inverse.nil? + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." + end + end end - def merge_with_conditions(attrs={}) - attrs ||= {} - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - attrs + def nullify_owner_attributes(record) + record[reflection.foreign_key] = nil end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index fba0a2bfcc..fdf8ae1453 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,40 +1,36 @@ -require "active_record/associations/through_association_scope" - module ActiveRecord # = Active Record Has One Through Association module Associations - class HasOneThroughAssociation < HasOneAssociation - include ThroughAssociationScope + class HasOneThroughAssociation < HasOneAssociation #:nodoc: + include ThroughAssociation - def replace(new_value) - create_through_record(new_value) - @target = new_value + def replace(record) + create_through_record(record) + self.target = record end private - def create_through_record(new_value) #nodoc: - klass = @reflection.through_reflection.klass + def create_through_record(record) + ensure_not_nested + + through_proxy = owner.association(through_reflection.name) + through_record = through_proxy.send(:load_target) - current_object = @owner.send(@reflection.through_reflection.name) + if through_record && !record + through_record.destroy + elsif record + attributes = construct_join_attributes(record) - if current_object - new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy - elsif new_value - if @owner.new_record? - self.target = new_value - through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name) - through_association.build(construct_join_attributes(new_value)) - else - @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value))) + if through_record + through_record.update_attributes(attributes) + elsif owner.new_record? + through_proxy.build(attributes) + else + through_proxy.create(attributes) + end end end - end - - private - def find_target - with_scope(construct_scope) { @reflection.klass.find(:first) } - end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb new file mode 100644 index 0000000000..504f25271c --- /dev/null +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -0,0 +1,215 @@ +module ActiveRecord + module Associations + class JoinDependency # :nodoc: + autoload :JoinPart, 'active_record/associations/join_dependency/join_part' + autoload :JoinBase, 'active_record/associations/join_dependency/join_base' + autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' + + attr_reader :join_parts, :reflections, :alias_tracker, :active_record + + def initialize(base, associations, joins) + @active_record = base + @table_joins = joins + @join_parts = [JoinBase.new(base)] + @associations = {} + @reflections = [] + @alias_tracker = AliasTracker.new(joins) + @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 + build(associations) + end + + def graft(*associations) + associations.each do |association| + join_associations.detect {|a| association == a} || + build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) + end + self + end + + def join_associations + join_parts.last(join_parts.length - 1) + end + + def join_base + join_parts.first + end + + def columns + join_parts.collect { |join_part| + table = join_part.aliased_table + join_part.column_names_with_alias.collect{ |column_name, aliased_name| + table[column_name].as Arel.sql(aliased_name) + } + }.flatten + end + + def instantiate(rows) + primary_key = join_base.aliased_primary_key + parents = {} + + records = rows.map { |model| + primary_id = model[primary_key] + parent = parents[primary_id] ||= join_base.instantiate(model) + construct(parent, @associations, join_associations, model) + parent + }.uniq + + remove_duplicate_results!(active_record, records, @associations) + records + end + + def remove_duplicate_results!(base, records, associations) + case associations + when Symbol, String + reflection = base.reflections[associations] + remove_uniq_by_reflection(reflection, records) + when Array + associations.each do |association| + remove_duplicate_results!(base, records, association) + end + when Hash + associations.keys.each do |name| + reflection = base.reflections[name] + remove_uniq_by_reflection(reflection, records) + + parent_records = [] + records.each do |record| + if descendant = record.send(reflection.name) + if reflection.collection? + parent_records.concat descendant.target.uniq + else + parent_records << descendant + end + end + end + + remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? + end + end + end + + protected + + def cache_joined_association(association) + associations = [] + parent = association.parent + while parent != join_base + associations.unshift(parent.reflection.name) + parent = parent.parent + end + ref = @associations + associations.each do |key| + ref = ref[key] + end + ref[association.reflection.name] ||= {} + end + + def build(associations, parent = nil, join_type = Arel::InnerJoin) + parent ||= join_parts.last + case associations + when Symbol, String + reflection = parent.reflections[associations.to_s.intern] or + raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" + unless join_association = find_join_association(reflection, parent) + @reflections << reflection + join_association = build_join_association(reflection, parent) + join_association.join_type = join_type + @join_parts << join_association + cache_joined_association(join_association) + end + join_association + when Array + associations.each do |association| + build(association, parent, join_type) + end + when Hash + associations.keys.sort_by { |a| a.to_s }.each do |name| + join_association = build(name, parent, join_type) + build(associations[name], join_association, join_type) + end + else + raise ConfigurationError, associations.inspect + end + end + + def find_join_association(name_or_reflection, parent) + if String === name_or_reflection + name_or_reflection = name_or_reflection.to_sym + end + + join_associations.detect { |j| + j.reflection == name_or_reflection && j.parent == parent + } + end + + def remove_uniq_by_reflection(reflection, records) + if reflection && reflection.collection? + records.each { |record| record.send(reflection.name).target.uniq! } + end + end + + def build_join_association(reflection, parent) + JoinAssociation.new(reflection, self, parent) + end + + def construct(parent, associations, join_parts, row) + case associations + when Symbol, String + name = associations.to_s + + join_part = join_parts.detect { |j| + j.reflection.name.to_s == name && + j.parent_table_name == parent.class.table_name } + + raise(ConfigurationError, "No such association") unless join_part + + join_parts.delete(join_part) + construct_association(parent, join_part, row) + when Array + associations.each do |association| + construct(parent, association, join_parts, row) + end + when Hash + associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc| + association = construct(parent, association_name, join_parts, row) + construct(association, assoc, join_parts, row) if association + end + else + raise ConfigurationError, associations.inspect + end + end + + def construct_association(record, join_part, row) + return if record.id.to_s != join_part.parent.record_id(row).to_s + + macro = join_part.reflection.macro + if macro == :has_one + return if record.association_cache.key?(join_part.reflection.name) + association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? + set_target_and_inverse(join_part, association, record) + else + return if row[join_part.aliased_primary_key].nil? + association = join_part.instantiate(row) + case macro + when :has_many, :has_and_belongs_to_many + other = record.association(join_part.reflection.name) + other.loaded! + other.target.push(association) + other.set_inverse_instance(association) + when :belongs_to + set_target_and_inverse(join_part, association, record) + else + raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + end + end + association + end + + def set_target_and_inverse(join_part, association, record) + other = record.association(join_part.reflection.name) + other.target = association + other.set_inverse_instance(association) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb new file mode 100644 index 0000000000..4121a5b378 --- /dev/null +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -0,0 +1,144 @@ +module ActiveRecord + module Associations + class JoinDependency # :nodoc: + class JoinAssociation < JoinPart # :nodoc: + include JoinHelper + + # The reflection of the association represented + attr_reader :reflection + + # The JoinDependency object which this JoinAssociation exists within. This is mainly + # relevant for generating aliases which do not conflict with other joins which are + # part of the query. + attr_reader :join_dependency + + # A JoinBase instance representing the active record we are joining onto. + # (So in Author.has_many :posts, the Author would be that base record.) + attr_reader :parent + + # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin + attr_accessor :join_type + + # These implement abstract methods from the superclass + attr_reader :aliased_prefix + + attr_reader :tables + + delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection + delegate :table, :table_name, :to => :parent, :prefix => :parent + delegate :alias_tracker, :to => :join_dependency + + alias :alias_suffix :parent_table_name + + def initialize(reflection, join_dependency, parent = nil) + reflection.check_validity! + + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) + end + + super(reflection.klass) + + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @join_type = Arel::InnerJoin + @aliased_prefix = "t#{ join_dependency.join_parts.size }" + @tables = construct_tables.reverse + end + + def ==(other) + other.class == self.class && + other.reflection == reflection && + other.parent == parent + end + + def find_parent_in(other_join_dependency) + other_join_dependency.join_parts.detect do |join_part| + parent == join_part + end + end + + def join_to(relation) + tables = @tables.dup + foreign_table = parent_table + + # The chain starts with the target table, but we want to end with it here (makes + # more sense in this context), so we reverse + chain.reverse.each_with_index do |reflection, i| + table = tables.shift + + case reflection.source_macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + when :has_and_belongs_to_many + # Join the join table first... + relation.from(join( + table, + table[reflection.foreign_key]. + eq(foreign_table[reflection.active_record_primary_key]) + )) + + foreign_table, table = table, tables.shift + + key = reflection.association_primary_key + foreign_key = reflection.association_foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + end + + constraint = table[key].eq(foreign_table[foreign_key]) + + if reflection.klass.finder_needs_type_condition? + constraint = table.create_and([ + constraint, + reflection.klass.send(:type_condition, table) + ]) + end + + relation.from(join(table, constraint)) + + unless conditions[i].empty? + relation.where(sanitize(conditions[i], table)) + end + + # The current table in this iteration becomes the foreign table in the next + foreign_table = table + end + + relation + end + + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) + end + + def table + tables.last + end + + def aliased_table_name + table.table_alias || table.name + end + + def conditions + @conditions ||= reflection.conditions.reverse + end + + private + + def interpolate(conditions) + if conditions.respond_to?(:to_proc) + instance_eval(&conditions) + else + conditions + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb new file mode 100644 index 0000000000..3920e84976 --- /dev/null +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module Associations + class JoinDependency # :nodoc: + class JoinBase < JoinPart # :nodoc: + def ==(other) + other.class == self.class && + other.active_record == active_record + end + + def aliased_prefix + "t0" + end + + def table + Arel::Table.new(table_name, arel_engine) + end + + def aliased_table_name + active_record.table_name + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb new file mode 100644 index 0000000000..3279e56e7d --- /dev/null +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -0,0 +1,78 @@ +module ActiveRecord + module Associations + class JoinDependency # :nodoc: + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # everything else is being joined onto. A JoinAssociation represents an association which + # is joining to the base. A JoinAssociation may result in more than one actual join + # operations (for example a has_and_belongs_to_many JoinAssociation would result in + # two; one for the join table and one for the target table). + class JoinPart # :nodoc: + # The Active Record class which this join part is associated 'about'; for a JoinBase + # this is the actual base model, for a JoinAssociation this is the target model of the + # association. + attr_reader :active_record + + delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record + + def initialize(active_record) + @active_record = active_record + @cached_record = {} + @column_names_with_alias = nil + end + + def aliased_table + Arel::Nodes::TableAlias.new aliased_table_name, table + end + + def ==(other) + raise NotImplementedError + end + + # An Arel::Table for the active_record + def table + raise NotImplementedError + end + + # The prefix to be used when aliasing columns in the active_record's table + def aliased_prefix + raise NotImplementedError + end + + # The alias for the active_record's table + def aliased_table_name + raise NotImplementedError + end + + # The alias for the primary key of the active_record's table + def aliased_primary_key + "#{aliased_prefix}_r0" + end + + # An array of [column_name, alias] pairs for the table + def column_names_with_alias + unless @column_names_with_alias + @column_names_with_alias = [] + + ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| + @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] + end + end + @column_names_with_alias + end + + def extract_record(row) + Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] + end + + def record_id(row) + row[aliased_primary_key] + end + + def instantiate(row) + @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb new file mode 100644 index 0000000000..eae546e76e --- /dev/null +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module Associations + # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope + module JoinHelper #:nodoc: + + def join_type + Arel::InnerJoin + end + + private + + def construct_tables + tables = [] + chain.each do |reflection| + tables << alias_tracker.aliased_table_for( + table_name_for(reflection), + table_alias_for(reflection, reflection != self.reflection) + ) + + if reflection.source_macro == :has_and_belongs_to_many + tables << alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) + end + end + tables + end + + def table_name_for(reflection) + reflection.table_name + end + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{alias_suffix}" + name << "_join" if join + name + end + + def join(table, constraint) + table.create_join(table, table.create_on(constraint), join_type) + end + + def sanitize(conditions, table) + conditions = conditions.map do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + condition + end + + conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb new file mode 100644 index 0000000000..fafed94ff2 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -0,0 +1,177 @@ +module ActiveRecord + module Associations + # Implements the details of eager loading of Active Record associations. + # + # Note that 'eager loading' and 'preloading' are actually the same thing. + # However, there are two different eager loading strategies. + # + # The first one is by using table joins. This was only strategy available + # prior to Rails 2.1. Suppose that you have an Author model with columns + # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using + # this strategy, Active Record would try to retrieve all data for an author + # and all of its books via a single query: + # + # SELECT * FROM authors + # LEFT OUTER JOIN books ON authors.id = books.id + # WHERE authors.name = 'Ken Akamatsu' + # + # However, this could result in many rows that contain redundant data. After + # having received the first row, we already have enough data to instantiate + # the Author object. In all subsequent rows, only the data for the joined + # 'books' table is useful; the joined 'authors' data is just redundant, and + # processing this redundant data takes memory and CPU time. The problem + # quickly becomes worse and worse as the level of eager loading increases + # (i.e. if Active Record is to eager load the associations' associations as + # well). + # + # The second strategy is to use multiple database queries, one for each + # level of association. Since Rails 2.1, this is the default strategy. In + # situations where a table join is necessary (e.g. when the +:conditions+ + # option references an association's column), it will fallback to the table + # join strategy. + class Preloader #:nodoc: + autoload :Association, 'active_record/associations/preloader/association' + autoload :SingularAssociation, 'active_record/associations/preloader/singular_association' + autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association' + autoload :ThroughAssociation, 'active_record/associations/preloader/through_association' + + autoload :HasMany, 'active_record/associations/preloader/has_many' + autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' + autoload :HasOne, 'active_record/associations/preloader/has_one' + autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' + autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' + autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' + + attr_reader :records, :associations, :options, :model + + # Eager loads the named associations for the given Active Record record(s). + # + # In this description, 'association name' shall refer to the name passed + # to an association creation method. For example, a model that specifies + # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association + # names +:author+ and +:buyers+. + # + # == Parameters + # +records+ is an array of ActiveRecord::Base. This array needs not be flat, + # i.e. +records+ itself may also contain arrays of records. In any case, + # +preload_associations+ will preload the all associations records by + # flattening +records+. + # + # +associations+ specifies one or more associations that you want to + # preload. It may be: + # - a Symbol or a String which specifies a single association name. For + # example, specifying +:books+ allows this method to preload all books + # for an Author. + # - an Array which specifies multiple association names. This array + # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt> + # allows this method to preload an author's avatar as well as all of his + # books. + # - a Hash which specifies multiple association names, as well as + # association names for the to-be-preloaded association objects. For + # example, specifying <tt>{ :author => :avatar }</tt> will preload a + # book's author, as well as that author's avatar. + # + # +:associations+ has the same format as the +:include+ option for + # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this: + # + # :books + # [ :books, :author ] + # { :author => :avatar } + # [ :books, { :author => :avatar } ] + # + # +options+ contains options that will be passed to ActiveRecord::Base#find + # (which is called under the hood for preloading records). But it is passed + # only one level deep in the +associations+ argument, i.e. it's not passed + # to the child associations when +associations+ is a Hash. + def initialize(records, associations, options = {}) + @records = Array.wrap(records).compact.uniq + @associations = Array.wrap(associations) + @options = options + end + + def run + unless records.empty? + associations.each { |association| preload(association) } + end + end + + private + + def preload(association) + case association + when Hash + preload_hash(association) + when String, Symbol + preload_one(association.to_sym) + else + raise ArgumentError, "#{association.inspect} was not recognised for preload" + end + end + + def preload_hash(association) + association.each do |parent, child| + Preloader.new(records, parent, options).run + Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run + end + end + + # Not all records have the same class, so group then preload group on the reflection + # itself so that if various subclass share the same association then we do not split + # them unnecessarily + # + # Additionally, polymorphic belongs_to associations can have multiple associated + # classes, depending on the polymorphic_type field. So we group by the classes as + # well. + def preload_one(association) + grouped_records(association).each do |reflection, klasses| + klasses.each do |klass, records| + preloader_for(reflection).new(klass, records, reflection, options).run + end + end + end + + def grouped_records(association) + Hash[ + records_by_reflection(association).map do |reflection, records| + [reflection, records.group_by { |record| association_klass(reflection, record) }] + end + ] + end + + def records_by_reflection(association) + records.group_by do |record| + reflection = record.class.reflections[association] + + unless reflection + raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \ + "perhaps you misspelled it?" + end + + reflection + end + end + + def association_klass(reflection, record) + if reflection.macro == :belongs_to && reflection.options[:polymorphic] + klass = record.send(reflection.foreign_type) + klass && klass.constantize + else + reflection.klass + end + end + + def preloader_for(reflection) + case reflection.macro + when :has_many + reflection.options[:through] ? HasManyThrough : HasMany + when :has_one + reflection.options[:through] ? HasOneThrough : HasOne + when :has_and_belongs_to_many + HasAndBelongsToMany + when :belongs_to + BelongsTo + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb new file mode 100644 index 0000000000..7256dd5288 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -0,0 +1,126 @@ +module ActiveRecord + module Associations + class Preloader + class Association #:nodoc: + attr_reader :owners, :reflection, :preload_options, :model, :klass + + def initialize(klass, owners, reflection, preload_options) + @klass = klass + @owners = owners + @reflection = reflection + @preload_options = preload_options || {} + @model = owners.first && owners.first.class + @scoped = nil + @owners_by_key = nil + end + + def run + unless owners.first.association(reflection.name).loaded? + preload + end + end + + def preload + raise NotImplementedError + end + + def scoped + @scoped ||= build_scope + end + + def records_for(ids) + scoped.where(association_key.in(ids)) + end + + def table + klass.arel_table + end + + # The name of the key on the associated records + def association_key_name + raise NotImplementedError + end + + # This is overridden by HABTM as the condition should be on the foreign_key column in + # the join table + def association_key + table[association_key_name] + end + + # The name of the key on the model which declares the association + def owner_key_name + raise NotImplementedError + end + + # We're converting to a string here because postgres will return the aliased association + # key in a habtm as a string (for whatever reason) + def owners_by_key + @owners_by_key ||= owners.group_by do |owner| + key = owner[owner_key_name] + key && key.to_s + end + end + + def options + reflection.options + end + + private + + def associated_records_by_owner + owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq + + if klass.nil? || owner_keys.empty? + records = [] + else + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size) + records = sliced.map { |slice| records_for(slice) }.flatten + end + + # Each record may have multiple owners, and vice-versa + records_by_owner = Hash[owners.map { |owner| [owner, []] }] + records.each do |record| + owner_key = record[association_key_name].to_s + + owners_by_key[owner_key].each do |owner| + records_by_owner[owner] << record + end + end + records_by_owner + end + + def build_scope + scope = klass.scoped + + scope = scope.where(process_conditions(options[:conditions])) + scope = scope.where(process_conditions(preload_options[:conditions])) + + scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) + scope = scope.includes(preload_options[:include] || options[:include]) + + if options[:as] + scope = scope.where( + klass.table_name => { + reflection.type => model.base_class.sti_name + } + ) + end + + scope + end + + def process_conditions(conditions) + if conditions.respond_to?(:to_proc) + conditions = klass.send(:instance_eval, &conditions) + end + + if conditions + klass.send(:sanitize_sql, conditions) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb new file mode 100644 index 0000000000..5091d4717a --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module Associations + class Preloader + class BelongsTo < SingularAssociation #:nodoc: + + def association_key_name + reflection.options[:primary_key] || klass && klass.primary_key + end + + def owner_key_name + reflection.foreign_key + end + + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb new file mode 100644 index 0000000000..c248aeaaf6 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module Associations + class Preloader + class CollectionAssociation < Association #:nodoc: + + private + + def build_scope + super.order(preload_options[:order] || options[:order]) + end + + def preload + associated_records_by_owner.each do |owner, records| + association = owner.association(reflection.name) + association.loaded! + association.target.concat(records) + records.each { |record| association.set_inverse_instance(record) } + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb new file mode 100644 index 0000000000..24be279449 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -0,0 +1,60 @@ +module ActiveRecord + module Associations + class Preloader + class HasAndBelongsToMany < CollectionAssociation #:nodoc: + attr_reader :join_table + + def initialize(klass, records, reflection, preload_options) + super + @join_table = Arel::Table.new(options[:join_table]).alias('t0') + end + + # Unlike the other associations, we want to get a raw array of rows so that we can + # access the aliased column on the join table + def records_for(ids) + scope = super + klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values) + end + + def owner_key_name + reflection.active_record_primary_key + end + + def association_key_name + 'ar_association_key_name' + end + + def association_key + join_table[reflection.foreign_key] + end + + private + + # Once we have used the join table column (in super), we manually instantiate the + # actual records, ensuring that we don't create more than one instances of the same + # record + def associated_records_by_owner + records = {} + super.each do |owner_key, rows| + rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) } + end + end + + def build_scope + super.joins(join).select(join_select) + end + + def join_select + association_key.as(Arel.sql(association_key_name)) + end + + def join + condition = table[reflection.association_primary_key].eq( + join_table[reflection.association_foreign_key]) + + table.create_join(join_table, table.create_on(condition)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb new file mode 100644 index 0000000000..3ea91a8c11 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/has_many.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module Associations + class Preloader + class HasMany < CollectionAssociation #:nodoc: + + def association_key_name + reflection.foreign_key + end + + def owner_key_name + reflection.active_record_primary_key + end + + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb new file mode 100644 index 0000000000..c6e9ede356 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Associations + class Preloader + class HasManyThrough < CollectionAssociation #:nodoc: + include ThroughAssociation + + def associated_records_by_owner + super.each do |owner, records| + records.uniq! if options[:uniq] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb new file mode 100644 index 0000000000..848448bb48 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Associations + class Preloader + class HasOne < SingularAssociation #:nodoc: + + def association_key_name + reflection.foreign_key + end + + def owner_key_name + reflection.active_record_primary_key + end + + private + + def build_scope + super.order(preload_options[:order] || options[:order]) + end + + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb new file mode 100644 index 0000000000..f063f85574 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/has_one_through.rb @@ -0,0 +1,9 @@ +module ActiveRecord + module Associations + class Preloader + class HasOneThrough < SingularAssociation #:nodoc: + include ThroughAssociation + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb new file mode 100644 index 0000000000..44e804d785 --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module Associations + class Preloader + class SingularAssociation < Association #:nodoc: + + private + + def preload + associated_records_by_owner.each do |owner, associated_records| + record = associated_records.first + + association = owner.association(reflection.name) + association.target = record + association.set_inverse_instance(record) + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb new file mode 100644 index 0000000000..ad6374d09a --- /dev/null +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Associations + class Preloader + module ThroughAssociation #:nodoc: + + def through_reflection + reflection.through_reflection + end + + def source_reflection + reflection.source_reflection + end + + def associated_records_by_owner + through_records = through_records_by_owner + + ActiveRecord::Associations::Preloader.new( + through_records.values.flatten, + source_reflection.name, options + ).run + + through_records.each do |owner, records| + records.map! { |r| r.send(source_reflection.name) }.flatten! + records.compact! + end + end + + private + + def through_records_by_owner + ActiveRecord::Associations::Preloader.new( + owners, through_reflection.name, + through_options + ).run + + Hash[owners.map do |owner| + through_records = Array.wrap(owner.send(through_reflection.name)) + + # Dont cache the association - we would only be caching a subset + if reflection.options[:source_type] && through_reflection.collection? + owner.association(through_reflection.name).reset + end + + [owner, through_records] + end] + end + + def through_options + through_options = {} + + if options[:source_type] + through_options[:conditions] = { reflection.foreign_type => options[:source_type] } + else + if options[:conditions] + through_options[:include] = options[:include] || options[:source] + through_options[:conditions] = options[:conditions] + end + + through_options[:order] = options[:order] + end + + through_options + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb new file mode 100644 index 0000000000..4edbe216be --- /dev/null +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -0,0 +1,55 @@ +module ActiveRecord + module Associations + class SingularAssociation < Association #:nodoc: + # Implements the reader method, e.g. foo.bar for Foo.has_one :bar + def reader(force_reload = false) + if force_reload + klass.uncached { reload } + elsif !loaded? || stale_target? + reload + end + + target + end + + # Implements the writer method, e.g. foo.items= for Foo.has_many :items + def writer(record) + replace(record) + end + + def create(attributes = {}) + new_record(:create, attributes) + end + + def create!(attributes = {}) + build(attributes).tap { |record| record.save! } + end + + def build(attributes = {}) + new_record(:build, attributes) + end + + private + + def find_target + scoped.first.tap { |record| set_inverse_instance(record) } + end + + # Implemented by subclasses + def replace(record) + raise NotImplementedError, "Subclasses must implement a replace(record) method" + end + + def set_new_record(record) + replace(record) + end + + def new_record(method, attributes) + attributes = scoped.scope_for_create.merge(attributes || {}) + record = reflection.send("#{method}_association", attributes) + set_new_record(record) + record + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb new file mode 100644 index 0000000000..e6ab628719 --- /dev/null +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -0,0 +1,80 @@ +module ActiveRecord + # = Active Record Through Association + module Associations + module ThroughAssociation #:nodoc: + + delegate :source_reflection, :through_reflection, :chain, :to => :reflection + + protected + + # We merge in these scopes for two reasons: + # + # 1. To get the default_scope conditions for any of the other reflections in the chain + # 2. To get the type conditions for any STI models in the chain + def target_scope + scope = super + chain[1..-1].each do |reflection| + scope = scope.merge(reflection.klass.scoped) + end + scope + end + + private + + # Construct attributes for :through pointing to owner and associate. This is used by the + # methods which create and delete records on the association. + # + # We only support indirectly modifying through associations which has a belongs_to source. + # This is the "has_many :tags, :through => :taggings" situation, where the join model + # typically has a belongs_to on both side. In other words, associations which could also + # be represented as has_and_belongs_to_many associations. + # + # We do not support creating/deleting records on the association where the source has + # some other type, because this opens up a whole can of worms, and in basically any + # situation it is more natural for the user to just create or modify their join records + # directly as required. + def construct_join_attributes(*records) + if source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end + + join_attributes = { + source_reflection.foreign_key => + records.map { |record| + record.send(source_reflection.association_primary_key) + } + } + + if options[:source_type] + join_attributes[source_reflection.foreign_type] = + records.map { |record| record.class.base_class.name } + end + + if records.count == 1 + Hash[join_attributes.map { |k, v| [k, v.first] }] + else + join_attributes + end + end + + # Note: this does not capture all cases, for example it would be crazy to try to + # properly support stale-checking for nested associations. + def stale_state + if through_reflection.macro == :belongs_to + owner[through_reflection.foreign_key].to_s + end + end + + def foreign_key_present? + through_reflection.macro == :belongs_to && + !owner[through_reflection.foreign_key].nil? + end + + def ensure_not_nested + if reflection.nested? + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb deleted file mode 100644 index cabb33c4a8..0000000000 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ /dev/null @@ -1,154 +0,0 @@ -module ActiveRecord - # = Active Record Through Association Scope - module Associations - module ThroughAssociationScope - - protected - - def construct_scope - { :create => construct_owner_attributes(@reflection), - :find => { :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly], - } } - end - - # Build SQL conditions from attributes, qualified by table name. - def construct_conditions - table_name = @reflection.through_reflection.quoted_table_name - conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value| - "#{table_name}.#{attr} = #{value}" - end - conditions << sql_conditions if sql_conditions - "(" + conditions.join(') AND (') + ")" - end - - # Associate attributes pointing to owner, quoted. - def construct_quoted_owner_attributes(reflection) - if as = reflection.options[:as] - { "#{as}_id" => owner_quoted_id, - "#{as}_type" => reflection.klass.quote_value( - @owner.class.base_class.name.to_s, - reflection.klass.columns_hash["#{as}_type"]) } - elsif reflection.macro == :belongs_to - { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) } - else - { reflection.primary_key_name => owner_quoted_id } - end - end - - def construct_from - @reflection.table_name - end - - def construct_select(custom_select = nil) - distinct = "DISTINCT " if @reflection.options[:uniq] - selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" - end - - def construct_joins(custom_joins = nil) - polymorphic_join = nil - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name - if @reflection.options[:source_type] - polymorphic_join = "AND %s.%s = %s" % [ - @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}", - @owner.class.quote_value(@reflection.options[:source_type]) - ] - end - else - reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - polymorphic_join = "AND %s.%s = %s" % [ - @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type", - @owner.class.quote_value(@reflection.through_reflection.klass.name) - ] - end - end - - "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ - @reflection.through_reflection.quoted_table_name, - @reflection.quoted_table_name, reflection_primary_key, - @reflection.through_reflection.quoted_table_name, source_primary_key, - polymorphic_join - ] - end - - # Construct attributes for associate pointing to owner. - def construct_owner_attributes(reflection) - if as = reflection.options[:as] - { "#{as}_id" => @owner.id, - "#{as}_type" => @owner.class.base_class.name.to_s } - else - { reflection.primary_key_name => @owner.id } - end - end - - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - - join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) - - if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s) - end - - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) - end - - join_attributes - end - - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) - end - end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions - end - end -end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 56e18eced0..5833c65893 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -10,7 +10,18 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods - super(columns_hash.keys) + return if attribute_methods_generated? + super(column_names) + @attribute_methods_generated = true + end + + def attribute_methods_generated? + @attribute_methods_generated ||= false + end + + def undefine_attribute_methods(*args) + super + @attribute_methods_generated = false end # Checks whether the method is defined in the model or any of its subclasses @@ -18,7 +29,11 @@ module ActiveRecord # method is defined by Active Record though. def instance_method_already_implemented?(method_name) method_name = method_name.to_s - @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set + index = ancestors.index(ActiveRecord::Base) || ancestors.length + @_defined_class_methods ||= ancestors.first(index).map { |m| + m.instance_methods(false) | m.private_instance_methods(false) + }.flatten.map {|m| m.to_s }.to_set + @@_defined_activerecord_methods ||= defined_activerecord_methods raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) @_defined_class_methods.include?(method_name) @@ -27,9 +42,8 @@ module ActiveRecord def defined_activerecord_methods active_record = ActiveRecord::Base super_klass = ActiveRecord::Base.superclass - methods = active_record.public_instance_methods - super_klass.public_instance_methods - methods += active_record.private_instance_methods - super_klass.private_instance_methods - methods += active_record.protected_instance_methods - super_klass.protected_instance_methods + methods = (active_record.instance_methods - super_klass.instance_methods) + + (active_record.private_instance_methods - super_klass.private_instance_methods) methods.map {|m| m.to_s }.to_set end end @@ -48,13 +62,13 @@ module ActiveRecord end def respond_to?(*args) - self.class.define_attribute_methods + self.class.define_attribute_methods unless self.class.attribute_methods_generated? super end protected def attribute_method?(attr_name) - attr_name == 'id' || attributes.include?(attr_name) + attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index a4e144f233..bde11d0494 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -13,21 +13,19 @@ module ActiveRecord # Returns a hash of attributes before typecasting and deserialization. def attributes_before_type_cast - self.attribute_names.inject({}) do |attrs, name| - attrs[name] = read_attribute_before_type_cast(name) - attrs - end + @attributes end private - # Handle *_before_type_cast for method_missing. - def attribute_before_type_cast(attribute_name) - if attribute_name == 'id' - read_attribute_before_type_cast(self.class.primary_key) - else - read_attribute_before_type_cast(attribute_name) - end + + # Handle *_before_type_cast for method_missing. + def attribute_before_type_cast(attribute_name) + if attribute_name == 'id' + read_attribute_before_type_cast(self.class.primary_key) + else + read_attribute_before_type_cast(attribute_name) end + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index cf4594ad7f..3eff3d54e3 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' module ActiveRecord @@ -12,7 +13,7 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - superclass_delegating_accessor :partial_updates + class_attribute :partial_updates self.partial_updates = true end @@ -21,6 +22,8 @@ module ActiveRecord if status = super @previously_changed = changes @changed_attributes.clear + elsif IdentityMap.enabled? + IdentityMap.remove(self) end status end @@ -31,6 +34,9 @@ module ActiveRecord @previously_changed = changes @changed_attributes.clear end + rescue + IdentityMap.remove(self) if IdentityMap.enabled? + raise end # <tt>reload</tt> the record and clears changed attributes. @@ -88,7 +94,7 @@ module ActiveRecord end def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym) + old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym) end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 82d94b848a..fcdd31ddea 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -3,36 +3,48 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array - # or nil if the record is a new_record? + # Returns this record's primary key value wrapped in an Array or nil if + # the record is not persisted? or has just been destroyed. def to_key - new_record? ? nil : [ id ] + key = send(self.class.primary_key) + [key] if key end module ClassMethods # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the # primary_key_prefix_type setting, though. def primary_key - reset_primary_key + @primary_key ||= reset_primary_key end def reset_primary_key #:nodoc: - key = get_primary_key(base_class.name) + key = self == base_class ? get_primary_key(base_class.name) : + base_class.primary_key + set_primary_key(key) key end def get_primary_key(base_name) #:nodoc: - key = 'id' + return 'id' unless base_name && !base_name.blank? + case primary_key_prefix_type - when :table_name - key = base_name.to_s.foreign_key(false) - when :table_name_with_underscore - key = base_name.to_s.foreign_key + when :table_name + base_name.foreign_key(false) + when :table_name_with_underscore + base_name.foreign_key + else + if ActiveRecord::Base != self && connection.table_exists?(table_name) + connection.primary_key(table_name) + else + 'id' + end end - key end + attr_accessor :original_primary_key + attr_writer :primary_key + # Sets the name of the primary key column to use to the given value, # or (if the value is nil or false) to the value returned by the given # block. @@ -41,9 +53,12 @@ module ActiveRecord # set_primary_key "sysid" # end def set_primary_key(value = nil, &block) - define_attr_method :primary_key, value, &block + @primary_key ||= '' + self.original_primary_key = @primary_key + value &&= value.to_s + connection_pool.primary_keys[table_name] = value + self.primary_key = block_given? ? instance_eval(&block) : value end - alias :primary_key= :set_primary_key end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 3da3d9d8cc..69d5cd83f1 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -20,14 +20,13 @@ module ActiveRecord # be cached. Usually caching only pays off for attributes with expensive conversion # methods, like time related columns (e.g. +created_at+, +updated_at+). def cache_attributes(*attribute_names) - attribute_names.each {|attr| cached_attributes << attr.to_s} + cached_attributes.merge attribute_names.map { |attr| attr.to_s } end # Returns the attributes which are cached. By default time related columns # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. def cached_attributes - @cached_attributes ||= - columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set + @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set end # Returns +true+ if the provided attribute is being cached. @@ -37,10 +36,10 @@ module ActiveRecord protected def define_method_attribute(attr_name) - if self.serialized_attributes[attr_name] + if serialized_attributes.include?(attr_name) define_read_method_for_serialized_attribute(attr_name) else - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + define_read_method(attr_name, attr_name, columns_hash[attr_name]) end if attr_name == primary_key && attr_name != "id" @@ -49,33 +48,52 @@ module ActiveRecord end private + def cacheable_column?(column) + serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type) + end + # Define read method for serialized attribute. def define_read_method_for_serialized_attribute(attr_name) - generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) + access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']" + generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) end # Define an attribute reader method. Cope with nil column. def define_read_method(symbol, attr_name, column) - cast_code = column.type_cast_code('v') if column - access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" + cast_code = column.type_cast_code('v') + access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") end if cache_attribute?(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" end - generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__) + if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/ + generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) + else + generated_attribute_methods.module_eval do + define_method("_#{symbol}") { eval(access_code) } + alias_method(symbol, "_#{symbol}") + end + end end end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) + send "_#{attr_name}" + rescue NoMethodError + _read_attribute attr_name + end + + def _read_attribute(attr_name) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' - if !(value = @attributes[attr_name]).nil? + value = @attributes[attr_name] + unless value.nil? if column = column_for_attribute(attr_name) if unserializable_attribute?(attr_name, column) unserialize_attribute(attr_name) @@ -85,26 +103,20 @@ module ActiveRecord else value end - else - nil end end # Returns true if the attribute is of a text column and marked for serialization. def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes[attr_name] + column.text? && self.class.serialized_attributes.include?(attr_name) end # Returns the unserialized object of the attribute. def unserialize_attribute(attr_name) - unserialized_object = object_from_yaml(@attributes[attr_name]) + coder = self.class.serialized_attributes[attr_name] + unserialized_object = coder.load(@attributes[attr_name]) - if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - else - raise SerializationTypeMismatch, - "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" - end + @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object end private diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 8f0aacba42..6aac96df6f 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -7,24 +9,25 @@ module ActiveRecord cattr_accessor :time_zone_aware_attributes, :instance_writer => false self.time_zone_aware_attributes = false - class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false + class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods protected # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time + # This enhanced read method automatically converts the UTC time stored in the database to the time # zone stored in Time.zone. def define_method_attribute(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}(reload = false) + def _#{attr_name} cached = @attributes_cache['#{attr_name}'] - return cached if cached && !reload - time = read_attribute('#{attr_name}') + return cached if cached + time = _read_attribute('#{attr_name}') @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time end + alias #{attr_name} _#{attr_name} EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) else @@ -37,12 +40,14 @@ module ActiveRecord def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(time) + def #{attr_name}=(original_time) + time = original_time unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, time) + write_attribute(:#{attr_name}, original_time) + @attributes_cache["#{attr_name}"] = time end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -53,7 +58,7 @@ module ActiveRecord private def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 7a2de3bf80..3c4dab304e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -10,11 +10,17 @@ module ActiveRecord module ClassMethods protected def define_method_attribute=(attr_name) - generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) + if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/ + generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) + else + generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| + write_attribute(attr_name, new_value) + end + end end end - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings # for fixnum and float columns are turned into +nil+. def write_attribute(attr_name, value) attr_name = attr_name.to_s diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 2c7afe3c9f..48dbe0838a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -2,14 +2,14 @@ require 'active_support/core_ext/array/wrap' module ActiveRecord # = Active Record Autosave Association - # - # AutosaveAssociation is a module that takes care of automatically saving - # associacted records when parent is saved. In addition to saving, it + # + # +AutosaveAssociation+ is a module that takes care of automatically saving + # associated records when their parent is saved. In addition to saving, it # also destroys any associated records that were marked for destruction. - # (See mark_for_destruction and marked_for_destruction?) + # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>). # # Saving of the parent, its associations, and the destruction of marked - # associations, all happen inside 1 transaction. This should never leave the + # associations, all happen inside a transaction. This should never leave the # database in an inconsistent state. # # If validations for any of the associations fail, their error messages will @@ -18,6 +18,9 @@ module ActiveRecord # Note that it also means that associations marked for destruction won't # be destroyed directly. They will however still be marked for destruction. # + # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>. + # When the <tt>:autosave</tt> option is not present new associations are saved. + # # === One-to-one Example # # class Post @@ -28,7 +31,7 @@ module ActiveRecord # automatically _and_ atomically: # # post = Post.find(1) - # post.title # => "The current global position of migrating ducks" + # post.title # => "The current global position of migrating ducks" # post.author.name # => "alloy" # # post.title = "On the migration of ducks" @@ -36,7 +39,7 @@ module ActiveRecord # # post.save # post.reload - # post.title # => "On the migration of ducks" + # post.title # => "On the migration of ducks" # post.author.name # => "Eloy Duran" # # Destroying an associated model, as part of the parent's save action, is as @@ -46,6 +49,7 @@ module ActiveRecord # post.author.marked_for_destruction? # => true # # Note that the model is _not_ yet removed from the database: + # # id = post.author.id # Author.find_by_id(id).nil? # => false # @@ -53,40 +57,49 @@ module ActiveRecord # post.reload.author # => nil # # Now it _is_ removed from the database: + # # Author.find_by_id(id).nil? # => true # # === One-to-many Example # - # Consider a Post model with many Comments: + # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved: # # class Post - # has_many :comments, :autosave => true + # has_many :comments # :autosave option is no declared # end # - # Saving changes to the parent and its associated model can now be performed - # automatically _and_ atomically: + # post = Post.new(:title => 'ruby rocks') + # post.comments.build(:body => 'hello world') + # post.save # => saves both post and comment # - # post = Post.find(1) - # post.title # => "The current global position of migrating ducks" - # post.comments.first.body # => "Wow, awesome info thanks!" - # post.comments.last.body # => "Actually, your article should be named differently." + # post = Post.create(:title => 'ruby rocks') + # post.comments.build(:body => 'hello world') + # post.save # => saves both post and comment # - # post.title = "On the migration of ducks" - # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks." + # post = Post.create(:title => 'ruby rocks') + # post.comments.create(:body => 'hello world') + # post.save # => saves both post and comment # - # post.save - # post.reload - # post.title # => "On the migration of ducks" - # post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks." + # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records: # - # Destroying one of the associated models members, as part of the parent's - # save action, is as simple as marking it for destruction: + # class Post + # has_many :comments, :autosave => true + # end + # + # post = Post.create(:title => 'ruby rocks') + # post.comments.create(:body => 'hello world') + # post.comments[0].body = 'hi everyone' + # post.save # => saves both post and comment, with 'hi everyone' as body + # + # Destroying one of the associated models as part of the parent's save action + # is as simple as marking it for destruction: # # post.comments.last.mark_for_destruction # post.comments.last.marked_for_destruction? # => true # post.comments.length # => 2 # # Note that the model is _not_ yet removed from the database: + # # id = post.comments.last.id # Comment.find_by_id(id).nil? # => false # @@ -94,62 +107,53 @@ module ActiveRecord # post.reload.comments.length # => 1 # # Now it _is_ removed from the database: + # # Comment.find_by_id(id).nil? # => true # # === Validation # - # Validation is performed on the parent as usual, but also on all autosave - # enabled associations. If any of the associations fail validation, its - # error messages will be applied on the parents errors object and validation - # of the parent will fail. - # - # Consider a Post model with Author which validates the presence of its name - # attribute: - # - # class Post - # has_one :author, :autosave => true - # end - # - # class Author - # validates_presence_of :name - # end - # - # post = Post.find(1) - # post.author.name = '' - # post.save # => false - # post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author.name"=>["can't be blank"]}, @base=#<Post ...>> - # - # No validations will be performed on the associated models when validations - # are skipped for the parent: - # - # post = Post.find(1) - # post.author.name = '' - # post.save(:validate => false) # => true + # Children records are validated unless <tt>:validate</tt> is +false+. module AutosaveAssociation extend ActiveSupport::Concern - ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } + ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany } + + module AssociationBuilderExtension #:nodoc: + def self.included(base) + base.valid_options << :autosave + end + + def build + reflection = super + model.send(:add_autosave_association_callbacks, reflection) + reflection + end + end included do ASSOCIATION_TYPES.each do |type| - send("valid_keys_for_#{type}_association") << :autosave + Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension) end end module ClassMethods private - # def belongs_to(name, options = {}) - # super - # add_autosave_association_callbacks(reflect_on_association(name)) - # end - ASSOCIATION_TYPES.each do |type| - module_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{type}(name, options = {}) - super - add_autosave_association_callbacks(reflect_on_association(name)) + def define_non_cyclic_method(name, reflection, &block) + define_method(name) do |*args| + result = true; @_already_called ||= {} + # Loop prevention for validation of associations + unless @_already_called[[name, reflection.name]] + begin + @_already_called[[name, reflection.name]]=true + result = instance_eval(&block) + ensure + @_already_called[[name, reflection.name]]=false + end end - CODE + + result + end end # Adds validation and save callbacks for the association as specified by @@ -172,16 +176,25 @@ module ActiveRecord if collection before_save :before_save_collection_association - define_method(save_method) { save_collection_association(reflection) } + define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) } # Doesn't use after_save as that would save associations added in after_create/after_update twice after_create save_method after_update save_method else if reflection.macro == :has_one define_method(save_method) { save_has_one_association(reflection) } - after_save save_method + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method else - define_method(save_method) { save_belongs_to_association(reflection) } + define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } before_save save_method end end @@ -189,7 +202,7 @@ module ActiveRecord if reflection.validate? && !method_defined?(validation_method) method = (collection ? :validate_collection_association : :validate_single_association) - define_method(validation_method) { send(method, reflection) } + define_non_cyclic_method(validation_method, reflection) { send(method, reflection) } validate validation_method end end @@ -202,7 +215,7 @@ module ActiveRecord end # Marks this record to be destroyed as part of the parents save transaction. - # This does _not_ actually destroy the record instantly, rather child record will be destroyed + # This does _not_ actually destroy the record instantly, rather child record will be destroyed # when <tt>parent.save</tt> is called. # # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. @@ -222,7 +235,7 @@ module ActiveRecord def changed_for_autosave? new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? end - + private # Returns the record for an association collection that should be validated @@ -230,7 +243,7 @@ module ActiveRecord # unless the parent is/was a new record itself. def associated_records_to_validate_or_save(association, new_record, autosave) if new_record - association + association && association.target elsif autosave association.target.find_all { |record| record.changed_for_autosave? } else @@ -243,16 +256,16 @@ module ActiveRecord def nested_records_changed_for_autosave? self.class.reflect_on_all_autosave_associations.any? do |reflection| association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any?(&:changed_for_autosave?) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } end end - + # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is # turned on for the association. def validate_single_association(reflection) - if (association = association_instance_get(reflection.name)) && !association.target.nil? - association_valid?(reflection, association) - end + association = association_instance_get(reflection.name) + record = association && association.target + association_valid?(reflection, record) if record end # Validate the associated records if <tt>:validate</tt> or @@ -269,12 +282,12 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, association) - return true if association.destroyed? || association.marked_for_destruction? + def association_valid?(reflection, record) + return true if record.destroyed? || record.marked_for_destruction? - unless valid = association.valid? + unless valid = record.valid? if reflection.options[:autosave] - association.errors.each do |attribute, message| + record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" errors[attribute] << message errors[attribute].uniq! @@ -306,27 +319,35 @@ module ActiveRecord autosave = reflection.options[:autosave] if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + begin records.each do |record| next if record.destroyed? + saved = true + if autosave && record.marked_for_destruction? - association.destroy(record) + association.proxy.destroy(record) elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave - saved = association.send(:insert_record, record, false, false) + saved = association.insert_record(record, false) else - association.send(:insert_record, record) + association.insert_record(record) end elsif autosave saved = record.save(:validate => false) end - raise ActiveRecord::Rollback if saved == false + raise ActiveRecord::Rollback unless saved end + rescue + records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled? + raise + end + end - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) + # reconstruct the scope now that we know the owner's id + association.send(:construct_scope) if association.respond_to?(:construct_scope) end end @@ -339,16 +360,18 @@ module ActiveRecord # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. def save_has_one_association(reflection) - if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed? + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] - if autosave && association.marked_for_destruction? - association.destroy + if autosave && record.marked_for_destruction? + record.destroy else key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) - association[reflection.primary_key_name] = key - saved = association.save(:validate => !autosave) + if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave) + record[reflection.foreign_key] = key + saved = record.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved end @@ -360,17 +383,20 @@ module ActiveRecord # # In addition, it will destroy the association if it was marked for destruction. def save_belongs_to_association(reflection) - if (association = association_instance_get(reflection.name)) && !association.destroyed? + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] - if autosave && association.marked_for_destruction? - association.destroy + if autosave && record.marked_for_destruction? + record.destroy elsif autosave != false - saved = association.save(:validate => !autosave) if association.new_record? || autosave + saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) if association.updated? - association_id = association.send(reflection.options[:primary_key] || :id) - self[reflection.primary_key_name] = association_id + association_id = record.send(reflection.options[:primary_key] || :id) + self[reflection.foreign_key] = association_id + association.loaded! end saved if autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 8da4fbcba7..b778b0c0f0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,3 +1,8 @@ +begin + require 'psych' +rescue LoadError +end + require 'yaml' require 'set' require 'active_support/benchmarkable' @@ -7,7 +12,7 @@ require 'active_support/time' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/indifferent_access' @@ -15,7 +20,6 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/blank' @@ -26,18 +30,18 @@ require 'active_record/log_subscriber' module ActiveRecord #:nodoc: # = Active Record # - # Active Record objects don't specify their attributes directly, but rather infer them from - # the table definition with which they're linked. Adding, removing, and changing attributes - # and their type is done directly in the database. Any change is instantly reflected in the + # Active Record objects don't specify their attributes directly, but rather infer them from + # the table definition with which they're linked. Adding, removing, and changing attributes + # and their type is done directly in the database. Any change is instantly reflected in the # Active Record objects. The mapping that binds a given Active Record class to a certain # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones. # - # See the mapping rules in table_name and the full example in link:files/README.html for more insight. + # See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight. # # == Creation # - # Active Records accept constructor parameters either in a hash or as a block. The hash - # method is especially useful when you're receiving the data from somewhere else, like an + # Active Records accept constructor parameters either in a hash or as a block. The hash + # method is especially useful when you're receiving the data from somewhere else, like an # HTTP request. It works like this: # # user = User.new(:name => "David", :occupation => "Code Artist") @@ -77,16 +81,16 @@ module ActiveRecord #:nodoc: # end # end # - # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query - # and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+ + # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query + # and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+ # parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and - # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ - # before inserting them in the query, which will ensure that an attacker can't escape the + # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ + # before inserting them in the query, which will ensure that an attacker can't escape the # query and fake the login (or worse). # - # When using multiple parameters in the conditions, it can easily become hard to read exactly - # what the fourth or fifth question mark is supposed to represent. In those cases, you can - # resort to named bind variables instead. That's done by replacing the question marks with + # When using multiple parameters in the conditions, it can easily become hard to read exactly + # what the fourth or fifth question mark is supposed to represent. In those cases, you can + # resort to named bind variables instead. That's done by replacing the question marks with # symbols and supplying a hash with values for the matching symbol keys: # # Company.where( @@ -108,7 +112,7 @@ module ActiveRecord #:nodoc: # # Student.where(:grade => [9,11,12]) # - # When joining tables, nested hashes or keys written in the form 'table_name.column_name' + # When joining tables, nested hashes or keys written in the form 'table_name.column_name' # can be used to qualify the table name of a particular condition. For instance: # # Student.joins(:schools).where(:schools => { :type => 'public' }) @@ -116,10 +120,10 @@ module ActiveRecord #:nodoc: # # == Overwriting default accessors # - # All column values are automatically available through basic accessors on the Active Record - # object, but sometimes you want to specialize this behavior. This can be done by overwriting - # the default accessors (using the same name as the attribute) and calling - # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually + # All column values are automatically available through basic accessors on the Active Record + # object, but sometimes you want to specialize this behavior. This can be done by overwriting + # the default accessors (using the same name as the attribute) and calling + # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually # change things. # # class Song < ActiveRecord::Base @@ -134,7 +138,7 @@ module ActiveRecord #:nodoc: # end # end # - # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> + # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. # # == Attribute query methods @@ -153,43 +157,40 @@ module ActiveRecord #:nodoc: # # == Accessing attributes before they have been typecasted # - # Sometimes you want to be able to read the raw attribute data without having the column-determined - # typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt> - # accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute, + # Sometimes you want to be able to read the raw attribute data without having the column-determined + # typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt> + # accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute, # you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>. # - # This is especially useful in validation situations where the user might supply a string for an - # integer field and you want to display the original string back in an error message. Accessing the + # This is especially useful in validation situations where the user might supply a string for an + # integer field and you want to display the original string back in an error message. Accessing the # attribute normally would typecast the string to 0, which isn't what you want. # # == 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>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces 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>. Instead of writing + # 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>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces 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>. Instead of writing # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. - # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do + # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do # <tt>Person.find_all_by_last_name(last_name)</tt>. # # It's also possible to use multiple attributes in the same find by separating them with "_and_". - # + # # Person.where(:user_name => user_name, :password => password).first - # Person.find_by_user_name_and_password #with dynamic finder - # - # Person.where(:user_name => user_name, :password => password, :gender => 'male').first - # Payment.find_by_user_name_and_password_and_gender + # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder # # It's even possible to call these dynamic finder methods on relations and named scopes. # # Payment.order("created_on").find_all_by_amount(50) # Payment.pending.find_last_by_amount(100) # - # 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. + # 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. # # # No 'Summer' tag exists # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") @@ -200,33 +201,33 @@ module ActiveRecord #:nodoc: # # Now 'Bob' exist and is an 'admin' # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } # - # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without + # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without # saving it first. Protected attributes won't be set unless they are given in a block. # # # No 'Winter' tag exists # winter = Tag.find_or_initialize_by_name("Winter") - # winter.new_record? # true + # winter.persisted? # false # # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of # a list of parameters. # # Tag.find_or_create_by_name(:name => "rails", :creator => current_user) # - # That will either find an existing tag named "rails", or create a new one while setting the + # That will either find an existing tag named "rails", or create a new one while setting the # user that created it. # # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit. # - # User.scoped_by_user_name('David') + # User.scoped_by_user_name('David') # # == Saving arrays, hashes, and other non-mappable objects in text columns # - # Active Record can serialize any object in text columns using YAML. To do so, you must + # Active Record can serialize any object in text columns using YAML. To do so, you must # specify this with a call to the class method +serialize+. - # This makes it possible to store arrays, hashes, and other non-mappable objects without doing - # any additional work. + # This makes it possible to store arrays, hashes, and other non-mappable objects without doing + # any additional work. # # class User < ActiveRecord::Base # serialize :preferences @@ -235,7 +236,7 @@ module ActiveRecord #:nodoc: # user = User.create(:preferences => { "background" => "black", "display" => large }) # User.find(user.id).preferences # => { "background" => "black", "display" => large } # - # You can also specify a class option as the second parameter that'll raise an exception + # You can also specify a class option as the second parameter that'll raise an exception # if a serialized object is retrieved as a descendant of a class not in the hierarchy. # # class User < ActiveRecord::Base @@ -245,10 +246,21 @@ module ActiveRecord #:nodoc: # user = User.create(:preferences => %w( one two three )) # User.find(user.id).preferences # raises SerializationTypeMismatch # + # When you specify a class option, the default value for that attribute will be a new + # instance of that class. + # + # class User < ActiveRecord::Base + # serialize :preferences, OpenStruct + # end + # + # user = User.new + # user.preferences.theme_color = "red" + # + # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). # This means that an inheritance looking like this: # # class Company < ActiveRecord::Base; end @@ -256,12 +268,12 @@ module ActiveRecord #:nodoc: # class Client < Company; end # class PriorityClient < Client; end # - # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using + # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using # <tt>Company.where(:name => '37signals').first</tt> and it will return a Firm object. # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic # for differentiating between them or reloading the right type with find. # # Note, all the attributes for all the cases are kept in the same table. Read more: @@ -269,14 +281,14 @@ module ActiveRecord #:nodoc: # # == Connection to multiple databases in different models # - # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved - # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this - # connection. But you can also set a class-specific connection. For example, if Course is an + # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved + # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this + # connection. But you can also set a class-specific connection. For example, if Course is an # ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt> # and Course and all of its subclasses will use this connection instead. # - # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is - # a Hash indexed by the class. If a connection is requested, the retrieve_connection method + # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is + # a Hash indexed by the class. If a connection is requested, the retrieve_connection method # will go up the class-hierarchy until a connection is found in the connection pool. # # == Exceptions @@ -284,25 +296,25 @@ module ActiveRecord #:nodoc: # * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record. # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an # <tt>:adapter</tt> key. - # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a + # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a # non-existent adapter # (or a bad spelling of an existing one). - # * AssociationTypeMismatch - The object assigned to the association wasn't of the type + # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. - # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> + # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> # before querying. # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal # nothing was found, please check its documentation for further details. # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the - # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of + # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * AttributeAssignmentError - An error occurred while doing a mass assignment through the + # * AttributeAssignmentError - An error occurred while doing a mass assignment through the # <tt>attributes=</tt> method. - # You can inspect the +attribute+ property of the exception object to determine which attribute + # You can inspect the +attribute+ property of the exception object to determine which attribute # triggered the error. # # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). @@ -311,23 +323,11 @@ module ActiveRecord #:nodoc: class Base ## # :singleton-method: - # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, - # which is then passed on to any new database connections made and which can be retrieved on both + # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, + # which is then passed on to any new database connections made and which can be retrieved on both # a class and instance level by calling +logger+. cattr_accessor :logger, :instance_writer => false - class << self - def reset_subclasses #:nodoc: - ActiveSupport::Deprecation.warn 'ActiveRecord::Base.reset_subclasses no longer does anything in Rails 3. It will be removed in the final release; please update your apps and plugins.', caller - end - - def subclasses - descendants - end - - deprecate :subclasses => :descendants - end - ## # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - @@ -360,9 +360,9 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the + # Accessor for the prefix type that will be prepended to every primary key column name. + # The options are :table_name and :table_name_with_underscore. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the # latter is specified, the Product class will look for "product_id" instead of "id". Remember # that this is a global setting for all Active Records. cattr_accessor :primary_key_prefix_type, :instance_writer => false @@ -370,13 +370,13 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set - # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", - # etc. This is a convenient way of creating a namespace for tables in a shared database. + # Accessor for the name of the prefix string to prepend to every table name. So if set + # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", + # etc. This is a convenient way of creating a namespace for tables in a shared database. # By default, the prefix is the empty string. # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which # returns your chosen prefix. class_attribute :table_name_prefix, :instance_writer => false self.table_name_prefix = "" @@ -398,7 +398,7 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling + # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling # dates and times from the database. This is set to :local by default. cattr_accessor :default_timezone, :instance_writer => false @@default_timezone = :local @@ -421,24 +421,25 @@ module ActiveRecord #:nodoc: @@timestamped_migrations = true # Determine whether to store the full constant name including namespace when using STI - superclass_delegating_accessor :store_full_sti_class + class_attribute :store_full_sti_class self.store_full_sti_class = true # Stores the default scope for the class - class_inheritable_accessor :default_scoping, :instance_writer => false + class_attribute :default_scoping, :instance_writer => false self.default_scoping = [] - class << self # Class methods - def colorize_logging(*args) - ActiveSupport::Deprecation.warn "ActiveRecord::Base.colorize_logging and " << - "config.active_record.colorize_logging are deprecated. Please use " << - "Rails::LogSubscriber.colorize_logging or config.colorize_logging instead", caller - end - alias :colorize_logging= :colorize_logging + # Returns a hash of all the attributes that have been specified for serialization as + # keys and their class restriction as values. + class_attribute :serialized_attributes + self.serialized_attributes = {} + + class_attribute :_attr_readonly, :instance_writer => false + self._attr_readonly = [] + class << self # Class methods delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :reorder, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped + delegate :select, :group, :order, :except, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will @@ -462,9 +463,9 @@ module ActiveRecord #:nodoc: # # # You can use the same string replacement techniques as you can with ActiveRecord#find # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] - # > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...] - def find_by_sql(sql) - connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } + # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] + def find_by_sql(sql, binds = []) + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } end # Creates an object (or multiple objects) and saves it to the database, if validations pass. @@ -519,12 +520,12 @@ module ActiveRecord #:nodoc: # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || [])) + self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. def readonly_attributes - read_inheritable_attribute(:attr_readonly) || [] + self._attr_readonly end # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, @@ -539,24 +540,26 @@ module ActiveRecord #:nodoc: # # ==== Example # # Serialize a preferences attribute - # class User + # class User < ActiveRecord::Base # serialize :preferences # end def serialize(attr_name, class_name = Object) - serialized_attributes[attr_name.to_s] = class_name - end + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end - # Returns a hash of all the attributes that have been specified for serialization as - # keys and their class restriction as values. - def serialized_attributes - read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {}) + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) end - # Guesses the table name (in forced lower-case) based on the name of the class in the - # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy + # Guesses the table name (in forced lower-case) based on the name of the class in the + # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy # looks like: Reply < Message < ActiveRecord::Base, then Message is used - # to guess the table name even when called on Reply. The rules used to do the guess - # are handled by the Inflector class in Active Support, which knows almost all common + # to guess the table name even when called on Reply. The rules used to do the guess + # are handled by the Inflector class in Active Support, which knows almost all common # English inflections. You can add new inflections in config/initializers/inflections.rb. # # Nested classes are given table names prefixed by the singular form of @@ -605,10 +608,10 @@ module ActiveRecord #:nodoc: (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end - # Defines the column name for use with single table inheritance. Use + # Defines the column name for use with single table inheritance. Use # <tt>set_inheritance_column</tt> to set a different value. def inheritance_column - @inheritance_column ||= "type".freeze + @inheritance_column ||= "type" end # Lazy-set the sequence name to the connection's default. This method @@ -623,7 +626,7 @@ module ActiveRecord #:nodoc: default end - # Sets the table name. If the value is nil or false then the value returned by the given + # Sets the table name. If the value is nil or false then the value returned by the given # block is used. # # class Project < ActiveRecord::Base @@ -632,6 +635,9 @@ module ActiveRecord #:nodoc: def set_table_name(value = nil, &block) @quoted_table_name = nil define_attr_method :table_name, value, &block + + @arel_table = Arel::Table.new(table_name, arel_engine) + @relation = Relation.new(self, arel_table) end alias :table_name= :set_table_name @@ -675,16 +681,12 @@ module ActiveRecord #:nodoc: # Returns an array of column objects for the table associated with this class. def columns - unless defined?(@columns) && @columns - @columns = connection.columns(table_name, "#{name} Columns") - @columns.each { |column| column.primary = column.name == primary_key } - end - @columns + connection_pool.columns[table_name] end # Returns a hash of column objects for the table associated with this class. def columns_hash - @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash } + connection_pool.columns_hash[table_name] end # Returns an array of column names as strings. @@ -739,13 +741,16 @@ module ActiveRecord #:nodoc: # end # end def reset_column_information + connection.clear_cache! undefine_attribute_methods - @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = @arel_table = nil + connection_pool.clear_table_cache!(table_name) if table_exists? + + @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + @arel_engine = @relation = nil end - def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: - descendants.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } + def clear_cache! # :nodoc: + connection_pool.clear_cache! end def attribute_method?(attribute) @@ -756,15 +761,12 @@ module ActiveRecord #:nodoc: def lookup_ancestors #:nodoc: klass = self classes = [klass] + return classes if klass == ActiveRecord::Base + while klass != klass.base_class classes << klass = klass.superclass end classes - rescue - # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column' - # Apparently the method base_class causes some trouble. - # It now works for sure. - [self] end # Set the i18n scope to overwrite ActiveModel. @@ -786,7 +788,7 @@ module ActiveRecord #:nodoc: :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) end - # Returns a string like 'Post id:integer, title:string, body:text' + # Returns a string like 'Post(id:integer, title:string, body:text)' def inspect if self == Base super @@ -814,6 +816,10 @@ module ActiveRecord #:nodoc: object.is_a?(self) end + def symbolized_base_class + @symbolized_base_class ||= base_class.to_s.to_sym + end + # Returns the base AR subclass that this class descends from. If A # extends AR::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. @@ -847,15 +853,15 @@ module ActiveRecord #:nodoc: end def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) + Arel::Table.new(table_name, arel_engine) end def arel_engine @arel_engine ||= begin if self == ActiveRecord::Base - Arel::Table.engine + ActiveRecord::Base else - connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.arel_engine + connection_handler.connection_pools[name] ? self : superclass.arel_engine end end end @@ -876,7 +882,12 @@ module ActiveRecord #:nodoc: # limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # - def unscoped + # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt> + # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same. + # + # Post.unscoped.published + # Post.published + def unscoped #:nodoc: block_given? ? relation.scoping { yield } : relation end @@ -885,32 +896,55 @@ module ActiveRecord #:nodoc: Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup end - private + def before_remove_const #:nodoc: + reset_scoped_methods + end - def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) - finder_needs_type_condition? ? @relation.where(type_condition) : @relation + # Specifies how the record is loaded by +Marshal+. + # + # +_load+ sets an instance variable for each key in the hash it takes as input. + # Override this method if you require more complex marshalling. + def _load(data) + record = allocate + record.init_with(Marshal.load(data)) + record + end + + + # Finder methods must instantiate through this method to work with the + # single-table inheritance model that makes it possible to create + # objects of different types from the same table. + def instantiate(record) + sti_class = find_sti_class(record[inheritance_column]) + record_id = sti_class.primary_key && record[sti_class.primary_key] + + if ActiveRecord::IdentityMap.enabled? && record_id + if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? + record_id = record_id.to_i + end + if instance = IdentityMap.get(sti_class, record_id) + instance.reinit_with('attributes' => record) + else + instance = sti_class.allocate.init_with('attributes' => record) + IdentityMap.add(instance) + end + else + instance = sti_class.allocate.init_with('attributes' => record) end - # Finder methods must instantiate through this method to work with the - # single-table inheritance model that makes it possible to create - # objects of different types from the same table. - def instantiate(record) - object = find_sti_class(record[inheritance_column]).allocate + instance + end - object.instance_variable_set(:@attributes, record) - object.instance_variable_set(:@attributes_cache, {}) - object.instance_variable_set(:@new_record, false) - object.instance_variable_set(:@readonly, false) - object.instance_variable_set(:@destroyed, false) - object.instance_variable_set(:@marked_for_destruction, false) - object.instance_variable_set(:@previously_changed, {}) - object.instance_variable_set(:@changed_attributes, {}) + private - object.send(:_run_find_callbacks) - object.send(:_run_initialize_callbacks) + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) - object + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end end def find_sti_class(type_name) @@ -918,7 +952,11 @@ module ActiveRecord #:nodoc: self else begin - compute_type(type_name) + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end rescue NameError raise SubclassNotFound, "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + @@ -930,17 +968,16 @@ module ActiveRecord #:nodoc: end def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : unscoped.merge(options) + relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options relation = scope.merge(relation) if scope relation end - def type_condition - sti_column = arel_table[inheritance_column] - condition = sti_column.eq(sti_name) - descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } + def type_condition(table = arel_table) + sti_column = table[inheritance_column.to_sym] + sti_names = ([self] + descendants).map { |model| model.sti_name } - condition + sti_column.in(sti_names) end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -958,7 +995,7 @@ module ActiveRecord #:nodoc: if parent < ActiveRecord::Base && !parent.abstract_class? contained = parent.table_name contained = contained.singularize if parent.pluralize_table_names - contained << '_' + contained += '_' end "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" else @@ -967,14 +1004,14 @@ module ActiveRecord #:nodoc: end end - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and + # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders # section at the top of this file for more detailed information. # - # It's even possible to use all the additional parameters to +find+. For example, the + # It's even possible to use all the additional parameters to +find+. For example, the # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it + # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it # is first invoked, so that future attempts to use it do not run through method_missing. def method_missing(method_id, *arguments, &block) if match = DynamicFinderMatch.match(method_id) @@ -992,14 +1029,11 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) if match.scope? self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - options = args.extract_options! # options = args.extract_options! - attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments( - [:#{attribute_names.join(',:')}], args # [:user_name, :password], args - ) # ) - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end METHOD send(method_id, *arguments) end @@ -1008,30 +1042,22 @@ module ActiveRecord #:nodoc: end end - def construct_attributes_from_arguments(attribute_names, arguments) - attributes = {} - attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] } - attributes - end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. def expand_attribute_names_for_aggregates(attribute_names) - expanded_attribute_names = [] - attribute_names.each do |attribute_name| + attribute_names.map { |attribute_name| unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).each do |field_attr, aggregate_attr| - expanded_attribute_names << field_attr + aggregate_mapping(aggregation).map do |field_attr, _| + field_attr.to_sym end else - expanded_attribute_names << attribute_name + attribute_name.to_sym end - end - expanded_attribute_names + }.flatten end def all_attributes_exists?(attribute_names) - attribute_names = expand_attribute_names_for_aggregates(attribute_names) - attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } + (expand_attribute_names_for_aggregates(attribute_names) - + column_methods_hash.keys).empty? end protected @@ -1087,9 +1113,9 @@ module ActiveRecord #:nodoc: if method_scoping.is_a?(Hash) # Dup first and second level of hash (method and params). - method_scoping = method_scoping.inject({}) do |hash, (method, params)| - hash[method] = (params == true) ? params : params.dup - hash + method_scoping = method_scoping.dup + method_scoping.each do |method, params| + method_scoping[method] = params.dup unless params == true end method_scoping.assert_valid_keys([ :find, :create ]) @@ -1147,12 +1173,33 @@ MSG # class Person < ActiveRecord::Base # default_scope order('last_name, first_name') # end + # + # <tt>default_scope</tt> is also applied while creating/building a record. It is not + # applied while updating a record. + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # end + # + # Article.new.published # => true + # Article.create.published # => true def default_scope(options = {}) - self.default_scoping << construct_finder_arel(options, default_scoping.pop) + reset_scoped_methods + default_scoping = self.default_scoping.dup + self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop) end def current_scoped_methods #:nodoc: - scoped_methods.last + method = scoped_methods.last + if method.respond_to?(:call) + relation.scoping { method.call } + else + method + end + end + + def reset_scoped_methods #:nodoc: + Thread.current[:"#{self}_scoped_methods"] = nil end # Returns the class type of the record using the current module as a prefix. So descendants of @@ -1161,7 +1208,7 @@ MSG if type_name.match(/^::/) # If the type is prefixed with a scope operator then we assume that # the type_name is an absolute reference. - type_name.constantize + ActiveSupport::Dependencies.constantize(type_name) else # Build a list of candidates to search for candidates = [] @@ -1170,7 +1217,7 @@ MSG candidates.each do |candidate| begin - constant = candidate.constantize + constant = ActiveSupport::Dependencies.constantize(candidate) return constant if candidate == constant.to_s rescue NameError => e # We don't want to swallow NoMethodError < NameError errors @@ -1274,9 +1321,11 @@ MSG def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) - table = Arel::Table.new(self.table_name, :engine => arel_engine, :as => default_table_name) - builder = PredicateBuilder.new(arel_engine) - builder.build_from_hash(attrs, table).map(&:to_sql).join(' AND ') + table = Arel::Table.new(table_name).alias(default_table_name) + viz = Arel::Visitors.for(arel_engine) + PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| + viz.accept b + }.join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -1294,7 +1343,7 @@ MSG # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) statement, *values = ary - if values.first.is_a?(Hash) and statement =~ /:\w+/ + if values.first.is_a?(Hash) && statement =~ /:\w+/ replace_named_bind_variables(statement, values.first) elsif statement.include?('?') replace_bind_variables(statement, values) @@ -1375,6 +1424,8 @@ MSG # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) @attributes = attributes_from_column_definition + @association_cache = {} + @aggregation_cache = {} @attributes_cache = {} @new_record = true @readonly = false @@ -1384,43 +1435,66 @@ MSG @changed_attributes = {} ensure_proper_type + set_serialized_attributes - if scope = self.class.send(:current_scoped_methods) - create_with = scope.scope_for_create - create_with.each { |att,value| self.send("#{att}=", value) } if create_with - end + populate_with_current_scope_attributes self.attributes = attributes unless attributes.nil? result = yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize result end - # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone - # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is - # application specific and is therefore left to the application to implement according to its need. - def initialize_copy(other) - _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - cloned_attributes.delete(self.class.primary_key) + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end - @attributes = cloned_attributes + # Initialize an empty model object from +coder+. +coder+ must contain + # the attributes necessary for initializing an empty model object. For + # example: + # + # class Post < ActiveRecord::Base + # end + # + # post = Post.allocate + # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.title # => 'hello world' + def init_with(coder) + @attributes = coder['attributes'] - @changed_attributes = {} - attributes_from_column_definition.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) - end + set_serialized_attributes - clear_aggregation_cache - clear_association_cache - @attributes_cache = {} - @new_record = true - ensure_proper_type + @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} + @association_cache = {} + @aggregation_cache = {} + @readonly = @destroyed = @marked_for_destruction = false + @new_record = false + run_callbacks :find + run_callbacks :initialize - if scope = self.class.send(:current_scoped_methods) - create_with = scope.scope_for_create - create_with.each { |att,value| self.send("#{att}=", value) } if create_with - end + self + end + + # Specifies how the record is dumped by +Marshal+. + # + # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this + # method if you require more complex marshalling. + def _dump(level) + dump = {} + encode_with(dump) + Marshal.dump(dump) end # Returns a String, which Action Pack uses for constructing an URL to this @@ -1477,22 +1551,9 @@ MSG @attributes.has_key?(attr_name.to_s) end - # Returns an array of names for the attributes available on this object sorted alphabetically. + # Returns an array of names for the attributes available on this object. def attribute_names - @attributes.keys.sort - end - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - # (Alias for the protected read_attribute method). - def [](attr_name) - read_attribute(attr_name) - end - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). - def []=(attr_name, value) - write_attribute(attr_name, value) + @attributes.keys end # Allows you to set all the attributes at once by passing in a hash with keys @@ -1525,8 +1586,10 @@ MSG attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + send("#{k}=", v) else - respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") + raise(UnknownAttributeError, "unknown attribute: #{k}") end end @@ -1535,9 +1598,7 @@ MSG # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. def attributes - attrs = {} - attribute_names.each { |name| attrs[name] = read_attribute(name) } - attrs + Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -1568,8 +1629,7 @@ MSG # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) - value = read_attribute(attribute) - !value.blank? + !_read_attribute(attribute).blank? end # Returns the column object for the named attribute. @@ -1577,16 +1637,25 @@ MSG self.class.columns_hash[name.to_s] end - # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id. + # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ + # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. + # + # Note that new records are different from any other record by definition, unless the + # other record is the receiver itself. Besides, if you fetch existing records with + # +select+ and leave the ID out, you're on your own, this predicate will return false. + # + # Note also that destroying a record preserves its ID in the model instance, so deleted + # models are still comparable. def ==(comparison_object) comparison_object.equal?(self) || - (comparison_object.instance_of?(self.class) && - comparison_object.id == id && !comparison_object.new_record?) + comparison_object.instance_of?(self.class) && + id.present? && + comparison_object.id == id end # Delegates to == def eql?(comparison_object) - self == (comparison_object) + self == comparison_object end # Delegates to id in order to allow two records of the same type and id to work with something like: @@ -1605,11 +1674,42 @@ MSG @attributes.frozen? end - # Returns duplicated record with unfreezed attributes. - def dup - obj = super - obj.instance_variable_set('@attributes', @attributes.dup) - obj + # Backport dup from 1.9 so that initialize_dup() gets called + unless Object.respond_to?(:initialize_dup) + def dup # :nodoc: + copy = super + copy.initialize_dup(self) + copy + end + end + + # Duped objects have no id assigned and are treated as new records. Note + # that this is a "shallow" copy as it copies the object's attributes + # only, not its associations. The extent of a "deep" copy is application + # specific and is therefore left to the application to implement according + # to its need. + # The dup method does not preserve the timestamps (created|updated)_(at|on). + def initialize_dup(other) + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + cloned_attributes.delete(self.class.primary_key) + + @attributes = cloned_attributes + + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) + + @changed_attributes = {} + attributes_from_column_definition.each do |attr, orig_value| + @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + end + + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @new_record = true + + ensure_proper_type + populate_with_current_scope_attributes + clear_timestamp_attributes end # Returns +true+ if the record is read only. Records loaded through joins with piggy-back @@ -1626,7 +1726,7 @@ MSG # Returns the contents of the record as a nicely formatted string. def inspect attributes_as_nice_string = self.class.column_names.collect { |name| - if has_attribute?(name) || new_record? + if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" end }.compact.join(", ") @@ -1650,10 +1750,17 @@ MSG private - # Sets the attribute used for single table inheritance to this class name if this is not the + def set_serialized_attributes + (@attributes.keys & self.class.serialized_attributes.keys).each do |key| + coder = self.class.serialized_attributes[key] + @attributes[key] = coder.load @attributes[key] + end + end + + # Sets the attribute used for single table inheritance to this class name if this is not the # ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to - # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. # No such attribute would be set for objects of the Message class in that example. def ensure_proper_type unless self.class.descends_from_active_record? @@ -1671,17 +1778,25 @@ MSG # Returns a copy of the attributes hash where all the values have been safely quoted for use in # an Arel insert/update method. def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} + attrs = {} + klass = self.class + arel_table = klass.arel_table + attribute_names.each do |name| if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - value = read_attribute(name) - if value && ((self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))) || value.is_a?(Hash) || value.is_a?(Array)) - value = value.to_yaml - end - attrs[self.class.arel_table[name]] = value + value = if coder = klass.serialized_attributes[name] + coder.dump @attributes[name] + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already type + # casted, this code could be simplified + read_attribute(name) + end + + attrs[arel_table[name]] = value end end end @@ -1693,18 +1808,12 @@ MSG self.class.connection.quote(value, column) end - # Interpolate custom SQL string in instance context. - # Optional record argument is meant for custom insert_sql. - def interpolate_sql(sql, record = nil) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - # 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 (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, - # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, + # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the # attribute will be set to nil. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( @@ -1727,7 +1836,7 @@ MSG klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass # in order to allow a date to be set without a year, we must keep the empty values. # Otherwise, we wouldn't be able to distinguish it from a date with an empty day. - values = values_with_empty_parameters.reject(&:nil?) + values = values_with_empty_parameters.reject { |v| v.nil? } if values.empty? send(name + "=", nil) @@ -1786,10 +1895,7 @@ MSG end def quote_columns(quoter, hash) - hash.inject({}) do |quoted, (name, value)| - quoted[quoter.quote_column_name(name)] = value - quoted - end + Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }] end def quoted_comma_pair_list(quoter, hash) @@ -1808,9 +1914,21 @@ MSG end end - def object_from_yaml(string) - return string unless string.is_a?(String) && string =~ /^---/ - YAML::load(string) rescue string + def populate_with_current_scope_attributes + if scope = self.class.send(:current_scoped_methods) + create_with = scope.scope_for_create + create_with.each { |att,value| + respond_to?("#{att}=") && send("#{att}=", value) + } + end + end + + # Clear attributes and changed_attributes + def clear_timestamp_attributes + all_timestamp_attributes_in_model.each do |attribute_name| + self[attribute_name] = nil + changed_attributes.delete(attribute_name) + end end end @@ -1832,7 +1950,9 @@ MSG include AttributeMethods::Dirty include ActiveModel::MassAssignmentSecurity include Callbacks, ActiveModel::Observing, Timestamp - include Associations, AssociationPreload, NamedScope + include Associations, NamedScope + include IdentityMap + include ActiveModel::SecurePassword # AutosaveAssociation needs to be included before Transactions, because we want # #save_with_autosave_associations to be wrapped inside a transaction. @@ -1840,6 +1960,17 @@ MSG include Aggregations, Transactions, Reflection, Serialization NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) + + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # (Alias for the protected read_attribute method). + alias [] read_attribute + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. + # (Alias for the protected write_attribute method). + alias []= write_attribute + + public :[], :[]= end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index aa92bf999f..86d58df99b 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -2,8 +2,8 @@ require 'active_support/core_ext/array/wrap' module ActiveRecord # = Active Record Callbacks - # - # Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic + # + # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider @@ -25,9 +25,13 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # - # That's a total of ten callbacks, which gives you immense power to react and prepare for each state in the - # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, - # except that each <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback. + # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that + # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects + # are instantiated as well. + # + # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the + # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, + # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # # Examples: # class CreditCard < ActiveRecord::Base @@ -55,8 +59,8 @@ module ActiveRecord # # == Inheritable callback queues # - # Besides the overwritable callback methods, it's also possible to register callbacks through the - # use of the callback macros. Their main advantage is that the macros add behavior into a callback + # Besides the overwritable callback methods, it's also possible to register callbacks through the + # use of the callback macros. Their main advantage is that the macros add behavior into a callback # queue that is kept intact down through an inheritance hierarchy. # # class Topic < ActiveRecord::Base @@ -67,9 +71,9 @@ module ActiveRecord # before_destroy :destroy_readers # end # - # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is - # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation - # where the +before_destroy+ methis is overriden: + # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is + # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation + # where the +before_destroy+ method is overridden: # # class Topic < ActiveRecord::Base # def before_destroy() destroy_author end @@ -79,20 +83,20 @@ module ActiveRecord # def before_destroy() destroy_readers end # end # - # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. - # So, use the callback macros when you want to ensure that a certain callback is called for the entire - # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant + # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. + # So, use the callback macros when you want to ensure that a certain callback is called for the entire + # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant # to decide whether they want to call +super+ and trigger the inherited callbacks. # - # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the - # callbacks before specifying the associations. Otherwise, you might trigger the loading of a + # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the + # callbacks before specifying the associations. Otherwise, you might trigger the loading of a # child before the parent has registered the callbacks and they won't be inherited. # # == Types of callbacks # # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects, - # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects - # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for + # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects + # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for # creating mix-ins), and inline eval methods are deprecated. # # The method reference callbacks work by specifying a protected or private method available in the object, like this: @@ -170,14 +174,14 @@ module ActiveRecord # end # end # - # The callback macros usually accept a symbol for the method they're supposed to run, but you can also + # The callback macros usually accept a symbol for the method they're supposed to run, but you can also # pass a "method string", which will then be evaluated within the binding of the callback. Example: # # class Topic < ActiveRecord::Base # before_destroy 'self.class.delete_all "parent_id = #{id}"' # end # - # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback + # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback # is triggered. Also note that these inline callbacks can be stacked just like the regular ones: # # class Topic < ActiveRecord::Base @@ -185,25 +189,17 @@ module ActiveRecord # 'puts "Evaluated after parents are destroyed"' # end # - # == The +after_find+ and +after_initialize+ exceptions - # - # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, - # such as <tt>Base.find(:all)</tt>, we've had to implement a simple performance constraint (50% more speed - # on a simple test case). Unlike all the other callbacks, +after_find+ and +after_initialize+ will only be - # run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the - # callback types will be called. - # # == <tt>before_validation*</tt> returning statements # - # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be - # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a + # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be + # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a # ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object. # # == Canceling callbacks # - # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are - # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled. - # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as + # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are + # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled. + # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as # methods on the model, which are called last. # # == Transactions @@ -218,16 +214,6 @@ module ActiveRecord # needs to be aware of it because an ordinary +save+ will raise such exception # instead of quietly returning +false+. # - # == Debugging callbacks - # - # To list the methods and procs registered with a particular callback, append <tt>_callback_chain</tt> to - # the callback name that you wish to list and send that to your class from the Rails console: - # - # >> Topic.after_save_callback_chain - # => [#<ActiveSupport::Callbacks::Callback:0x3f6a448 - # @method=#<Proc:0x03f9a42c@/Users/foo/bar/app/models/topic.rb:43>, kind:after_save, identifiernil, - # options{}] - # module Callbacks extend ActiveSupport::Concern @@ -235,7 +221,7 @@ module ActiveRecord :after_initialize, :after_find, :after_touch, :before_validation, :after_validation, :before_save, :around_save, :after_save, :before_create, :around_create, :after_create, :before_update, :around_update, :after_update, - :before_destroy, :around_destroy, :after_destroy + :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] included do @@ -246,43 +232,26 @@ module ActiveRecord define_model_callbacks :save, :create, :update, :destroy end - module ClassMethods - def method_added(meth) - super - if CALLBACKS.include?(meth.to_sym) - ActiveSupport::Deprecation.warn("Base##{meth} has been deprecated, please use Base.#{meth} :method instead", caller[0,1]) - send(meth.to_sym, meth.to_sym) - end - end - end - def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } - end - - def deprecated_callback_method(symbol) #:nodoc: - if respond_to?(symbol, true) - ActiveSupport::Deprecation.warn("Overwriting #{symbol} in your models has been deprecated, please use Base##{symbol} :method_name instead") - send(symbol) - end + run_callbacks(:touch) { super } end private def create_or_update #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def create #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def update(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } end end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb new file mode 100644 index 0000000000..fb59d9fb07 --- /dev/null +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -0,0 +1,41 @@ +module ActiveRecord + # :stopdoc: + module Coders + class YAMLColumn + RESCUE_ERRORS = [ ArgumentError ] + + if defined?(Psych) && defined?(Psych::SyntaxError) + RESCUE_ERRORS << Psych::SyntaxError + end + + attr_accessor :object_class + + def initialize(object_class = Object) + @object_class = object_class + end + + def dump(obj) + YAML.dump obj + end + + def load(yaml) + return object_class.new if object_class != Object && yaml.nil? + return yaml unless yaml.is_a?(String) && yaml =~ /^---/ + begin + obj = YAML.load(yaml) + + unless obj.is_a?(object_class) || obj.nil? + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" + end + obj ||= object_class.new if object_class != Object + + obj + rescue *RESCUE_ERRORS + yaml + end + end + end + end + # :startdoc +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 02a8f4e214..4297c26413 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,3 +1,4 @@ +require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/synchronization' @@ -56,7 +57,9 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool + attr_accessor :automatic_reconnect attr_reader :spec, :connections + attr_reader :columns, :columns_hash, :primary_keys, :tables # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -73,18 +76,70 @@ module ActiveRecord # The mutex used to synchronize pool access @connection_mutex = Monitor.new @queue = @connection_mutex.new_cond - - # default 5 second timeout unless on ruby 1.9 - @timeout = - if RUBY_VERSION < '1.9' - spec.config[:wait_timeout] || 5 - end + @timeout = spec.config[:wait_timeout] || 5 # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @connections = [] @checked_out = [] + @automatic_reconnect = true + @tables = {} + + @columns = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + + # Fetch a list of columns + conn.columns(table_name, "#{table_name} Columns").tap do |columns| + + # set primary key information + columns.each do |column| + column.primary = column.name == primary_keys[table_name] + end + end + end + end + + @columns_hash = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + table_exists?(table_name) ? conn.primary_key(table_name) : 'id' + end + end + end + + # A cached lookup for table existence + def table_exists?(name) + return true if @tables.key? name + + with_connection do |conn| + conn.tables.each { |table| @tables[table] = true } + end + + @tables.key? name + end + + # Clears out internal caches: + # + # * columns + # * columns_hash + # * tables + def clear_cache! + @columns.clear + @columns_hash.clear + @tables.clear + end + + # Clear out internal caches for table with +table_name+ + def clear_table_cache!(table_name) + @columns.delete table_name + @columns_hash.delete table_name + @primary_keys.delete table_name end # Retrieve the connection associated with the current thread, or call @@ -93,11 +148,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a hash keyed by the thread id. def connection - if conn = @reserved_connections[current_connection_id] - conn - else - @reserved_connections[current_connection_id] = checkout - end + @reserved_connections[current_connection_id] ||= checkout end # Signal that the thread is finished with the current connection. @@ -109,7 +160,7 @@ module ActiveRecord end # If a connection already exists yield it to the block. If no connection - # exists checkout a connection, yield it to the block, and checkin the + # exists checkout a connection, yield it to the block, and checkin the # connection when finished. def with_connection connection_id = current_connection_id @@ -165,7 +216,6 @@ module ActiveRecord keys = @reserved_connections.keys - Thread.list.find_all { |t| t.alive? }.map { |thread| thread.object_id } - keys.each do |key| checkin @reserved_connections[key] @reserved_connections.delete(key) @@ -198,16 +248,18 @@ module ActiveRecord checkout_new_connection end return conn if conn - # No connections available; wait for one - if @queue.wait(@timeout) + + @queue.wait(@timeout) + + if(@checked_out.size < @connections.size) next else - # try looting dead threads clear_stale_cached_connections! if @size == @checked_out.size raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it." end end + end end end @@ -219,7 +271,7 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) @connection_mutex.synchronize do - conn.send(:_run_checkin_callbacks) do + conn.run_callbacks :checkin do @checked_out.delete conn @queue.signal end @@ -239,6 +291,8 @@ module ActiveRecord end def checkout_new_connection + raise ConnectionNotEstablished unless @automatic_reconnect + c = new_connection @connections << c checkout_and_verify(c) @@ -326,7 +380,7 @@ module ActiveRecord # already been opened. def connected?(klass) conn = retrieve_connection_pool(klass) - conn ? conn.connected? : false + conn && conn.connected? end # Remove the connection for this class. This will close the active @@ -337,7 +391,7 @@ module ActiveRecord pool = @connection_pools[klass.name] return nil unless pool - @connection_pools.delete_if { |key, value| value == pool } + pool.automatic_reconnect = false pool.disconnect! pool.spec.config end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 8e74eff0ab..d88720c8bf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -67,12 +67,12 @@ module ActiveRecord begin require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})" + rescue LoadError => e + raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" end adapter_method = "#{spec[:adapter]}_connection" - if !respond_to?(adapter_method) + unless respond_to?(adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end @@ -89,6 +89,16 @@ module ActiveRecord retrieve_connection end + # Returns the configuration of the associated connection as a hash: + # + # ActiveRecord::Base.connection_config + # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} + # + # Please use only for reading. + def connection_config + connection_pool.spec.config + end + def connection_pool connection_handler.retrieve_connection_pool(self) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index a130c330dd..29ac9341ec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -37,9 +37,9 @@ module ActiveRecord 16 end - # the maximum number of elements in an IN (x,y,z) clause + # the maximum number of elements in an IN (x,y,z) clause. nil means no limit def in_clause_length - 65535 + nil end # the maximum length of an SQL query 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 25432e9985..a3082b8f01 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,10 +1,20 @@ +require 'active_support/core_ext/module/deprecation' + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil) - select(sql, name) + def select_all(sql, name = nil, binds = []) + if supports_statement_cache? + select(sql, name, binds) + else + return select(sql, name) if binds.empty? + binds = binds.dup + select sql.gsub('?') { + quote(*binds.shift.reverse) + }, name + end end # Returns a record hash with the column names as keys and column values @@ -35,10 +45,16 @@ module ActiveRecord undef_method :select_rows # Executes the SQL statement in the context of this connection. - def execute(sql, name = nil, skip_logging = false) + def execute(sql, name = nil) end undef_method :execute + # Executes +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is logged along with + # the executed +sql+ statement. + def exec_query(sql, name = 'SQL', binds = []) + end + # Returns the last auto-generated ID from the affected table. def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) insert_sql(sql, name, pk, id_value, sequence_name) @@ -68,6 +84,12 @@ module ActiveRecord nil end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + false + end + # Runs the given block in a database transaction, and returns the result # of the block. # @@ -209,11 +231,12 @@ module ActiveRecord # # This method *modifies* the +sql+ parameter. # + # This method is deprecated!! Stop using it! + # # ===== Examples # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) # generates # SELECT * FROM suppliers LIMIT 10 OFFSET 50 - def add_limit_offset!(sql, options) if limit = options[:limit] sql << " LIMIT #{sanitize_limit(limit)}" @@ -223,6 +246,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! def default_sequence_name(table, column) nil @@ -236,7 +260,19 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' + columns = Hash[columns(table_name).map { |c| [c.name, c] }] + + key_list = [] + value_list = fixture.map do |name, value| + key_list << quote_column_name(name) + quote(value, columns[name]) + end + + execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' + end + + def null_insert_value + Arel.sql 'DEFAULT' end def empty_insert_statement_value @@ -251,10 +287,29 @@ module ActiveRecord "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" end + # Sanitizes the given LIMIT parameter in order to prevent SQL injection. + # + # The +limit+ may be anything that can evaluate to a string via #to_s. It + # should look like an integer, or a comma-delimited list of integers, or + # an Arel SQL literal. + # + # Returns Integer and Arel::Nodes::SqlLiteral limits as is. + # Returns the sanitized limit parameter, either as an integer, or as a + # string which contains a comma-delimited list of integers. + def sanitize_limit(limit) + if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) + limit + elsif limit.to_s =~ /,/ + Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') + else + Integer(limit) + end + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) + def select(sql, name = nil, binds = []) end undef_method :select @@ -274,21 +329,6 @@ module ActiveRecord update_sql(sql, name) end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. - # - # +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers. - # - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. - def sanitize_limit(limit) - if limit.to_s =~ /,/ - limit.to_s.split(',').map{ |i| i.to_i }.join(',') - else - limit.to_i - end - end - # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. def rollback_transaction_records(rollback) #:nodoc diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 78fffaff6e..1db397f584 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/duplicable' - module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache @@ -49,40 +47,26 @@ module ActiveRecord @query_cache.clear end - def select_all(*args) - if @query_cache_enabled - cache_sql(args.first) { super } - else - super - end - end - - def columns(*) + def select_all(sql, name = nil, binds = []) if @query_cache_enabled - @query_cache["SHOW FIELDS FROM #{args.first}"] ||= super + cache_sql(sql, binds) { super } else super end end private - def cache_sql(sql) + def cache_sql(sql, binds) result = - if @query_cache.has_key?(sql) + if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => self.object_id) - @query_cache[sql] + :sql => sql, :name => "CACHE", :connection_id => object_id) + @query_cache[sql][binds] else - @query_cache[sql] = yield + @query_cache[sql][binds] = yield end - if Array === result - result.collect { |row| row.dup } - else - result.duplicable? ? result.dup : result - end - rescue TypeError - result + result.collect { |row| row.dup } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index e2b3773a99..7489e88eef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,28 +10,32 @@ module ActiveRecord return value.quoted_id if value.respond_to?(:quoted_id) case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode) - elsif column && [:integer, :float].include?(column.type) - value = column.type == :integer ? value.to_i : value.to_f - value.to_s - else - "'#{quote_string(value)}'" # ' (for ruby-mode) - end - when NilClass then "NULL" - when TrueClass then (column && column.type == :integer ? '1' : quoted_true) - when FalseClass then (column && column.type == :integer ? '0' : quoted_false) - when Float, Fixnum, Bignum then value.to_s - # BigDecimals need to be output in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') + when String, ActiveSupport::Multibyte::Chars + value = value.to_s + return "'#{quote_string(value)}'" unless column + + case column.type + when :binary then "'#{quote_string(column.string_to_binary(value))}'" + when :integer then value.to_i.to_s + when :float then value.to_f.to_s + else + "'#{quote_string(value)}'" + end + + when true, false + if column && column.type == :integer + value ? '1' : '0' else - if value.acts_like?(:date) || value.acts_like?(:time) - "'#{quoted_date(value)}'" - else - "'#{quote_string(value.to_s)}'" - end + value ? quoted_true : quoted_false + end + # BigDecimals need to be put in a non-normalized form and quoted. + when nil then "NULL" + when BigDecimal then value.to_s('F') + when Numeric then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + else + "'#{quote_string(value.to_yaml)}'" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 9118ceb33c..7ac48c6646 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,259 +6,6 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - # An abstract definition of a column in a table. - class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale - attr_accessor :primary - - # Instantiates a new column in the table. - # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. - # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. - # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type = nil, null = true) - @name, @sql_type, @null = name, sql_type, null - @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - - @primary = nil - end - - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - - def has_default? - !default.nil? - end - - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime then Time - when :date then Date - when :timestamp then Time - when :time then Time - when :text, :string then String - when :binary then String - when :boolean then Object - end - end - - # Casts value (which is a String) to an appropriate instance. - def type_cast(value) - return nil if value.nil? - case type - when :string then value - when :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f - when :decimal then self.class.value_to_decimal(value) - when :datetime then self.class.string_to_time(value) - when :timestamp then self.class.string_to_time(value) - when :time then self.class.string_to_dummy_time(value) - when :date then self.class.string_to_date(value) - when :binary then self.class.binary_to_string(value) - when :boolean then self.class.value_to_boolean(value) - else value - end - end - - def type_cast_code(var_name) - case type - when :string then nil - when :text then nil - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :float then "#{var_name}.to_f" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :datetime then "#{self.class.name}.string_to_time(#{var_name})" - when :timestamp then "#{self.class.name}.string_to_time(#{var_name})" - when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})" - when :date then "#{self.class.name}.string_to_date(#{var_name})" - when :binary then "#{self.class.name}.binary_to_string(#{var_name})" - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else nil - end - end - - # Returns the human name of the column name. - # - # ===== Examples - # Column.new('sales_stage', ...).human_name # => 'Sales stage' - def human_name - Base.human_attribute_name(@name) - end - - def extract_default(default) - type_cast(default) - end - - class << self - # Used to convert from Strings to BLOBs - def string_to_binary(value) - value - end - - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def string_to_date(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_date(string) || fallback_string_to_date(string) - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - string_to_time "2000-01-01 #{string}" - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.blank? - nil - else - TRUE_VALUES.include?(value) - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || year == 0 - - Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i, /string/i - :string - when /boolean/i - :boolean - end - end - end - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: end @@ -268,6 +15,10 @@ module ActiveRecord # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: + def string_to_binary(value) + value + end + def sql_type base.type_to_sql(type.to_sym, limit, precision, scale) rescue type end @@ -318,21 +69,13 @@ module ActiveRecord @base = base end - #Handles non supported datatypes - e.g. XML - def method_missing(symbol, *args) - if symbol.to_s == 'xml' - xml_column_fallback(args) - else - super - end - end + def xml(*args) + raise NotImplementedError unless %w{ + sqlite mysql mysql2 + }.include? @base.adapter_name.downcase - def xml_column_fallback(*args) - case @base.adapter_name.downcase - when 'sqlite', 'mysql' - options = args.extract_options! - column(args[0], :text, options) - end + options = args.extract_options! + column(args[0], :text, options) end # Appends a primary key definition to the table definition. @@ -360,7 +103,7 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and + # Requests a maximum column length. This is number of characters for <tt>:string</tt> and # <tt>:text</tt> columns and number of bytes for :binary and :integer columns. # * <tt>:default</tt> - # The column's default value. Use nil for NULL. @@ -464,7 +207,7 @@ module ActiveRecord # TableDefinition#timestamps that'll add created_at and +updated_at+ as datetimes. # # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type - # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of + # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this: # # create_table :taggings do |t| @@ -528,7 +271,7 @@ module ActiveRecord # concatenated together. This string can then be prepended and appended to # to generate the final SQL to create the table. def to_sql - @columns.map(&:to_sql) * ', ' + @columns.map { |c| c.to_sql } * ', ' end private diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7dee68502f..8bae50885f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -12,7 +12,7 @@ module ActiveRecord # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) - table_name[0..table_alias_length-1].gsub(/\./, '_') + table_name[0...table_alias_length].gsub(/\./, '_') end # def tables(name = nil) end @@ -110,8 +110,8 @@ module ActiveRecord # # Also note that this just sets the primary key in the table. You additionally # need to configure the primary key in the model via the +set_primary_key+ macro. - # Models do NOT auto-detect the primary key from their table definition. - # + # Models do NOT auto-detect the primary key from their table definition. + # # [<tt>:options</tt>] # Any extra options you want appended to the table definition. # [<tt>:temporary</tt>] @@ -151,10 +151,10 @@ module ActiveRecord # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - table_definition = TableDefinition.new(self) - table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + td = table_definition + td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false - yield table_definition if block_given? + yield td if block_given? if options[:force] && table_exists?(table_name) drop_table(table_name, options) @@ -162,7 +162,7 @@ module ActiveRecord create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " create_sql << "#{quote_table_name(table_name)} (" - create_sql << table_definition.to_sql + create_sql << td.to_sql create_sql << ") #{options[:options]}" execute create_sql end @@ -176,6 +176,13 @@ module ActiveRecord # # Other column alterations here # end # + # The +options+ hash can include the following keys: + # [<tt>:bulk</tt>] + # Set this to true to make this a bulk alter query, such as + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # Defaults to false. + # # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| @@ -224,8 +231,14 @@ module ActiveRecord # # See also Table for details on # all of the various column transformation - def change_table(table_name) - yield Table.new(table_name, self) + def change_table(table_name, options = {}) + if supports_bulk_alter? && options[:bulk] + recorder = ActiveRecord::Migration::CommandRecorder.new(self) + yield Table.new(table_name, recorder) + bulk_change_table(table_name, recorder.commands) + else + yield Table.new(table_name, self) + end end # Renames a table. @@ -253,10 +266,7 @@ module ActiveRecord # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" - end + columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" } end alias :remove_columns :remove_column @@ -269,12 +279,11 @@ module ActiveRecord raise NotImplementedError, "change_column is not implemented" end - # Sets a new default value for a column. If you want to set the default - # value to +NULL+, you are out of luck. You need to - # DatabaseStatements#execute the appropriate SQL statement yourself. + # Sets a new default value for a column. # ===== Examples # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) + # change_column_default(:users, :email, nil) def change_column_default(table_name, column_name, default) raise NotImplementedError, "change_column_default is not implemented" end @@ -327,29 +336,8 @@ module ActiveRecord # # Note: SQLite doesn't support index length def add_index(table_name, column_name, options = {}) - options[:name] = options[:name].to_s if options.key?(:name) - - column_names = Array.wrap(column_name) - index_name = index_name(table_name, :column => column_names) - - if Hash === options # legacy support, since this param was a string - index_type = options[:unique] ? "UNIQUE" : "" - index_name = options[:name] || index_name - else - index_type = options - end - - if index_name.length > index_name_length - @logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.") - return - end - if index_name_exists?(table_name, index_name, false) - @logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.") - return - end - quoted_column_names = quoted_columns_for_index(column_names, options).join(", ") - - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})" + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" end # Remove the given index from the table. @@ -363,12 +351,7 @@ module ActiveRecord # Remove the index named by_branch_party in the accounts table. # remove_index :accounts, :name => :by_branch_party def remove_index(table_name, options = {}) - index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) - @logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.") - return - end - remove_index!(table_name, index_name) + remove_index!(table_name, index_name_for_remove(table_name, options)) end def remove_index!(table_name, index_name) #:nodoc: @@ -407,6 +390,7 @@ module ActiveRecord # as there's no way to determine the correct answer in that case. def index_name_exists?(table_name, index_name, default) return default unless respond_to?(:indexes) + index_name = index_name.to_s indexes(table_name).detect { |i| i.name == index_name } end @@ -446,12 +430,14 @@ module ActiveRecord end end - def assume_migrated_upto_version(version, migrations_path = ActiveRecord::Migrator.migrations_path) + def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + migrations_paths = Array.wrap(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename| + migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i } + paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } + versions = Dir[*paths].map do |filename| filename.split('/').last.split('_').first.to_i end @@ -471,7 +457,7 @@ module ActiveRecord end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - if native = native_database_types[type] + if native = native_database_types[type.to_sym] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup if type == :decimal # ignore limit, use precision and scale @@ -538,6 +524,50 @@ module ActiveRecord def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def add_index_options(table_name, column_name, options = {}) + column_names = Array.wrap(column_name) + index_name = index_name(table_name, :column => column_names) + + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name].to_s if options.key?(:name) + else + index_type = options + end + + if index_name.length > index_name_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + end + if index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + index_columns = quoted_columns_for_index(column_names, options).join(", ") + + [index_name, index_type, index_columns] + end + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + + def columns_for_remove(table_name, *column_names) + column_names = column_names.flatten + + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? + column_names.map {|column_name| quote_column_name(column_name) } + end + + private + def table_definition + TableDefinition.new(self) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d8c92d0ad3..0f44baa2fe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' # TODO: Autoload these files +require 'active_record/connection_adapters/column' require 'active_record/connection_adapters/abstract/schema_definitions' require 'active_record/connection_adapters/abstract/schema_statements' require 'active_record/connection_adapters/abstract/database_statements' @@ -12,6 +13,7 @@ require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' require 'active_record/connection_adapters/abstract/database_limits' +require 'active_record/result' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -40,7 +42,7 @@ module ActiveRecord @active = nil @connection, @logger = connection, logger @query_cache_enabled = false - @query_cache = {} + @query_cache = Hash.new { |h,sql| h[sql] = {} } @instrumenter = ActiveSupport::Notifications.instrumenter end @@ -76,8 +78,12 @@ module ActiveRecord false end - # Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite - # does not. + def supports_bulk_alter? + false + end + + # Does this adapter support savepoints? PostgreSQL and MySQL do, + # SQLite < 3.6.8 does not. def supports_savepoints? false end @@ -97,6 +103,12 @@ module ActiveRecord quote_column_name(name) end + # Returns a bind substitution value given a +column+ and list of current + # +binds+ + def substitute_for(column, binds) + Arel.sql '?' + end + # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -135,6 +147,13 @@ module ActiveRecord # this should be overridden by concrete adapters end + ### + # Clear any caching the database adapter may be doing, for example + # clearing the prepared statement cache. This is database specific. + def clear_cache! + # this should be overridden by concrete adapters + end + # Returns true if its required to reload the connection between requests for development mode. # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite. def requires_reloading? @@ -190,12 +209,13 @@ module ActiveRecord protected - def log(sql, name) - name ||= "SQL" - @instrumenter.instrument("sql.active_record", - :sql => sql, :name => name, :connection_id => object_id) do - yield - end + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } rescue Exception => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.debug message if @logger diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb new file mode 100644 index 0000000000..4e3d8a096f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -0,0 +1,268 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + # An abstract definition of a column in a table. + class Column + TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set + + module Format + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + end + + attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale + attr_accessor :primary, :coder + + alias :encoded? :coder + + # Instantiates a new column in the table. + # + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. + # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. + # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in + # <tt>company_name varchar(60)</tt>. + # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. + # +null+ determines if this column allows +NULL+ values. + def initialize(name, default, sql_type = nil, null = true) + @name = name + @sql_type = sql_type + @null = null + @limit = extract_limit(sql_type) + @precision = extract_precision(sql_type) + @scale = extract_scale(sql_type) + @type = simplified_type(sql_type) + @default = extract_default(default) + @primary = nil + @coder = nil + end + + # Returns +true+ if the column is either of type string or text. + def text? + type == :string || type == :text + end + + # Returns +true+ if the column is either of type integer, float or decimal. + def number? + type == :integer || type == :float || type == :decimal + end + + def has_default? + !default.nil? + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime, :timestamp, :time then Time + when :date then Date + when :text, :string, :binary then String + when :boolean then Object + end + end + + # Casts value (which is a String) to an appropriate instance. + def type_cast(value) + return nil if value.nil? + return coder.load(value) if encoded? + + klass = self.class + + case type + when :string, :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f + when :decimal then klass.value_to_decimal(value) + when :datetime, :timestamp then klass.string_to_time(value) + when :time then klass.string_to_dummy_time(value) + when :date then klass.string_to_date(value) + when :binary then klass.binary_to_string(value) + when :boolean then klass.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + klass = self.class.name + + case type + when :string, :text then var_name + when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :float then "#{var_name}.to_f" + when :decimal then "#{klass}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" + when :time then "#{klass}.string_to_dummy_time(#{var_name})" + when :date then "#{klass}.string_to_date(#{var_name})" + when :binary then "#{klass}.binary_to_string(#{var_name})" + when :boolean then "#{klass}.value_to_boolean(#{var_name})" + else var_name + end + end + + # Returns the human name of the column name. + # + # ===== Examples + # Column.new('sales_stage', ...).human_name # => 'Sales stage' + def human_name + Base.human_attribute_name(@name) + end + + def extract_default(default) + type_cast(default) + end + + # Used to convert from Strings to BLOBs + def string_to_binary(value) + self.class.string_to_binary(value) + end + + class << self + # Used to convert from Strings to BLOBs + def string_to_binary(value) + value + end + + # Used to convert from BLOBs to Strings + def binary_to_string(value) + value + end + + def string_to_date(string) + return string unless string.is_a?(String) + return nil if string.empty? + + fast_string_to_date(string) || fallback_string_to_date(string) + end + + def string_to_time(string) + return string unless string.is_a?(String) + return nil if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + def string_to_dummy_time(string) + return string unless string.is_a?(String) + return nil if string.empty? + + string_to_time "2000-01-01 #{string}" + end + + # convert something to a boolean + def value_to_boolean(value) + if value.is_a?(String) && value.blank? + nil + else + TRUE_VALUES.include?(value) + end + end + + # convert something to a BigDecimal + def value_to_decimal(value) + # Using .class is faster than .is_a? and + # subclasses of BigDecimal will be handled + # in the else clause + if value.class == BigDecimal + value + elsif value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end + + protected + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + end + + def new_date(year, mon, mday) + if year && year != 0 + Date.new(year, mon, mday) rescue nil + end + end + + def new_time(year, mon, mday, hour, min, sec, microsec) + # Treat 0000-00-00 00:00:00 as nil. + return nil if year.nil? || year == 0 + + Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + + def fast_string_to_date(string) + if string =~ Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ Format::ISO_DATETIME + microsec = ($7.to_f * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def fallback_string_to_time(string) + time_hash = Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + + private + def extract_limit(sql_type) + $1.to_i if sql_type =~ /\((.*)\)/ + end + + def extract_precision(sql_type) + $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i + end + + def extract_scale(sql_type) + case sql_type + when /^(numeric|decimal|number)\((\d+)\)/i then 0 + when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i + end + end + + def simplified_type(field_type) + case field_type + when /int/i + :integer + when /float|double/i + :float + when /decimal|numeric|number/i + extract_scale(field_type) == 0 ? :integer : :decimal + when /datetime/i + :datetime + when /timestamp/i + :timestamp + when /time/i + :time + when /date/i + :date + when /clob/i, /text/i + :text + when /blob/i, /binary/i + :binary + when /char/i, /string/i + :string + when /boolean/i + :boolean + end + end + end + end + # :startdoc: +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 568759775b..7bad511c64 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,11 +1,16 @@ # encoding: utf-8 -require 'mysql2' unless defined? Mysql2 +require 'mysql2' module ActiveRecord class Base def self.mysql2_connection(config) config[:username] = 'root' if config[:username].nil? + + if Mysql2::Client.const_defined? :FOUND_ROWS + config[:flags] = Mysql2::Client::FOUND_ROWS + end + client = Mysql2::Client.new(config.symbolize_keys) options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) @@ -13,6 +18,9 @@ module ActiveRecord end module ConnectionAdapters + class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: + end + class Mysql2Column < Column BOOL = "tinyint(1)" def extract_default(default) @@ -34,62 +42,17 @@ module ActiveRecord super end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime then Time - when :date then Date - when :timestamp then Time - when :time then Time - when :text, :string then String - when :binary then String - when :boolean then Object - end - end - - def type_cast(value) - return nil if value.nil? - case type - when :string then value - when :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f # returns self if it's already a Float - when :decimal then self.class.value_to_decimal(value) - when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value) - when :time then value.class == Time ? value : self.class.string_to_dummy_time(value) - when :date then value.class == Date ? value : self.class.string_to_date(value) - when :binary then value - when :boolean then self.class.value_to_boolean(value) - else value - end - end - - def type_cast_code(var_name) - case type - when :string then nil - when :text then nil - when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0" - when :float then "#{var_name}.to_f" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})" - when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})" - when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})" - when :binary then nil - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else nil - end - end - private def simplified_type(field_type) return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) - return :string if field_type =~ /enum/i or field_type =~ /set/i - return :integer if field_type =~ /year/i - return :binary if field_type =~ /bit/i - super + + case field_type + when /enum/i, /set/i then :string + when /year/i then :integer + when /bit/i then :binary + else + super + end end def extract_limit(sql_type) @@ -234,10 +197,7 @@ module ActiveRecord def active? return false unless @connection - @connection.query 'select 1' - true - rescue Mysql2::Error - false + @connection.ping end def reconnect! @@ -277,7 +237,7 @@ module ActiveRecord # return r # end # end - # + # # # Returns a single value from a record # def select_value(sql, name = nil) # result = execute(sql, name) @@ -285,7 +245,7 @@ module ActiveRecord # first.first # end # end - # + # # # Returns an array of the values of the first column in a select: # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] # def select_values(sql, name = nil) @@ -368,6 +328,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! # SCHEMA STATEMENTS ======================================== @@ -442,10 +403,11 @@ module ActiveRecord if current_index != row[:Key_name] next if row[:Key_name] == PRIMARY # skip the primary key current_index = row[:Key_name] - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, []) + indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) end indexes.last.columns << row[:Column_name] + indexes.last.lengths << row[:Sub_part] end indexes end @@ -453,7 +415,7 @@ module ActiveRecord def columns(table_name, name = nil) sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql, :skip_logging) + result = execute(sql) result.each(:symbolize_keys => true, :as => :hash) { |field| columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") } @@ -606,12 +568,21 @@ module ActiveRecord def configure_connection @connection.query_options.merge!(:as => :array) - encoding = @config[:encoding] - execute("SET NAMES '#{encoding}'", :skip_logging) if encoding # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 - execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + variable_assignments = ['SQL_AUTO_IS_NULL=0'] + encoding = @config[:encoding] + + # make sure we set the encoding + variable_assignments << "NAMES '#{encoding}'" if encoding + + # increase timeout so mysql server doesn't disconnect us + wait_timeout = @config[:wait_timeout] + wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + variable_assignments << "@@wait_timeout = #{wait_timeout}" + + execute("SET #{variable_assignments.join(', ')}", :skip_logging) end # Returns an array of record hashes with the column names as keys and diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index ba0051de05..e1186209d3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -3,6 +3,28 @@ require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' require 'set' +begin + require 'mysql' +rescue LoadError + raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'" +end + +unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) + raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" +end + +class Mysql + class Time + ### + # This monkey patch is for test_additional_columns_from_join_table + def to_date + Date.new(year, month, day) + end + end + class Stmt; include Enumerable end + class Result; include Enumerable end +end + module ActiveRecord class Base # Establishes a connection to the database that's used by all Active Record objects. @@ -15,22 +37,11 @@ module ActiveRecord password = config[:password].to_s database = config[:database] - unless defined? Mysql - begin - require 'mysql' - rescue LoadError - raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'" - end - - unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) - raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'" - end - end - mysql = Mysql.init mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0 + default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) options = [host, username, password, database, port, socket, default_flags] ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) end @@ -38,6 +49,30 @@ module ActiveRecord module ConnectionAdapters class MysqlColumn < Column #:nodoc: + class << self + def string_to_time(value) + return super unless Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + end + + def string_to_dummy_time(v) + return super unless Mysql::Time === v + new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) + end + + def string_to_date(v) + return super unless Mysql::Time === v + new_date(v.year, v.month, v.day) + end + end + def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? @@ -131,7 +166,7 @@ module ActiveRecord cattr_accessor :emulate_booleans self.emulate_booleans = true - ADAPTER_NAME = 'MySQL'.freeze + ADAPTER_NAME = 'MySQL' LOST_CONNECTION_ERROR_MESSAGES = [ "Server shutdown in progress", @@ -139,10 +174,10 @@ module ActiveRecord "Lost connection to MySQL server during query", "MySQL server has gone away" ] - QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze + QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze, + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", :string => { :name => "varchar", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "int", :limit => 4 }, @@ -160,6 +195,7 @@ module ActiveRecord super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @statements = {} connect end @@ -167,6 +203,16 @@ module ActiveRecord ADAPTER_NAME end + def supports_bulk_alter? #:nodoc: + true + end + + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -251,6 +297,7 @@ module ActiveRecord def reconnect! disconnect! + clear_cache! connect end @@ -271,14 +318,69 @@ module ActiveRecord def select_rows(sql, name = nil) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each { |row| rows << row } - result.free + rows = exec_without_stmt(sql, name).rows + @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end - # Executes an SQL query and returns a MySQL::Result object. Note that you have to free + def clear_cache! + @statements.values.each do |cache| + cache[:stmt].close + end + @statements.clear + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = nil + + cache = {} + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end + + stmt.execute(*binds.map { |col, val| + col ? col.type_cast(val) : val + }) + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + + metadata.free + result = ActiveRecord::Result.new(cols, stmt.to_a) + end + + stmt.free_result + stmt.close if binds.empty? + + result + end + end + + def exec_without_stmt(sql, name = 'SQL') # :nodoc: + # Some queries, like SHOW CREATE TABLE don't work through the prepared + # statement API. For those queries, we need to use this method. :'( + log(sql, name) do + result = @connection.query(sql) + cols = [] + rows = [] + + if result + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + end + ActiveRecord::Result.new(cols, rows) + end + end + + # Executes an SQL query and returns a MySQL::Result object. Note that you have to free # the Result object after you're done using it. def execute(sql, name = nil) #:nodoc: if name == :skip_logging @@ -306,8 +408,8 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - execute "BEGIN" - rescue Exception + exec_without_stmt "BEGIN" + rescue Mysql::Error # Transactions aren't supported end @@ -346,6 +448,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! # SCHEMA STATEMENTS ======================================== @@ -356,10 +459,11 @@ module ActiveRecord sql = "SHOW TABLES" end - select_all(sql).inject("") do |structure, table| + select_all(sql).map do |table| table.delete('Table_type') - structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" - end + sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" + exec_without_stmt(sql).first['Create Table'] + ";\n\n" + end.join("") end def recreate_database(name, options = {}) #:nodoc: @@ -400,14 +504,28 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil) #:nodoc: + def tables(name = nil, database = nil) #:nodoc: tables = [] - result = execute("SHOW TABLES", name) + result = execute(["SHOW TABLES", database].compact.join(' IN '), name) result.each { |field| tables << field[0] } result.free tables end + def table_exists?(name) + return true if super + + name = name.to_s + schema, table = name.split('.', 2) + + unless table # A table was provided without a schema + table = schema + schema = nil + end + + tables(nil, schema).include? table + end + def drop_table(table_name, options = {}) super(table_name, options) end @@ -433,7 +551,7 @@ module ActiveRecord def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql, :skip_logging) + result = execute(sql) result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } result.free columns @@ -447,11 +565,23 @@ module ActiveRecord execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" end + def bulk_change_table(table_name, operations) #:nodoc: + sqls = operations.map do |command, args| + table, arguments = args.shift, args + method = :"#{command}_sql" + + if respond_to?(method) + send(method, table, *arguments) + else + raise "Unknown method called : #{method}(#{arguments.inspect})" + end + end.flatten.join(", ") + + execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") + end + def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - execute(add_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") end def change_column_default(table_name, column_name, default) #:nodoc: @@ -470,34 +600,11 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - options[:null] = column.null - end - - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - execute(change_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - options = {} - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - execute(rename_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") end # Maps logical Rails types to MySQL-specific data types. @@ -580,6 +687,69 @@ module ActiveRecord end end + def add_column_sql(table_name, column_name, type, options = {}) + add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + add_column_sql + end + + def remove_column_sql(table_name, *column_names) + columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } + end + alias :remove_columns_sql :remove_column + + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + change_column_sql + end + + def rename_column_sql(table_name, column_name, new_column_name) + options = {} + + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + rename_column_sql + end + + def add_index_sql(table_name, column_name, options = {}) + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + end + + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "DROP INDEX #{index_name}" + end + + def add_timestamps_sql(table_name) + [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + end + + def remove_timestamps_sql(table_name) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end + private def connect encoding = @config[:encoding] @@ -612,12 +782,10 @@ module ActiveRecord execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) end - def select(sql, name = nil) + def select(sql, name = nil, binds = []) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each_hash { |row| rows << row } - result.free + rows = exec_query(sql, name, binds).to_a + @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6fae899e87..5a830a50fb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,20 +1,19 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' +require 'pg' module ActiveRecord class Base # Establishes a connection to the database that's used by all Active Record objects def self.postgresql_connection(config) # :nodoc: - require 'pg' - config = config.symbolize_keys host = config[:host] port = config[:port] || 5432 username = config[:username].to_s if config[:username] password = config[:password].to_s if config[:password] - if config.has_key?(:database) + if config.key?(:database) database = config[:database] else raise ArgumentError, "No database specified. Missing argument: database." @@ -27,12 +26,6 @@ module ActiveRecord end module ConnectionAdapters - class TableDefinition - def xml(*args) - options = args.extract_options! - column(args[0], 'xml', options) - end - end # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: # Instantiates a new PostgreSQL column definition in a table. @@ -170,9 +163,7 @@ module ActiveRecord end end end - end - module ConnectionAdapters # The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure # Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers. # @@ -183,19 +174,24 @@ module ActiveRecord # * <tt>:username</tt> - Defaults to nothing. # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given + # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option. - # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO + # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO # <encoding></tt> call on the connection. - # * <tt>:min_messages</tt> - An optional client min messages that is used in a + # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. - # * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; - # otherwise, use blocking query methods. class PostgreSQLAdapter < AbstractAdapter - ADAPTER_NAME = 'PostgreSQL'.freeze + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + def xml(*args) + options = args.extract_options! + column(args[0], 'xml', options) + end + end + + ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { - :primary_key => "serial primary key".freeze, + :primary_key => "serial primary key", :string => { :name => "character varying", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "integer" }, @@ -215,6 +211,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) @@ -224,11 +226,19 @@ module ActiveRecord @local_tz = nil @table_alias_length = nil @postgresql_version = nil + @statements = {} connect @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] end + def clear_cache! + @statements.each_value do |value| + @connection.query "DEALLOCATE #{value}" + end + @statements.clear + end + # Is this connection alive and ready for queries? def active? if @connection.respond_to?(:status) @@ -246,6 +256,7 @@ module ActiveRecord # Close then reopen the connection. def reconnect! if @connection.respond_to?(:reset) + clear_cache! @connection.reset configure_connection else @@ -254,8 +265,14 @@ module ActiveRecord end end + def reset! + clear_cache! + super + end + # Close the connection. def disconnect! + clear_cache! @connection.close rescue nil end @@ -317,19 +334,22 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: return super unless column - if value.kind_of?(String) && column.type == :binary - "'#{escape_bytea(value)}'" - elsif value.kind_of?(String) && column.sql_type == 'xml' - "xml '#{quote_string(value)}'" - elsif value.kind_of?(Numeric) && column.sql_type == 'money' + case value + when Numeric + return super unless column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. "'#{value}'" - elsif value.kind_of?(String) && column.sql_type =~ /^bit/ - case value - when /^[01]*$/ - "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i - "X'#{value}'" # Hexadecimal notation + when String + case column.sql_type + when 'bytea' then "'#{escape_bytea(value)}'" + when 'xml' then "xml '#{quote_string(value)}'" + when /^bit/ + case value + when /^[01]*$/ then "B'#{value}'" # Bit-string notation + when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation + end + else + super end else super @@ -375,13 +395,16 @@ module ActiveRecord end end + # Set the authorized user for this session + def session_auth=(user) + clear_cache! + exec_query "SET SESSION AUTHORIZATION #{user}" + end + # REFERENTIAL INTEGRITY ==================================== def supports_disable_referential_integrity?() #:nodoc: - version = query("SHOW server_version")[0][0].split('.') - version[0].to_i >= 8 && version[1].to_i >= 1 - rescue - return false + postgresql_version >= 80100 end def disable_referential_integrity #:nodoc: @@ -430,7 +453,7 @@ module ActiveRecord # If a pk is given, fallback to default sequence name. # Don't fetch last insert id for a table without a pk. if pk && sequence_name ||= default_sequence_name(table, pk) - last_insert_id(table, sequence_name) + last_insert_id(sequence_name) end end end @@ -466,8 +489,8 @@ module ActiveRecord # (2) $12.345.678,12 case data when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d\.]/, '') - when /^-?\D+[\d\.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') end end @@ -502,6 +525,35 @@ module ActiveRecord end end + def substitute_for(column, current_values) + Arel.sql("$#{current_values.length + 1}") + end + + def exec_query(sql, name = 'SQL', binds = []) + return exec_no_cache(sql, name) if binds.empty? + + log(sql, name, binds) do + unless @statements.key? sql + nextkey = "a#{@statements.length + 1}" + @connection.prepare nextkey, sql + @statements[sql] = nextkey + end + + key = @statements[sql] + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(key, binds.map { |col, val| + col ? col.type_cast(val) : val + }) + @connection.block + result = @connection.get_last_result + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + return ret + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -659,8 +711,8 @@ module ActiveRecord # Returns the list of all column definitions for a table. def columns(table_name, name = nil) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).collect do |name, type, default, notnull| - PostgreSQLColumn.new(name, default, type, notnull == 'f') + column_definitions(table_name).collect do |column_name, type, default, notnull| + PostgreSQLColumn.new(column_name, default, type, notnull == 'f') end end @@ -734,7 +786,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, 'PK and serial sequence')[0] + result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first SELECT attr.attname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -793,14 +845,18 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. def add_column(table_name, column_name, type, options = {}) - default = options[:default] - notnull = options[:null] == false + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) - # Add the column. - execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") + begin + execute add_column_sql + rescue ActiveRecord::StatementInvalid => e + raise e if postgresql_version > 80000 - change_column_default(table_name, column_name, default) if options_include_default?(options) - change_column_null(table_name, column_name, false, default) if notnull + execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") + change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + end end # Changes the column of a table. @@ -850,6 +906,10 @@ module ActiveRecord execute "DROP INDEX #{quote_table_name(index_name)}" end + def rename_index(table_name, old_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end + def index_name_length 63 end @@ -873,13 +933,13 @@ module ActiveRecord # requires that the ORDER BY include the distinct column. # # distinct("posts.id", "posts.created_at desc") - def distinct(columns, order_by) #:nodoc: - return "DISTINCT #{columns}" if order_by.blank? + def distinct(columns, orders) #:nodoc: + return "DISTINCT #{columns}" if orders.empty? # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = order_by.split(',').collect { |s| s.split.first } - order_columns.delete_if(&:blank?) + order_columns = orders.collect { |s| s =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : s } + order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } # Return a DISTINCT ON() clause that's distinct on the columns we want but includes @@ -897,8 +957,12 @@ module ActiveRecord else # Mimic PGconn.server_version behavior begin - query('SELECT version()')[0][0] =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/ - ($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i + if query('SELECT version()')[0][0] =~ /PostgreSQL ([0-9.]+)/ + major, minor, tiny = $1.split(".") + (major.to_i * 10000) + (minor.to_i * 100) + tiny.to_i + else + 0 + end rescue 0 end @@ -917,6 +981,15 @@ module ActiveRecord end private + def exec_no_cache(sql, name) + log(sql, name) do + result = @connection.async_exec(sql) + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + ret + end + end + # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -929,7 +1002,7 @@ module ActiveRecord PGconn.translate_results = false if PGconn.respond_to?(:translate_results=) # Ignore async_exec and async_query when using postgres-pr. - @async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec) + @async = @connection.respond_to?(:async_exec) # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision @@ -965,17 +1038,15 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id(table, sequence_name) #:nodoc: - Integer(select_value("SELECT currval('#{sequence_name}')")) + def last_insert_id(sequence_name) #:nodoc: + r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) + Integer(r.rows.first.first) end # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil) - fields, rows = select_raw(sql, name) - rows.map do |row| - Hash[*fields.zip(row).flatten] - end + def select(sql, name = nil, binds = []) + exec_query(sql, name, binds).to_a end def select_raw(sql, name = nil) @@ -1005,7 +1076,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: - query <<-end_sql + exec_query(<<-end_sql).rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum @@ -1016,14 +1087,18 @@ module ActiveRecord end def extract_pg_identifier_from_name(name) - match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) + match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) if match_data - rest = name[match_data[0].length..-1] - rest = rest[1..-1] if rest[0,1] == "." + rest = name[match_data[0].length, name.length] + rest = rest[1, rest.length] if rest.start_with? "." [match_data[1], (rest.length > 0 ? rest : nil)] end end + + def table_definition + TableDefinition.new(self) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e5e92f2b1c..c3a7b039ff 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,4 +1,5 @@ require 'active_record/connection_adapters/sqlite_adapter' +require 'sqlite3' module ActiveRecord class Base @@ -20,16 +21,12 @@ module ActiveRecord raise ArgumentError, 'adapter name should be "sqlite3"' end - unless self.class.const_defined?(:SQLite3) - require_library_or_gem(config[:adapter]) - end - db = SQLite3::Database.new( config[:database], :results_as_hash => true ) - db.busy_timeout(config[:timeout]) unless config[:timeout].nil? + db.busy_timeout(config[:timeout]) if config[:timeout] ConnectionAdapters::SQLite3Adapter.new(db, logger, config) end @@ -37,14 +34,21 @@ module ActiveRecord module ConnectionAdapters #:nodoc: class SQLite3Adapter < SQLiteAdapter # :nodoc: + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + else + super + end + end # Returns the current database encoding format as a string, eg: 'UTF-8' def encoding if @connection.respond_to?(:encoding) @connection.encoding.to_s else - encoding = @connection.execute('PRAGMA encoding') - encoding[0]['encoding'] + @connection.execute('PRAGMA encoding')[0]['encoding'] end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 82ad0a3b8e..ae61d6ce94 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -29,7 +29,7 @@ module ActiveRecord end end - # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby + # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby # drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/). # # Options: @@ -40,16 +40,17 @@ module ActiveRecord include Comparable def initialize(version_string) - @version = version_string.split('.').map(&:to_i) + @version = version_string.split('.').map { |v| v.to_i } end def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) + @version <=> version_string.split('.').map { |v| v.to_i } end end def initialize(connection, logger, config) super(connection, logger) + @statements = {} @config = config end @@ -61,6 +62,16 @@ module ActiveRecord sqlite_version >= '2.0.0' end + def supports_savepoints? + sqlite_version >= '3.6.8' + end + + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -79,9 +90,14 @@ module ActiveRecord def disconnect! super + clear_cache! @connection.close rescue nil end + def clear_cache! + @statements.clear + end + def supports_count_distinct? #:nodoc: sqlite_version >= '3.2.6' end @@ -121,7 +137,7 @@ module ActiveRecord # Quote date/time values for use in SQL input. Includes microseconds # if the value is a Time responding to usec. def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) + if value.respond_to?(:usec) "#{super}.#{sprintf("%06d", value.usec)}" else super @@ -131,6 +147,32 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def exec_query(sql, name = nil, binds = []) + log(sql, name, binds) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + col ? col.type_cast(val) : val + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.execute(sql) } end @@ -151,9 +193,19 @@ module ActiveRecord alias :create :insert_sql def select_rows(sql, name = nil) - execute(sql, name).map do |row| - (0...(row.size / 2)).map { |i| row[i] } - end + exec_query(sql, name).rows + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") end def begin_db_transaction #:nodoc: @@ -177,24 +229,33 @@ module ActiveRecord WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL - execute(sql, name).map do |row| + exec_query(sql, name).map do |row| row['name'] end end def columns(table_name, name = nil) #:nodoc: table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/ + field["dflt_value"] = $1.gsub(/''/, "'") + when /^"(.*)"$/ + field["dflt_value"] = $1.gsub(/""/, '"') + end + SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) end end def indexes(table_name, name = nil) #:nodoc: - execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| IndexDefinition.new( table_name, row['name'], - row['unique'].to_i != 0, - execute("PRAGMA index_info('#{row['name']}')").map { |col| + row['unique'] != 0, + exec_query("PRAGMA index_info('#{row['name']}')").map { |col| col['name'] }) end @@ -202,17 +263,17 @@ module ActiveRecord def primary_key(table_name) #:nodoc: column = table_structure(table_name).find { |field| - field['pk'].to_i == 1 + field['pk'] == 1 } column && column['name'] end def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_column_name(index_name)}" + exec_query "DROP INDEX #{quote_column_name(index_name)}" end def rename_table(name, new_name) - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" end # See: http://www.sqlite.org/lang_altertable.html @@ -249,7 +310,7 @@ module ActiveRecord def change_column_null(table_name, column_name, null, default = nil) unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end alter_table(table_name) do |definition| definition[column_name].null = null @@ -275,23 +336,21 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end + def null_insert_value + Arel.sql 'NULL' + end + def empty_insert_statement_value "VALUES(NULL)" end protected - def select(sql, name = nil) #:nodoc: - execute(sql, name).map do |row| - record = {} - row.each do |key, value| - record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String) - end - record - end + def select(sql, name = nil, binds = []) #:nodoc: + exec_query(sql, name, binds).to_a end def table_structure(table_name) - structure = @connection.table_info(quote_table_name(table_name)) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})").to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? structure end @@ -345,7 +404,7 @@ module ActiveRecord name = name[5..-1] end - to_column_names = columns(to).map(&:name) + to_column_names = columns(to).map { |c| c.name } columns = index.columns.map {|c| rename[c] || c }.select do |column| to_column_names.include?(column) end @@ -360,18 +419,18 @@ module ActiveRecord end def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[*columns.map {|name| [name, name]}.flatten] - rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map} + column_mappings = Hash[columns.map {|name| [name, name]}] + rename.each { |a| column_mappings[a.last] = a.first } from_columns = columns(from).collect {|col| col.name} columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) - @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row| + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' sql << ')' - @connection.execute sql + exec_query sql end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 237cd56683..7839f03848 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -30,9 +30,10 @@ module ActiveRecord reflection = belongs_to.find { |e| e.class_name == expected_name } counter_name = reflection.counter_cache_column - self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({ + stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ arel_table[counter_name] => object.send(association).count }) + connection.update stmt.to_sql end return true end @@ -56,15 +57,15 @@ module ActiveRecord # Post.update_counters 5, :comment_count => -1, :action_count => 1 # # Executes the following SQL: # # UPDATE posts - # # SET comment_count = comment_count - 1, - # # action_count = action_count + 1 + # # SET comment_count = COALESCE(comment_count, 0) - 1, + # # action_count = COALESCE(action_count, 0) + 1 # # WHERE id = 5 # # # For the Posts with id of 10 and 15, increment the comment_count by 1 # Post.update_counters [10, 15], :comment_count => 1 # # Executes the following SQL: # # UPDATE posts - # # SET comment_count = comment_count + 1, + # # SET comment_count = COALESCE(comment_count, 0) + 1, # # WHERE id IN (10, 15) def update_counters(id, counters) updates = counters.map do |counter_name, value| @@ -73,6 +74,8 @@ module ActiveRecord "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" end + IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled? + update_all(updates.join(', '), primary_key => id ) end diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index 0dc965bd26..b309df9b1b 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -1,45 +1,48 @@ module ActiveRecord # = Active Record Dynamic Finder Match - # + # # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info # class DynamicFinderMatch def self.match(method) - df_match = self.new(method) - df_match.finder ? df_match : nil - end - - def initialize(method) - @finder = :first - @bang = false - @instantiator = nil + finder = :first + bang = false + instantiator = nil case method.to_s - when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/ - @finder = :last if $1 == 'last_by' - @finder = :all if $1 == 'all_by' + when /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ + finder = :last if $1 == 'last_' + finder = :all if $1 == 'all_' names = $2 when /^find_by_([_a-zA-Z]\w*)\!$/ - @bang = true + bang = true names = $1 when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - @instantiator = $1 == 'initialize' ? :new : :create + instantiator = $1 == 'initialize' ? :new : :create names = $2 else - @finder = nil + return nil end - @attribute_names = names && names.split('_and_') + + new(finder, instantiator, bang, names.split('_and_')) + end + + def initialize(finder, instantiator, bang, attribute_names) + @finder = finder + @instantiator = instantiator + @bang = bang + @attribute_names = attribute_names end attr_reader :finder, :attribute_names, :instantiator def finder? - !@finder.nil? && @instantiator.nil? + @finder && !@instantiator end def instantiator? - @finder == :first && !@instantiator.nil? + @finder == :first && @instantiator end def creator? diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb index 15f65be6bc..c832e927d6 100644 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ b/activerecord/lib/active_record/dynamic_scope_match.rb @@ -1,32 +1,23 @@ module ActiveRecord # = Active Record Dynamic Scope Match - # + # # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> # if, for example, the <tt>Product</tt> has an attribute with that name. You can # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named # scope except that it's dynamic. class DynamicScopeMatch def self.match(method) - ds_match = self.new(method) - ds_match.scope ? ds_match : nil + return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/ + new(true, $1 && $1.split('_and_')) end - def initialize(method) - @scope = true - case method.to_s - when /^scoped_by_([_a-zA-Z]\w*)$/ - names = $1 - else - @scope = nil - end - @attribute_names = names && names.split('_and_') + def initialize(scope, attribute_names) + @scope = scope + @attribute_names = attribute_names end attr_reader :scope, :attribute_names - - def scope? - !@scope.nil? - end + alias :scope? :scope end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index e9ac5516ec..ea1709cb1f 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -30,7 +30,7 @@ module ActiveRecord class SerializationTypeMismatch < ActiveRecordError end - # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> + # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> # misses adapter field). class AdapterNotSpecified < ActiveRecordError end @@ -39,7 +39,7 @@ module ActiveRecord class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for example when <tt>connection=</tt> + # Raised when connection to the database could not been established (for example when <tt>connection=</tt> # is given a nil object). class ConnectionNotEstablished < ActiveRecordError end @@ -53,7 +53,7 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError end - # Raised when SQL statement cannot be executed by the database (for example, it's often the case for + # Raised when SQL statement cannot be executed by the database (for example, it's often the case for # MySQL when Ruby driver used is too old). class StatementInvalid < ActiveRecordError end @@ -81,7 +81,7 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end - # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, + # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, # when using +find+ method) # does not match number of expected variables. # diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index e44102b538..d523c643ba 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,4 +1,10 @@ require 'erb' + +begin + require 'psych' +rescue LoadError +end + require 'yaml' require 'csv' require 'zlib' @@ -6,15 +12,7 @@ require 'active_support/dependencies' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/logger' - -if RUBY_VERSION < '1.9' - module YAML #:nodoc: - class Omap #:nodoc: - def keys; map { |k, v| k } end - def values; map { |k, v| v } end - end - end -end +require 'active_support/ordered_hash' if defined? ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -24,6 +22,8 @@ else end end +class FixturesFileNotFound < StandardError; end + # Fixtures are a way of organizing data that you want to test against; in short, sample data. # # = Fixture formats @@ -39,9 +39,9 @@ end # This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures # in a non-verbose, human-readable format. It ships with Ruby 1.8.1+. # -# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed -# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is -# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). +# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed +# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is +# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). # The fixture file ends with the <tt>.yml</tt> file extension (Rails example: # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this: # @@ -59,7 +59,7 @@ end # indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing # pleasure. # -# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. +# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. # See http://yaml.org/type/omap.html # for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table. # This is commonly needed for tree structures. Example: @@ -81,7 +81,7 @@ end # (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>). # # The format of this type of fixture file is much more compact than the others, but also a little harder to read by us -# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the +# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the # file is then comprised # of the actual data (1 per line). Here's an example: # @@ -102,15 +102,15 @@ end # # == Single-file fixtures # -# This type of fixture was the original format for Active Record that has since been deprecated in +# This type of fixture was the original format for Active Record that has since been deprecated in # favor of the YAML and CSV formats. -# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) -# to the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically +# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) +# to the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> -- # like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model). # # Each text file placed in this directory represents a "record". Usually these types of fixtures are named without -# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. +# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. # Here's what the above example might look like: # # web_sites/google @@ -137,7 +137,7 @@ end # end # end # -# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, +# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, # so this test will succeed. # The testing environment will automatically load the all fixtures into the database before each test. # To ensure consistent data, the environment deletes the fixtures before running the load. @@ -187,14 +187,14 @@ end # This will create 1000 very simple YAML fixtures. # # Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. -# This is however a feature to be used with some caution. The point of fixtures are that they're -# stable units of predictable sample data. If you feel that you need to inject dynamic values, then -# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values +# This is however a feature to be used with some caution. The point of fixtures are that they're +# stable units of predictable sample data. If you feel that you need to inject dynamic values, then +# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values # in fixtures are to be considered a code smell. # # = Transactional fixtures # -# TestCases can use begin+rollback to isolate their changes to the database instead of having to +# TestCases can use begin+rollback to isolate their changes to the database instead of having to # delete+insert for every test case. # # class FooTest < ActiveSupport::TestCase @@ -212,17 +212,17 @@ end # end # # If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, -# then you may omit all fixtures declarations in your test cases since all the data's already there +# then you may omit all fixtures declarations in your test cases since all the data's already there # and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide -# access to fixture data for every table that has been loaded through fixtures (depending on the +# access to fixture data for every table that has been loaded through fixtures (depending on the # value of +use_instantiated_fixtures+) # # When *not* to use transactional fixtures: # -# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until -# all parent transactions commit, particularly, the fixtures transaction which is begun in setup +# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until +# all parent transactions commit, particularly, the fixtures transaction which is begun in setup # and rolled back in teardown. Thus, you won't be able to verify # the results of your transaction until Active Record supports nested transactions or savepoints (in progress). # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. @@ -444,20 +444,23 @@ end # # Any fixture labeled "DEFAULTS" is safely ignored. -class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) +class Fixtures MAX_ID = 2 ** 30 - 1 - DEFAULT_FILTER_RE = /\.ya?ml$/ - @@all_cached_fixtures = {} + @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.reset_cache(connection = nil) - connection ||= ActiveRecord::Base.connection - @@all_cached_fixtures[connection.object_id] = {} + def self.find_table_name(table_name) # :nodoc: + ActiveRecord::Base.pluralize_table_names ? + table_name.to_s.singularize.camelize : + table_name.to_s.camelize + end + + def self.reset_cache + @@all_cached_fixtures.clear end def self.cache_for_connection(connection) - @@all_cached_fixtures[connection.object_id] ||= {} - @@all_cached_fixtures[connection.object_id] + @@all_cached_fixtures[connection] end def self.fixture_is_cached?(connection, table_name) @@ -466,27 +469,23 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) def self.cached_fixtures(connection, keys_to_fetch = nil) if keys_to_fetch - fixtures = cache_for_connection(connection).values_at(*keys_to_fetch) + cache_for_connection(connection).values_at(*keys_to_fetch) else - fixtures = cache_for_connection(connection).values + cache_for_connection(connection).values end - fixtures.size > 1 ? fixtures : fixtures.first end def self.cache_fixtures(connection, fixtures_map) cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true) - object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures + def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) if load_instances - ActiveRecord::Base.silence do - fixtures.each do |name, fixture| - begin - object.instance_variable_set "@#{name}", fixture.find - rescue FixtureClassNotFound - nil - end + fixtures.each do |name, fixture| + begin + object.instance_variable_set "@#{name}", fixture.find + rescue FixtureClassNotFound + nil end end end @@ -503,36 +502,56 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) def self.create_fixtures(fixtures_directory, table_names, class_names = {}) table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') } - connection = block_given? ? yield : ActiveRecord::Base.connection + table_names.each { |n| + class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') + } - table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } + # FIXME: Apparently JK uses this. + connection = block_given? ? yield : ActiveRecord::Base.connection - unless table_names_to_fetch.empty? - ActiveRecord::Base.silence do - connection.disable_referential_integrity do - fixtures_map = {} + files_to_read = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } - fixtures = table_names_to_fetch.map do |table_name| - fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) - end + unless files_to_read.empty? + connection.disable_referential_integrity do + fixtures_map = {} + + fixture_files = files_to_read.map do |path| + table_name = path.tr '/', '_' + + fixtures_map[path] = Fixtures.new( + connection, + table_name, + class_names[table_name.to_sym], + File.join(fixtures_directory, path)) + end + + all_loaded_fixtures.update(fixtures_map) - all_loaded_fixtures.update(fixtures_map) + connection.transaction(:requires_new => true) do + fixture_files.each do |ff| + conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection + table_rows = ff.table_rows - connection.transaction(:requires_new => true) do - fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } - fixtures.each { |fixture| fixture.insert_fixtures } + table_rows.keys.each do |table| + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) + table_rows.each do |table_name,rows| + rows.each do |row| + conn.insert_fixture(row, table_name) end end end - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) + table_names.each do |table_name| + connection.reset_pk_sequence!(table_name.tr('/', '_')) + end + end end + + cache_fixtures(connection, fixtures_map) end end cached_fixtures(connection, table_names) @@ -544,40 +563,59 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) Zlib.crc32(label.to_s) % MAX_ID end - attr_reader :table_name, :name + attr_reader :table_name, :name, :fixtures, :model_class + + def initialize(connection, table_name, class_name, fixture_path) + @connection = connection + @table_name = table_name + @fixture_path = fixture_path + @name = table_name # preserve fixture base name + @class_name = class_name + + @fixtures = ActiveSupport::OrderedHash.new + @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" + + # Should be an AR::Base type class + if class_name.is_a?(Class) + @table_name = class_name.table_name + @connection = class_name.connection + @model_class = class_name + else + @model_class = class_name.constantize rescue nil + end - def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) - @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter - @name = table_name # preserve fixture base name - @class_name = class_name || - (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize) - @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" - @table_name = class_name.table_name if class_name.respond_to?(:table_name) - @connection = class_name.connection if class_name.respond_to?(:connection) read_fixture_files end - def delete_existing_fixtures - @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete' + def [](x) + fixtures[x] + end + + def []=(k,v) + fixtures[k] = v + end + + def each(&block) + fixtures.each(&block) + end + + def size + fixtures.size end - def insert_fixtures + # Return a hash of rows to be inserted. The key is the table, the value is + # a list of rows to insert to that table. + def table_rows now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML - if is_a?(Hash) - delete('DEFAULTS') - else - delete(assoc('DEFAULTS')) - end + fixtures.delete('DEFAULTS') # track any join tables we need to insert later - habtm_fixtures = Hash.new do |h, habtm| - h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil) - end + rows = Hash.new { |h,table| h[table] = [] } - each do |label, fixture| + rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash if model_class && model_class < ActiveRecord::Base @@ -613,14 +651,9 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] - if value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - target_type = $1 - target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s - - # support polymorphic belongs_to as "label (Type)" - row[target_type_name] = target_type - end + if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 end row[fk_name] = Fixtures.identify(value) @@ -628,47 +661,22 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - join_fixtures = habtm_fixtures[association] - - targets.each do |target| - join_fixtures["#{label}_#{target}"] = Fixture.new( - { association.primary_key_name => row[primary_key_name], - association.association_foreign_key => Fixtures.identify(target) }, - nil, @connection) - end + table_name = association.options[:join_table] + rows[table_name].concat targets.map { |target| + { association.foreign_key => row[primary_key_name], + association.association_foreign_key => Fixtures.identify(target) } + } end end end end - @connection.insert_fixture(fixture, @table_name) - end - - # insert any HABTM join tables we discovered - habtm_fixtures.values.each do |fixture| - fixture.delete_existing_fixtures - fixture.insert_fixtures + row end + rows end private - class HabtmFixtures < ::Fixtures #:nodoc: - def read_fixture_files; end - end - - def model_class - unless defined?(@model_class) - @model_class = - if @class_name.nil? || @class_name.is_a?(Class) - @class_name - else - @class_name.constantize rescue nil - end - end - - @model_class - end - def primary_key_name @primary_key_name ||= model_class && model_class.primary_key end @@ -688,7 +696,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) end def column_names - @column_names ||= @connection.columns(@table_name).collect(&:name) + @column_names ||= @connection.columns(@table_name).collect { |c| c.name } end def read_fixture_files @@ -696,15 +704,15 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) read_yaml_fixture_files elsif File.file?(csv_file_path) read_csv_fixture_files + else + raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}" end end def read_yaml_fixture_files - yaml_string = "" - Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path| - yaml_string << IO.read(subfixture_path) - end - yaml_string << IO.read(yaml_file_path) + yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f| + File.file?(f) + } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join if yaml = parse_yaml_string(yaml_string) # If the file is an ordered map, extract its children. @@ -722,7 +730,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" end - self[name] = Fixture.new(data, model_class, @connection) + fixtures[name] = Fixture.new(data, model_class) end end end @@ -735,7 +743,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) reader.each do |row| data = {} row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } - self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection) + fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class) end end @@ -771,44 +779,30 @@ class Fixture #:nodoc: class FormatError < FixtureError #:nodoc: end - attr_reader :model_class + attr_reader :model_class, :fixture - def initialize(fixture, model_class, connection = ActiveRecord::Base.connection) - @connection = connection - @fixture = fixture - @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil + def initialize(fixture, model_class) + @fixture = fixture + @model_class = model_class end def class_name - @model_class.name if @model_class + model_class.name if model_class end def each - @fixture.each { |item| yield item } + fixture.each { |item| yield item } end def [](key) - @fixture[key] - end - - def to_hash - @fixture + fixture[key] end - def key_list - @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ') - end - - def value_list - cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {} - @fixture.map do |key, value| - @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r") - end.join(', ') - end + alias :to_hash :fixture def find if model_class - model_class.find(self[model_class.primary_key]) + model_class.find(fixture[model_class.primary_key]) else raise FixtureClassNotFound, "No class attached to find." end @@ -823,19 +817,21 @@ module ActiveRecord setup :setup_fixtures teardown :teardown_fixtures - superclass_delegating_accessor :fixture_path - superclass_delegating_accessor :fixture_table_names - superclass_delegating_accessor :fixture_class_names - superclass_delegating_accessor :use_transactional_fixtures - superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances - superclass_delegating_accessor :pre_loaded_fixtures + class_attribute :fixture_path + class_attribute :fixture_table_names + class_attribute :fixture_class_names + class_attribute :use_transactional_fixtures + class_attribute :use_instantiated_fixtures # true, false, or :no_instances + class_attribute :pre_loaded_fixtures self.fixture_table_names = [] self.use_transactional_fixtures = true self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = {} + self.fixture_class_names = Hash.new do |h, table_name| + h[table_name] = Fixtures.find_table_name(table_name) + end end module ClassMethods @@ -843,17 +839,17 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names) end - def fixtures(*table_names) - if table_names.first == :all - table_names = Dir["#{fixture_path}/**/*.{yml,csv}"] - table_names.map! { |f| f[(fixture_path.size + 1)..-5] } + def fixtures(*fixture_names) + if fixture_names.first == :all + fixture_names = Dir["#{fixture_path}/**/*.{yml,csv}"] + fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] } else - table_names = table_names.flatten.map { |n| n.to_s } + fixture_names = fixture_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= table_names - require_fixture_classes(table_names) - setup_fixture_accessors(table_names) + self.fixture_table_names |= fixture_names + require_fixture_classes(fixture_names) + setup_fixture_accessors(fixture_names) end def try_to_load_dependency(file_name) @@ -868,43 +864,48 @@ module ActiveRecord end end - def require_fixture_classes(table_names = nil) - (table_names || fixture_table_names).each do |table_name| - file_name = table_name.to_s + def require_fixture_classes(fixture_names = nil) + (fixture_names || fixture_table_names).each do |fixture_name| + file_name = fixture_name.to_s file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(table_names = nil) - table_names = Array.wrap(table_names || fixture_table_names) - table_names.each do |table_name| - table_name = table_name.to_s.tr('./', '_') + def setup_fixture_accessors(fixture_names = nil) + fixture_names = Array.wrap(fixture_names || fixture_table_names) + methods = Module.new do + fixture_names.each do |fixture_name| + fixture_name = fixture_name.to_s.tr('./', '_') - redefine_method(table_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(fixture_name) do |*fixtures| + force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload - @fixture_cache[table_name] ||= {} + @fixture_cache[fixture_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[table_name].delete(fixture) if force_reload + instances = fixtures.map do |fixture| + @fixture_cache[fixture_name].delete(fixture) if force_reload - if @loaded_fixtures[table_name][fixture.to_s] - @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find - else - raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'" + if @loaded_fixtures[fixture_name][fixture.to_s] + ActiveRecord::IdentityMap.without do + @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find + end + else + raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'" + end end - end - instances.size == 1 ? instances.first : instances + instances.size == 1 ? instances.first : instances + end + private fixture_name end - private table_name end + include methods end def uses_transaction(*methods) @uses_transaction = [] unless defined?(@uses_transaction) - @uses_transaction.concat methods.map(&:to_s) + @uses_transaction.concat methods.map { |m| m.to_s } end def uses_transaction?(method) @@ -919,7 +920,7 @@ module ActiveRecord end def setup_fixtures - return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? + return unless !ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' @@ -933,7 +934,7 @@ module ActiveRecord if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - load_fixtures + @loaded_fixtures = load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end ActiveRecord::Base.connection.increment_open_transactions @@ -943,7 +944,7 @@ module ActiveRecord else Fixtures.reset_cache @@already_loaded_fixtures[self.class] = nil - load_fixtures + @loaded_fixtures = load_fixtures end # Instantiate fixtures for every test if requested. @@ -967,15 +968,8 @@ module ActiveRecord private def load_fixtures - @loaded_fixtures = {} fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) - unless fixtures.nil? - if fixtures.instance_of?(Fixtures) - @loaded_fixtures[fixtures.name] = fixtures - else - fixtures.each { |f| @loaded_fixtures[f.name] = f } - end - end + Hash[fixtures.map { |f| [f.name, f] }] end # for pre_loaded_fixtures, only require the classes once. huge speed improvement @@ -991,8 +985,8 @@ module ActiveRecord Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? - @loaded_fixtures.each do |table_name, fixtures| - Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?) + @loaded_fixtures.each do |fixture_name, fixtures| + Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) end end end diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb new file mode 100644 index 0000000000..d18b2b0a54 --- /dev/null +++ b/activerecord/lib/active_record/identity_map.rb @@ -0,0 +1,102 @@ +module ActiveRecord + # = Active Record Identity Map + # + # Ensures that each object gets loaded only once by keeping every loaded + # object in a map. Looks up objects using the map when referring to them. + # + # More information on Identity Map pattern: + # http://www.martinfowler.com/eaaCatalog/identityMap.html + # + # == Configuration + # + # In order to enable IdentityMap, set <tt>config.active_record.identity_map = true</tt> + # in your <tt>config/application.rb</tt> file. + # + # IdentityMap is disabled by default. + # + module IdentityMap + extend ActiveSupport::Concern + + class << self + def enabled=(flag) + Thread.current[:identity_map_enabled] = flag + end + + def enabled + Thread.current[:identity_map_enabled] + end + alias enabled? enabled + + def repository + Thread.current[:identity_map] ||= Hash.new { |h,k| h[k] = {} } + end + + def use + old, self.enabled = enabled, true + + yield if block_given? + ensure + self.enabled = old + clear + end + + def without + old, self.enabled = enabled, false + + yield if block_given? + ensure + self.enabled = old + end + + def get(klass, primary_key) + obj = repository[klass.symbolized_base_class][primary_key] + obj.is_a?(klass) ? obj : nil + end + + def add(record) + repository[record.class.symbolized_base_class][record.id] = record + end + + def remove(record) + repository[record.class.symbolized_base_class].delete(record.id) + end + + def remove_by_id(symbolized_base_class, id) + repository[symbolized_base_class].delete(id) + end + + def clear + repository.clear + end + end + + # Reinitialize an Identity Map model object from +coder+. + # +coder+ must contain the attributes necessary for initializing an empty + # model object. + def reinit_with(coder) + @attributes_cache = {} + dirty = @changed_attributes.keys + @attributes.update(coder['attributes'].except(*dirty)) + @changed_attributes.update(coder['attributes'].slice(*dirty)) + @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} + + set_serialized_attributes + + run_callbacks :find + + self + end + + class Middleware + def initialize(app) + @app = app + end + + def call(env) + ActiveRecord::IdentityMap.use do + @app.call(env) + end + end + end + end +end diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index a0e94cbec1..44328f63b6 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -22,7 +22,7 @@ en: # attributes: # login: # blank: "This is a custom blank message for User login" - # Will define custom blank validation message for User model and + # Will define custom blank validation message for User model and # custom blank validation message for login attribute of User model. #models: @@ -31,7 +31,7 @@ en: # For example, # user: "Dude" # will translate User model name to "Dude" - + # Translate model attribute names. Used in Model.human_attribute_name(attribute). #attributes: # For example, diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index b6f87a57b8..9a31675782 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -23,7 +23,7 @@ module ActiveRecord # p2.first_name = "should fail" # p2.save # Raises a ActiveRecord::StaleObjectError # - # Optimistic locking will also check for stale data when objects are destroyed. Example: + # Optimistic locking will also check for stale data when objects are destroyed. Example: # # p1 = Person.find(1) # p2 = Person.find(1) @@ -58,6 +58,12 @@ module ActiveRecord end private + def increment_lock + lock_col = self.class.locking_column + previous_lock_value = send(lock_col).to_i + send(lock_col + '=', previous_lock_value + 1) + end + def attributes_from_column_definition result = super @@ -70,7 +76,7 @@ module ActiveRecord result[self.class.locking_column] ||= 0 end - return result + result end def update(attribute_names = @attributes.keys) #:nodoc: @@ -78,8 +84,8 @@ module ActiveRecord return 0 if attribute_names.empty? lock_col = self.class.locking_column - previous_value = send(lock_col).to_i - send(lock_col + '=', previous_value + 1) + previous_lock_value = send(lock_col).to_i + increment_lock attribute_names += [lock_col] attribute_names.uniq! @@ -87,11 +93,13 @@ module ActiveRecord begin relation = self.class.unscoped - affected_rows = relation.where( + stmt = relation.where( relation.table[self.class.primary_key].eq(quoted_id).and( - relation.table[self.class.locking_column].eq(quote_value(previous_value)) + relation.table[lock_col].eq(quote_value(previous_lock_value)) ) - ).arel.update(arel_attributes_values(false, false, attribute_names)) + ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) + + affected_rows = connection.update stmt.to_sql unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" @@ -101,7 +109,7 @@ module ActiveRecord # If something went wrong, revert the version. rescue Exception - send(lock_col + '=', previous_value) + send(lock_col + '=', previous_lock_value) raise end end @@ -109,13 +117,11 @@ module ActiveRecord def destroy #:nodoc: return super unless locking_enabled? - unless new_record? - lock_col = self.class.locking_column - previous_value = send(lock_col).to_i - + if persisted? table = self.class.arel_table - predicate = table[self.class.primary_key].eq(id) - predicate = predicate.and(table[self.class.locking_column].eq(previous_value)) + lock_col = self.class.locking_column + predicate = table[self.class.primary_key].eq(id). + and(table[lock_col].eq(send(lock_col).to_i)) affected_rows = self.class.unscoped.where(predicate).delete_all diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index fcc9ebb4af..862cf8f72a 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -9,13 +9,12 @@ module ActiveRecord # Account.find(1, :lock => true) # # Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause - # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. + # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # - # Example: # Account.transaction do # # select * from accounts where name = 'shugo' limit 1 for update - # shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true) - # yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true) + # shugo = Account.where("name = 'shugo'").lock(true).first + # yuko = Account.where("name = 'shugo'").lock(true).first # shugo.balance -= 100 # shugo.save! # yuko.balance += 100 @@ -24,9 +23,10 @@ module ActiveRecord # # You can also use ActiveRecord::Base#lock! method to lock one record by id. # This may be better if you don't need to lock every row. Example: + # # Account.transaction do # # select * from accounts where ... - # accounts = Account.find(:all, :conditions => ...) + # accounts = Account.where(...).all # account1 = accounts.detect { |account| ... } # account2 = accounts.detect { |account| ... } # # select * from accounts where id=? for update @@ -40,14 +40,14 @@ module ActiveRecord # # Database-specific information on row locking: # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html - # PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE + # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested # lock. Pass an SQL locking clause to append the end of the SELECT statement - # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns + # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns # the locked record. def lock!(lock = true) - reload(:lock => lock) unless new_record? + reload(:lock => lock) if persisted? self end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index c7ae12977a..afadbf03ef 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -22,8 +22,16 @@ module ActiveRecord self.class.runtime += event.duration return unless logger.debug? - name = '%s (%.1fms)' % [event.payload[:name], event.duration] - sql = event.payload[:sql].squeeze(' ') + payload = event.payload + name = '%s (%.1fms)' % [payload[:name], event.duration] + sql = payload[:sql].squeeze(' ') + binds = nil + + unless (payload[:binds] || []).empty? + binds = " " + payload[:binds].map { |col,v| + [col.name, v] + }.inspect + end if odd? name = color(name, CYAN, true) @@ -32,7 +40,7 @@ module ActiveRecord name = color(name, MAGENTA, true) end - debug " #{name} #{sql}" + debug " #{name} #{sql}#{binds}" end def odd? @@ -45,4 +53,4 @@ module ActiveRecord end end -ActiveRecord::LogSubscriber.attach_to :active_record
\ No newline at end of file +ActiveRecord::LogSubscriber.attach_to :active_record diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5e272f0ba4..640111096d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,4 @@ -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/aliasing' +require "active_support/core_ext/array/wrap" module ActiveRecord # Exception that can be raised to stop migrations from going backwards. @@ -31,39 +30,39 @@ module ActiveRecord end # = Active Record Migrations - # - # Migrations can manage the evolution of a schema used by several physical + # + # Migrations can manage the evolution of a schema used by several physical # databases. It's a solution to the common problem of adding a field to make # a new feature work in your local database, but being unsure of how to - # push that change to other developers and to the production server. With + # push that change to other developers and to the production server. With # migrations, you can describe the transformations in self-contained classes - # that can be checked into version control systems and executed against + # that can be checked into version control systems and executed against # another database that might be one, two, or five versions behind. # # Example of a simple migration: # # class AddSsl < ActiveRecord::Migration - # def self.up + # def up # add_column :accounts, :ssl_enabled, :boolean, :default => 1 # end # - # def self.down + # def down # remove_column :accounts, :ssl_enabled # end # end # - # This migration will add a boolean flag to the accounts table and remove it - # if you're backing out of the migration. It shows how all migrations have - # two class methods +up+ and +down+ that describes the transformations + # This migration will add a boolean flag to the accounts table and remove it + # if you're backing out of the migration. It shows how all migrations have + # two class methods +up+ and +down+ that describes the transformations # required to implement or remove the migration. These methods can consist # of both the migration specific methods like add_column and remove_column, - # but may also contain regular Ruby code for generating data needed for the + # but may also contain regular Ruby code for generating data needed for the # transformations. # # Example of a more complex migration that also needs to initialize data: # # class AddSystemSettings < ActiveRecord::Migration - # def self.up + # def up # create_table :system_settings do |t| # t.string :name # t.string :label @@ -72,58 +71,58 @@ module ActiveRecord # t.integer :position # end # - # SystemSetting.create :name => "notice", - # :label => "Use notice?", + # SystemSetting.create :name => "notice", + # :label => "Use notice?", # :value => 1 # end # - # def self.down + # def down # drop_table :system_settings # end # end # - # This migration first adds the system_settings table, then creates the very + # This migration first adds the system_settings table, then creates the very # first row in it using the Active Record model that relies on the table. It - # also uses the more advanced create_table syntax where you can specify a + # also uses the more advanced create_table syntax where you can specify a # complete table schema in one block call. # # == Available transformations # - # * <tt>create_table(name, options)</tt> Creates a table called +name+ and + # * <tt>create_table(name, options)</tt> Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, - # following the same format as add_column. See example above. The options hash - # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create + # following the same format as add_column. See example above. The options hash + # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. # * <tt>drop_table(name)</tt>: Drops the table called +name+. - # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ # to +new_name+. - # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column + # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column # to the table called +table_name+ # named +column_name+ specified to be one of the following types: - # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, + # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be - # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. - # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. - # <tt>{ :limit => 50, :null => false }</tt>) -- see + # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. + # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. + # <tt>{ :limit => 50, :null => false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames # a column but keeps the type and content. - # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named + # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named # +column_name+ from the table called +table_name+. - # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index + # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include - # <tt>:name</tt> and <tt>:unique</tt> (e.g. + # <tt>:name</tt> and <tt>:unique</tt> (e.g. # <tt>{ :name => "users_name_index", :unique => true }</tt>). - # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified + # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified # by +index_name+. # # == Irreversible transformations # - # Some transformations are destructive in a manner that cannot be reversed. - # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt> + # Some transformations are destructive in a manner that cannot be reversed. + # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt> # exception in their +down+ method. # # == Running migrations from within Rails @@ -134,11 +133,11 @@ module ActiveRecord # rails generate migration MyNewMigration # # where MyNewMigration is the name of your migration. The generator will - # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> - # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the + # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> + # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the # UTC formatted date and time that the migration was generated. # - # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of + # You may then edit the <tt>up</tt> and <tt>down</tt> methods of # MyNewMigration. # # There is a special syntactic shortcut to generate migrations that add fields to a table. @@ -147,11 +146,11 @@ module ActiveRecord # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration - # def self.up + # def up # add_column :tablenames, :fieldname, :string # end # - # def self.down + # def down # remove_column :tablenames, :fieldname # end # end @@ -179,11 +178,11 @@ module ActiveRecord # Not all migrations change the schema. Some just fix the data: # # class RemoveEmptyTags < ActiveRecord::Migration - # def self.up + # def up # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? } # end # - # def self.down + # def down # # not much we can do to restore deleted data # raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags" # end @@ -192,12 +191,12 @@ module ActiveRecord # Others remove columns when they migrate up instead of down: # # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration - # def self.up + # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count # end # - # def self.down + # def down # add_column :items, :incomplete_items_count # add_column :items, :completed_items_count # end @@ -206,24 +205,24 @@ module ActiveRecord # And sometimes you need to do something in SQL not abstracted directly by migrations: # # class MakeJoinUnique < ActiveRecord::Migration - # def self.up + # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end # - # def self.down + # def down # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`" # end # end # # == Using a model after changing its table # - # Sometimes you'll want to add a column in a migration and populate it - # immediately after. In that case, you'll need to make a call to - # <tt>Base#reset_column_information</tt> in order to ensure that the model has the + # Sometimes you'll want to add a column in a migration and populate it + # immediately after. In that case, you'll need to make a call to + # <tt>Base#reset_column_information</tt> in order to ensure that the model has the # latest column data from after the new column was added. Example: # # class AddPeopleSalary < ActiveRecord::Migration - # def self.up + # def up # add_column :people, :salary, :integer # Person.reset_column_information # Person.find(:all).each do |p| @@ -243,7 +242,7 @@ module ActiveRecord # You can also insert your own messages and benchmarks by using the +say_with_time+ # method: # - # def self.up + # def up # ... # say_with_time "Updating salaries..." do # Person.find(:all).each do |p| @@ -286,111 +285,216 @@ module ActiveRecord # # In application.rb. # + # == Reversible Migrations + # + # Starting with Rails 3.1, you will be able to define reversible migrations. + # Reversible migrations are migrations that know how to go +down+ for you. + # You simply supply the +up+ logic, and the Migration system will figure out + # how to execute the down commands for you. + # + # To define a reversible migration, define the +change+ method in your + # migration like this: + # + # class TenderloveMigration < ActiveRecord::Migration + # def change + # create_table(:horses) do + # t.column :content, :text + # t.column :remind_at, :datetime + # end + # end + # end + # + # This migration will create the horses table for you on the way up, and + # automatically figure out how to drop the table on the way down. + # + # Some commands like +remove_column+ cannot be reversed. If you care to + # define how to move up and down in these cases, you should define the +up+ + # and +down+ methods as before. + # + # If a command cannot be reversed, an + # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when + # the migration is moving down. + # + # For a list of commands that are reversible, please see + # <tt>ActiveRecord::Migration::CommandRecorder</tt>. class Migration - @@verbose = true - cattr_accessor :verbose + autoload :CommandRecorder, 'active_record/migration/command_recorder' class << self - def up_with_benchmarks #:nodoc: - migrate(:up) - end - - def down_with_benchmarks #:nodoc: - migrate(:down) - end + attr_accessor :delegate # :nodoc: + end - # Execute this migration in the named direction - def migrate(direction) - return unless respond_to?(direction) + def self.method_missing(name, *args, &block) # :nodoc: + (delegate || superclass.delegate).send(name, *args, &block) + end - case direction - when :up then announce "migrating" - when :down then announce "reverting" - end + cattr_accessor :verbose - result = nil - time = Benchmark.measure { result = send("#{direction}_without_benchmarks") } + attr_accessor :name, :version - case direction - when :up then announce "migrated (%.4fs)" % time.real; write - when :down then announce "reverted (%.4fs)" % time.real; write - end + def initialize + @name = self.class.name + @version = nil + @connection = nil + end - result - end + # instantiate the delegate object after initialize is defined + self.verbose = true + self.delegate = new - # Because the method added may do an alias_method, it can be invoked - # recursively. We use @ignore_new_methods as a guard to indicate whether - # it is safe for the call to proceed. - def singleton_method_added(sym) #:nodoc: - return if defined?(@ignore_new_methods) && @ignore_new_methods + def up + self.class.delegate = self + return unless self.class.respond_to?(:up) + self.class.up + end - begin - @ignore_new_methods = true + def down + self.class.delegate = self + return unless self.class.respond_to?(:down) + self.class.down + end - case sym - when :up, :down - singleton_class.send(:alias_method_chain, sym, "benchmarks") + # Execute this migration in the named direction + def migrate(direction) + return unless respond_to?(direction) + + case direction + when :up then announce "migrating" + when :down then announce "reverting" + end + + time = nil + ActiveRecord::Base.connection_pool.with_connection do |conn| + @connection = conn + if respond_to?(:change) + if direction == :down + recorder = CommandRecorder.new(@connection) + suppress_messages do + @connection = recorder + change + end + @connection = conn + time = Benchmark.measure { + recorder.inverse.each do |cmd, args| + send(cmd, *args) + end + } + else + time = Benchmark.measure { change } end - ensure - @ignore_new_methods = false + else + time = Benchmark.measure { send(direction) } end + @connection = nil end - def write(text="") - puts(text) if verbose + case direction + when :up then announce "migrated (%.4fs)" % time.real; write + when :down then announce "reverted (%.4fs)" % time.real; write end + end - def announce(message) - version = defined?(@version) ? @version : nil + def write(text="") + puts(text) if verbose + end - text = "#{version} #{name}: #{message}" - length = [0, 75 - text.length].max - write "== %s %s" % [text, "=" * length] - end + def announce(message) + text = "#{version} #{name}: #{message}" + length = [0, 75 - text.length].max + write "== %s %s" % [text, "=" * length] + end - def say(message, subitem=false) - write "#{subitem ? " ->" : "--"} #{message}" - end + def say(message, subitem=false) + write "#{subitem ? " ->" : "--"} #{message}" + end - def say_with_time(message) - say(message) - result = nil - time = Benchmark.measure { result = yield } - say "%.4fs" % time.real, :subitem - say("#{result} rows", :subitem) if result.is_a?(Integer) - result - end + def say_with_time(message) + say(message) + result = nil + time = Benchmark.measure { result = yield } + say "%.4fs" % time.real, :subitem + say("#{result} rows", :subitem) if result.is_a?(Integer) + result + end - def suppress_messages - save, self.verbose = verbose, false - yield - ensure - self.verbose = save - end + def suppress_messages + save, self.verbose = verbose, false + yield + ensure + self.verbose = save + end + + def connection + @connection || ActiveRecord::Base.connection + end + + def method_missing(method, *arguments, &block) + arg_list = arguments.map{ |a| a.inspect } * ', ' - def connection - ActiveRecord::Base.connection + say_with_time "#{method}(#{arg_list})" do + unless arguments.empty? || method == :execute + arguments[0] = Migrator.proper_table_name(arguments.first) + end + return super unless connection.respond_to?(method) + connection.send(method, *arguments, &block) end + end + + def copy(destination, sources, options = {}) + copied = [] + + FileUtils.mkdir_p(destination) unless File.exists?(destination) + + destination_migrations = ActiveRecord::Migrator.migrations(destination) + last = destination_migrations.last + sources.each do |name, path| + source_migrations = ActiveRecord::Migrator.migrations(path) - def method_missing(method, *arguments, &block) - arg_list = arguments.map(&:inspect) * ', ' + source_migrations.each do |migration| + source = File.read(migration.filename) + source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" - say_with_time "#{method}(#{arg_list})" do - unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) + if duplicate = destination_migrations.detect { |m| m.name == migration.name } + options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] + next end - connection.send(method, *arguments, &block) + + migration.version = next_migration_number(last ? last.version + 1 : 0).to_i + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + old_path, migration.filename = migration.filename, new_path + last = migration + + FileUtils.cp(old_path, migration.filename) + copied << migration + options[:on_copy].call(name, migration, old_path) if options[:on_copy] + destination_migrations << migration end end + + copied + end + + def next_migration_number(number) + if ActiveRecord::Base.timestamped_migrations + [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max + else + "%.3d" % number + end end end # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy + class MigrationProxy < Struct.new(:name, :version, :filename) - attr_accessor :name, :version, :filename + def initialize(name, version, filename) + super + @migration = nil + end + + def basename + File.basename(filename) + end delegate :migrate, :announce, :write, :to=>:migration @@ -402,47 +506,47 @@ module ActiveRecord def load_migration require(File.expand_path(filename)) - name.constantize + name.constantize.new end end class Migrator#:nodoc: class << self - def migrate(migrations_path, target_version = nil) + attr_writer :migrations_paths + alias :migrations_path= :migrations_paths= + + def migrate(migrations_paths, target_version = nil) case when target_version.nil? - up(migrations_path, target_version) + up(migrations_paths, target_version) when current_version == 0 && target_version == 0 + [] when current_version > target_version - down(migrations_path, target_version) + down(migrations_paths, target_version) else - up(migrations_path, target_version) + up(migrations_paths, target_version) end end - def rollback(migrations_path, steps=1) - move(:down, migrations_path, steps) + def rollback(migrations_paths, steps=1) + move(:down, migrations_paths, steps) end - def forward(migrations_path, steps=1) - move(:up, migrations_path, steps) + def forward(migrations_paths, steps=1) + move(:up, migrations_paths, steps) end - def up(migrations_path, target_version = nil) - self.new(:up, migrations_path, target_version).migrate + def up(migrations_paths, target_version = nil) + self.new(:up, migrations_paths, target_version).migrate end - def down(migrations_path, target_version = nil) - self.new(:down, migrations_path, target_version).migrate + def down(migrations_paths, target_version = nil) + self.new(:down, migrations_paths, target_version).migrate end - def run(direction, migrations_path, target_version) - self.new(direction, migrations_path, target_version).run - end - - def migrations_path - 'db/migrate' + def run(direction, migrations_paths, target_version) + self.new(direction, migrations_paths, target_version).run end def schema_migrations_table_name @@ -451,7 +555,7 @@ module ActiveRecord def get_all_versions table = Arel::Table.new(schema_migrations_table_name) - Base.connection.select_values(table.project(table['version']).to_sql).map(&:to_i).sort + Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort end def current_version @@ -468,24 +572,59 @@ module ActiveRecord name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" end + def migrations_paths + @migrations_paths ||= ['db/migrate'] + # just to not break things if someone uses: migration_path = some_string + Array.wrap(@migrations_paths) + end + + def migrations_path + migrations_paths.first + end + + def migrations(paths) + paths = Array.wrap(paths) + + files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }] + + seen = Hash.new false + + migrations = files.map do |file| + version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first + + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + raise DuplicateMigrationVersionError.new(version) if seen[version] + raise DuplicateMigrationNameError.new(name) if seen[name] + + seen[version] = seen[name] = true + + MigrationProxy.new(name, version, file) + end + + migrations.sort_by(&:version) + end + private - def move(direction, migrations_path, steps) - migrator = self.new(direction, migrations_path) + def move(direction, migrations_paths, steps) + migrator = self.new(direction, migrations_paths) start_index = migrator.migrations.index(migrator.current_migration) if start_index finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_path, version) + send(direction, migrations_paths, version) end end end - def initialize(direction, migrations_path, target_version = nil) + def initialize(direction, migrations_paths, target_version = nil) raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? Base.connection.initialize_schema_migrations_table - @direction, @migrations_path, @target_version = direction, migrations_path, target_version + @direction, @migrations_paths, @target_version = direction, migrations_paths, target_version end def current_version @@ -509,7 +648,7 @@ module ActiveRecord current = migrations.detect { |m| m.version == current_version } target = migrations.detect { |m| m.version == @target_version } - if target.nil? && !@target_version.nil? && @target_version > 0 + if target.nil? && @target_version && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end @@ -518,16 +657,19 @@ module ActiveRecord runnable = migrations[start..finish] # skip the last migration if we're headed down, but not ALL the way down - runnable.pop if down? && !target.nil? + runnable.pop if down? && target + ran = [] runnable.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + seen = migrated.include?(migration.version.to_i) + # On our way up, we skip migrating the ones we've already migrated - next if up? && migrated.include?(migration.version.to_i) + next if up? && seen # On our way down, we skip reverting the ones we've never migrated - if down? && !migrated.include?(migration.version.to_i) + if down? && !seen migration.announce 'never migrated, skipping'; migration.write next end @@ -537,39 +679,18 @@ module ActiveRecord migration.migrate(@direction) record_version_state_after_migrating(migration.version) end + ran << migration rescue => e canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end + ran end def migrations @migrations ||= begin - files = Dir["#{@migrations_path}/[0-9]*_*.rb"] - - migrations = files.inject([]) do |klasses, file| - version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first - - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - - if klasses.detect { |m| m.version == version } - raise DuplicateMigrationVersionError.new(version) - end - - if klasses.detect { |m| m.name == name.camelize } - raise DuplicateMigrationNameError.new(name.camelize) - end - - migration = MigrationProxy.new - migration.name = name.camelize - migration.version = version - migration.filename = file - klasses << migration - end - - migrations = migrations.sort_by(&:version) + migrations = self.class.migrations(@migrations_paths) down? ? migrations.reverse : migrations end end @@ -590,10 +711,12 @@ module ActiveRecord @migrated_versions ||= [] if down? @migrated_versions.delete(version) - table.where(table["version"].eq(version.to_s)).delete + stmt = table.where(table["version"].eq(version.to_s)).compile_delete + Base.connection.delete stmt.to_sql else @migrated_versions.push(version).sort! - table.insert table["version"] => version.to_s + stmt = table.compile_insert table["version"] => version.to_s + Base.connection.insert stmt.to_sql end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb new file mode 100644 index 0000000000..c9d57ce812 --- /dev/null +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -0,0 +1,103 @@ +module ActiveRecord + class Migration + # ActiveRecord::Migration::CommandRecorder records commands done during + # a migration and knows how to reverse those commands. The CommandRecorder + # knows how to invert the following commands: + # + # * add_column + # * add_index + # * add_timestamp + # * create_table + # * remove_timestamps + # * rename_column + # * rename_index + # * rename_table + class CommandRecorder + attr_accessor :commands, :delegate + + def initialize(delegate = nil) + @commands = [] + @delegate = delegate + end + + # record +command+. +command+ should be a method name and arguments. + # For example: + # + # recorder.record(:method_name, [:arg1, arg2]) + def record(*command) + @commands << command + end + + # Returns a list that represents commands that are the inverse of the + # commands stored in +commands+. For example: + # + # recorder.record(:rename_table, [:old, :new]) + # recorder.inverse # => [:rename_table, [:new, :old]] + # + # This method will raise an IrreversibleMigration exception if it cannot + # invert the +commands+. + def inverse + @commands.reverse.map { |name, args| + method = :"invert_#{name}" + raise IrreversibleMigration unless respond_to?(method, true) + send(method, args) + } + end + + def respond_to?(*args) # :nodoc: + super || delegate.respond_to?(*args) + end + + [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + class_eval <<-EOV, __FILE__, __LINE__ + 1 + def #{method}(*args) + record(:"#{method}", args) + end + EOV + end + + private + + def invert_create_table(args) + [:drop_table, args] + end + + def invert_rename_table(args) + [:rename_table, args.reverse] + end + + def invert_add_column(args) + [:remove_column, args.first(2)] + end + + def invert_rename_index(args) + [:rename_index, args.reverse] + end + + def invert_rename_column(args) + [:rename_column, [args.first] + args.last(2).reverse] + end + + def invert_add_index(args) + table, columns, _ = *args + [:remove_index, [table, {:column => columns}]] + end + + def invert_remove_timestamps(args) + [:add_timestamps, args] + end + + def invert_add_timestamps(args) + [:remove_timestamps, args] + end + + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + @delegate.send(method, *args, &block) + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}") + end + + end + end +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 0e560418dc..d291632260 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -2,12 +2,18 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Named \Scopes module NamedScope extend ActiveSupport::Concern + included do + class_attribute :scopes + self.scopes = {} + end + module ClassMethods # Returns an anonymous \scope. # @@ -20,10 +26,10 @@ module ActiveRecord # fruits = fruits.limit(10) if limited? # # Anonymous \scopes tend to be useful when procedurally generating complex - # queries, where passing intermediate values (\scopes) around as first-class + # queries, where passing intermediate values (\scopes) around as first-class # objects is convenient. # - # You can define a \scope that applies to all finders using + # You can define a \scope that applies to all finders using # ActiveRecord::Base.default_scope. def scoped(options = nil) if options @@ -33,10 +39,6 @@ module ActiveRecord end end - def scopes - read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) - end - # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. # @@ -48,20 +50,20 @@ module ActiveRecord # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. # - # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it - # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, - # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. - # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; + # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it + # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, + # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. + # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> # all behave as if Shirt.red really was an Array. # - # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce + # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce # all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> - # returns the number of garments for which these criteria obtain. Similarly with + # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. Similarly with # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which + # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, # # class Person < ActiveRecord::Base @@ -88,14 +90,22 @@ module ActiveRecord # end # end # end - def scope(name, scope_options = {}, &block) + # + # Scopes can also be used while creating/building a record. + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # end + # + # Article.published.new.published # => true + # Article.published.create.published # => true + def scope(name, scope_options = {}) name = name.to_sym valid_scope_name?(name) + extension = Module.new(&Proc.new) if block_given? - extension = Module.new(&block) if block_given? - - scopes[name] = lambda do |*args| - options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options + scope_proc = lambda do |*args| + options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options relation = if options.is_a?(Hash) scoped.apply_finder_options(options) @@ -108,12 +118,9 @@ module ActiveRecord extension ? relation.extending(extension) : relation end - singleton_class.send(:redefine_method, name, &scopes[name]) - end + self.scopes = self.scopes.merge name => scope_proc - def named_scope(*args, &block) - ActiveSupport::Deprecation.warn("Base.named_scope has been deprecated, please use Base.scope instead", caller) - scope(*args, &block) + singleton_class.send(:redefine_method, name, &scopes[name]) end protected diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e652296e2c..522c0cfc9f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/class/attribute' module ActiveRecord module NestedAttributes #:nodoc: @@ -11,7 +12,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_inheritable_accessor :nested_attributes_options, :instance_writer => false + class_attribute :nested_attributes_options, :instance_writer => false self.nested_attributes_options = {} end @@ -25,7 +26,7 @@ module ActiveRecord # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: - # + # # <tt>author_attributes=(attributes)</tt> and # <tt>pages_attributes=(attributes)</tt>. # @@ -190,6 +191,34 @@ module ActiveRecord # destruction, are saved and destroyed automatically and atomically when # the parent model is saved. This happens inside the transaction initiated # by the parents save method. See ActiveRecord::AutosaveAssociation. + # + # === Using with attr_accessible + # + # The use of <tt>attr_accessible</tt> can interfere with nested attributes + # if you're not careful. For example, if the <tt>Member</tt> model above + # was using <tt>attr_accessible</tt> like this: + # + # attr_accessible :name + # + # You would need to modify it to look like this: + # + # attr_accessible :name, :posts_attributes + # + # === Validating the presence of a parent model + # + # If you want to validate that a child record is associated with a parent + # record, you can use <tt>validates_presence_of</tt> and + # <tt>inverse_of</tt> as this example illustrates: + # + # class Member < ActiveRecord::Base + # has_many :posts, :inverse_of => :member + # accepts_nested_attributes_for :posts + # end + # + # class Post < ActiveRecord::Base + # belongs_to :member, :inverse_of => :posts + # validates_presence_of :member + # end module ClassMethods REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } } @@ -240,7 +269,11 @@ module ActiveRecord if reflection = reflect_on_association(association_name) reflection.options[:autosave] = true add_autosave_association_callbacks(reflection) + + nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options + self.nested_attributes_options = nested_attributes_options + type = (reflection.collection? ? :collection : :one_to_one) # def pirate_attributes=(attributes) @@ -287,18 +320,15 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = nested_attributes_options[association_name] + options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access - check_existing_record = (options[:update_only] || !attributes['id'].blank?) - if check_existing_record && (record = send(association_name)) && + if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && (options[:update_only] || record.id.to_s == attributes['id'].to_s) - assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) + assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) - elsif attributes['id'] - existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - self.send(association_name.to_s+'=', existing_record) + elsif attributes['id'].present? + raise_nested_attributes_record_not_found(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" @@ -338,7 +368,7 @@ module ActiveRecord # { :id => '2', :_destroy => true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = nested_attributes_options[association_name] + options = self.nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -349,16 +379,21 @@ module ActiveRecord end if attributes_collection.is_a? Hash - attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes } + keys = attributes_collection.keys + attributes_collection = if keys.include?('id') || keys.include?(:id) + Array.wrap(attributes_collection) + else + attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes } + end end - association = send(association_name) + association = association(association_name) existing_records = if association.loaded? - association.to_a + association.target else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.present? ? association.all(:conditions => {association.primary_key => attribute_ids}) : [] + attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids) end attributes_collection.each do |attributes| @@ -368,16 +403,31 @@ module ActiveRecord unless reject_new_record?(association_name, attributes) association.build(attributes.except(*UNASSIGNABLE_KEYS)) end - elsif existing_records.count == 0 #Existing record but not yet associated existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) - association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded? - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - + if !call_reject_if(association_name, attributes) + association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } - association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded? - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + unless association.loaded? || call_reject_if(association_name, attributes) + # Make sure we are operating on the actual object which is in the association's + # proxy_target array (either by finding it, or adding it if not found) + target_record = association.target.detect { |record| record == existing_record } + + if target_record + existing_record = target_record + else + association.add_to_target(existing_record) + end + end + + if !call_reject_if(association_name, attributes) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + end + else + raise_nested_attributes_record_not_found(association_name, attributes['id']) end end end @@ -385,11 +435,8 @@ module ActiveRecord # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) - if has_destroy_flag?(attributes) && allow_destroy - record.mark_for_destruction - else - record.attributes = attributes.except(*UNASSIGNABLE_KEYS) - end + record.attributes = attributes.except(*UNASSIGNABLE_KEYS) + record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end # Determines if a hash contains a truthy _destroy key. @@ -397,7 +444,7 @@ module ActiveRecord ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) end - # Determines if a new record should be built by checking for + # Determines if a new record should be build by checking for # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) @@ -405,7 +452,7 @@ module ActiveRecord end def call_reject_if(association_name, attributes) - case callback = nested_attributes_options[association_name][:reject_if] + case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc @@ -413,5 +460,8 @@ module ActiveRecord end end + def raise_nested_attributes_record_not_found(association_name, record_id) + raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + end end end diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 78bac55bf2..0893d7e337 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Observer # - # Observer classes respond to lifecycle callbacks to implement trigger-like + # Observer classes respond to life cycle callbacks to implement trigger-like # behavior outside the original class. This is a great way to reduce the # clutter that normally comes when the model class is burdened with # functionality that doesn't pertain to the core responsibility of the @@ -67,7 +67,7 @@ module ActiveRecord # # == Configuration # - # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration + # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration # setting in your <tt>config/application.rb</tt> file. # # config.active_record.observers = :comment_observer, :signup_observer @@ -89,50 +89,31 @@ module ActiveRecord # singletons and that call instantiates and registers them. # class Observer < ActiveModel::Observer - class_attribute :observed_methods - self.observed_methods = [].freeze - - def initialize - super - observed_descendants.each { |klass| add_observer!(klass) } - end - - def self.method_added(method) - method = method.to_sym - - if ActiveRecord::Callbacks::CALLBACKS.include?(method) - self.observed_methods += [method] - self.observed_methods.freeze - end - end protected - def observed_descendants - observed_classes.sum([]) { |klass| klass.descendants } - end - - def observe_callbacks? - self.class.observed_methods.any? + def observed_classes + klasses = super + klasses + klasses.map { |klass| klass.descendants }.flatten end def add_observer!(klass) super - define_callbacks klass if observe_callbacks? + define_callbacks klass end def define_callbacks(klass) - existing_methods = klass.instance_methods.map(&:to_sym) observer = self observer_name = observer.class.name.underscore.gsub('/', '__') - self.class.observed_methods.each do |method| - callback = :"_notify_#{observer_name}_for_#{method}" - unless existing_methods.include? callback - klass.send(:define_method, callback) do # def _notify_user_observer_for_before_save - observer.update(method, self) # observer.update(:before_save, self) - end # end - klass.send(method, callback) # before_save :_notify_user_observer_for_before_save + ActiveRecord::Callbacks::CALLBACKS.each do |callback| + next unless respond_to?(callback) + callback_meth = :"_notify_#{observer_name}_for_#{callback}" + unless klass.respond_to?(callback_meth) + klass.send(:define_method, callback_meth) do + observer.send(callback, self) + end + klass.send(callback, callback_meth) end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 71b46beaef..17a64b6e86 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,7 +1,7 @@ module ActiveRecord # = Active Record Persistence module Persistence - # Returns true if this object hasn't been saved yet -- that is, a record + # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the data store yet; otherwise, returns false. def new_record? @new_record @@ -18,9 +18,6 @@ module ActiveRecord !(new_record? || destroyed?) end - # :call-seq: - # save(options) - # # Saves the model. # # If the model is new a record gets created in the database, otherwise @@ -67,15 +64,19 @@ module ActiveRecord # callbacks, Observer methods, or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - self.class.delete(id) if persisted? + if persisted? + self.class.delete(id) + IdentityMap.remove(self) if IdentityMap.enabled? + end @destroyed = true freeze end - # Deletes the record in the database and freezes this instance to reflect + # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). def destroy if persisted? + IdentityMap.remove(self) if IdentityMap.enabled? self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all end @@ -83,15 +84,15 @@ module ActiveRecord freeze end - # Returns an instance of the specified +klass+ with the attributes of the - # current record. This is mostly useful in relation to single-table - # inheritance structures where you want a subclass to appear as the - # superclass. This can be used along with record identification in - # Action Pack to allow, say, <tt>Client < Company</tt> to do something + # Returns an instance of the specified +klass+ with the attributes of the + # current record. This is mostly useful in relation to single-table + # inheritance structures where you want a subclass to appear as the + # superclass. This can be used along with record identification in + # Action Pack to allow, say, <tt>Client < Company</tt> to do something # like render <tt>:partial => @client.becomes(Company)</tt> to render that # instance using the companies/company partial instead of clients/client. # - # Note: The new instance will share a link to the same attributes as the original class. + # Note: The new instance will share a link to the same attributes as the original class. # So any change to the attributes in either instance will affect the other. def becomes(klass) became = klass.new @@ -99,37 +100,23 @@ module ActiveRecord became.instance_variable_set("@attributes_cache", @attributes_cache) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) + became.type = klass.name unless self.class.descends_from_active_record? became end - # Updates a single attribute and saves the record. + # Updates a single attribute and saves the record. # This is especially useful for boolean flags on existing records. Also note that # - # * The attribute being updated must be a column name. # * Validation is skipped. - # * No callbacks are invoked. + # * Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. - # * Does not work on associations. - # * Does not work on attr_accessor attributes. - # * Does not work on new record. <tt>record.new_record?</tt> should return false for this method to work. - # * Updates only the attribute that is input to the method. If there are other changed attributes then - # those attributes are left alone. In that case even after this method has done its work <tt>record.changed?</tt> - # will return true. + # * Updates all the attributes that are dirty in this object. # def update_attribute(name, value) - raise ActiveRecordError, "#{name.to_s} is marked as readonly" if self.class.readonly_attributes.include? name.to_s - - changes = record_update_timestamps || {} - - if name - name = name.to_s - send("#{name}=", value) - changes[name] = read_attribute(name) - end - - @changed_attributes.except!(*changes.keys) - primary_key = self.class.primary_key - self.class.update_all(changes, { primary_key => self[primary_key] }) == 1 + name = name.to_s + raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + send("#{name}=", value) + save(:validate => false) end # Updates the attributes of the model from the passed-in hash and saves the @@ -213,22 +200,54 @@ module ActiveRecord def reload(options = nil) clear_aggregation_cache clear_association_cache - @attributes.update(self.class.unscoped { self.class.find(self.id, options) }.instance_variable_get('@attributes')) + + IdentityMap.without do + fresh_object = self.class.unscoped { self.class.find(self.id, options) } + @attributes.update(fresh_object.instance_variable_get('@attributes')) + end + @attributes_cache = {} self end # Saves the record with the updated_at/on attributes set to the current time. # Please note that no validation is performed and no callbacks are executed. - # If an attribute name is passed, that attribute is updated along with + # If an attribute name is passed, that attribute is updated along with # updated_at/on attributes. # - # Examples: - # # product.touch # updates updated_at/on # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on - def touch(attribute = nil) - update_attribute(attribute, current_time_from_proper_timezone) + # + # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object. + # + # class Brake < ActiveRecord::Base + # belongs_to :car, :touch => true + # end + # + # class Car < ActiveRecord::Base + # belongs_to :corporation, :touch => true + # end + # + # # triggers @brake.car.touch and @brake.car.corporation.touch + # @brake.touch + def touch(name = nil) + attributes = timestamp_attributes_for_update_in_model + attributes << name if name + + unless attributes.empty? + current_time = current_time_from_proper_timezone + changes = {} + + attributes.each do |column| + changes[column.to_s] = write_attribute(column.to_s, current_time) + end + + changes[self.class.locking_column] = increment_lock if locking_enabled? + + @changed_attributes.except!(*changes.keys) + primary_key = self.class.primary_key + self.class.update_all(changes, { primary_key => self[primary_key] }) == 1 + end end private @@ -243,26 +262,21 @@ module ActiveRecord def update(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_values(false, false, attribute_names) return 0 if attributes_with_values.empty? - self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values) + klass = self.class + stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) + klass.connection.update stmt.to_sql end # Creates a record with values matching those of the instance attributes # and returns its id. def create - if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) - self.id = connection.next_sequence_value(self.class.sequence_name) - end + attributes_values = arel_attributes_values(!id.nil?) - attributes_values = arel_attributes_values - - new_id = if attributes_values.empty? - self.class.unscoped.insert connection.empty_insert_statement_value - else - self.class.unscoped.insert attributes_values - end + new_id = self.class.unscoped.insert attributes_values self.id ||= new_id + IdentityMap.add(self) if IdentityMap.enabled? @new_record = false id end @@ -272,10 +286,9 @@ module ActiveRecord # that a new instance, or one populated from a passed-in Hash, still has all the attributes # that instances loaded from the database would. def attributes_from_column_definition - self.class.columns.inject({}) do |attributes, column| - attributes[column.name] = column.default unless column.name == self.class.primary_key - attributes - end + Hash[self.class.columns.map do |column| + [column.name, column.default] + end] end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 78fdb77216..cace6f0cc0 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -13,10 +13,14 @@ module ActiveRecord class Railtie < Rails::Railtie config.active_record = ActiveSupport::OrderedOptions.new - config.generators.orm :active_record, :migration => true, - :timestamps => true + config.app_generators.orm :active_record, :migration => true, + :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::QueryCache" + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::QueryCache" + + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::ConnectionAdapters::ConnectionManagement" rake_tasks do load "active_record/railties/databases.rake" @@ -39,6 +43,11 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.identity_map" do |app| + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::IdentityMap::Middleware" if config.active_record.delete(:identity_map) + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| @@ -65,27 +74,19 @@ module ActiveRecord end initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| - unless app.config.cache_classes - ActiveSupport.on_load(:active_record) do - ActionDispatch::Callbacks.after do - ActiveRecord::Base.clear_reloadable_connections! - end + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_cleanup do + ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! end end end - initializer "active_record.add_concurrency_middleware" do |app| - if app.config.allow_concurrency - app.config.middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" - end - end - config.after_initialize do ActiveSupport.on_load(:active_record) do instantiate_observers - ActionDispatch::Callbacks.to_prepare(:activerecord_instantiate_observers) do + ActionDispatch::Reloader.to_prepare do ActiveRecord::Base.instantiate_observers end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ae605d3e7a..ff36814684 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,7 +1,14 @@ -namespace :db do +db_namespace = namespace :db do task :load_config => :rails_env do require 'active_record' ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Migrator.migrations_paths = Rails.application.paths["db/migrate"].to_a + + if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) + if engine.paths["db/migrate"].existent + ActiveRecord::Migrator.migrations_paths += engine.paths["db/migrate"].to_a + end + end end namespace :create do @@ -45,8 +52,8 @@ namespace :db do # Create the SQLite database ActiveRecord::Base.establish_connection(config) ActiveRecord::Base.connection - rescue - $stderr.puts $!, *($!.backtrace) + rescue Exception => e + $stderr.puts e, *(e.backtrace) $stderr.puts "Couldn't create database for #{config.inspect}" end end @@ -61,12 +68,14 @@ namespace :db do @charset = ENV['CHARSET'] || 'utf8' @collation = ENV['COLLATION'] || 'utf8_unicode_ci' creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} + error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error + access_denied_error = 1045 begin ActiveRecord::Base.establish_connection(config.merge('database' => nil)) ActiveRecord::Base.connection.create_database(config['database'], creation_options) ActiveRecord::Base.establish_connection(config) - rescue Mysql::Error => sqlerr - if sqlerr.errno == Mysql::Error::ER_ACCESS_DENIED_ERROR + rescue error_class => sqlerr + if sqlerr.errno == access_denied_error print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>" root_password = $stdin.gets.strip grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \ @@ -84,13 +93,13 @@ namespace :db do end end when 'postgresql' - @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8' + @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8' begin ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding)) ActiveRecord::Base.establish_connection(config) - rescue - $stderr.puts $!, *($!.backtrace) + rescue Exception => e + $stderr.puts e, *(e.backtrace) $stderr.puts "Couldn't create database for #{config.inspect}" end end @@ -135,21 +144,21 @@ namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false)." - task :migrate => :environment do + task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) - Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) + db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end namespace :migrate do # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' - task :redo => :environment do + task :redo => [:environment, :load_config] do if ENV["VERSION"] - Rake::Task["db:migrate:down"].invoke - Rake::Task["db:migrate:up"].invoke + db_namespace["migrate:down"].invoke + db_namespace["migrate:up"].invoke else - Rake::Task["db:rollback"].invoke - Rake::Task["db:migrate"].invoke + db_namespace["rollback"].invoke + db_namespace["migrate"].invoke end end @@ -157,23 +166,23 @@ namespace :db do task :reset => ["db:drop", "db:create", "db:migrate"] # desc 'Runs the "up" for a given migration VERSION.' - task :up => :environment do + task :up => [:environment, :load_config] do version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil raise "VERSION is required" unless version - ActiveRecord::Migrator.run(:up, "db/migrate/", version) - Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version) + db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end # desc 'Runs the "down" for a given migration VERSION.' - task :down => :environment do + task :down => [:environment, :load_config] do version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil raise "VERSION is required" unless version - ActiveRecord::Migrator.run(:down, "db/migrate/", version) - Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) + db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end desc "Display status of migrations" - task :status => :environment do + task :status => [:environment, :load_config] do config = ActiveRecord::Base.configurations[Rails.env || 'development'] ActiveRecord::Base.establish_connection(config) unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) @@ -184,7 +193,7 @@ namespace :db do file_list = [] Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file| # only files matching "20091231235959_some_name.rb" pattern - if match_data = /(\d{14})_(.+)\.rb/.match(file) + if match_data = /^(\d{14})_(.+)\.rb$/.match(file) status = db_list.delete(match_data[1]) ? 'up' : 'down' file_list << [status, match_data[1], match_data[2]] end @@ -204,17 +213,17 @@ namespace :db do end desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).' - task :rollback => :environment do + task :rollback => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.rollback('db/migrate/', step) - Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) + db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' - task :forward => :environment do + task :forward => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.forward('db/migrate/', step) - Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step) + db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' @@ -258,7 +267,7 @@ namespace :db do # desc "Raises an error if there are pending migrations" task :abort_if_pending_migrations => :environment do if defined? ActiveRecord - pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations + pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending migrations:" @@ -275,8 +284,7 @@ namespace :db do desc 'Load the seed data from db/seeds.rb' task :seed => 'db:abort_if_pending_migrations' do - seed_file = File.join(Rails.root, 'db', 'seeds.rb') - load(seed_file) if File.exist?(seed_file) + Rails.application.load_seed end namespace :fixtures do @@ -288,8 +296,8 @@ namespace :db do base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir["#{fixtures_dir}/**/*.{yml,csv}"]).each do |fixture_file| - Fixtures.create_fixtures(fixtures_dir, fixture_file[(fixtures_dir.size + 1)..-5]) + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + Fixtures.create_fixtures(fixtures_dir, fixture_file) end end @@ -319,12 +327,12 @@ namespace :db do namespace :schema do desc "Create a db/schema.rb file that can be portably used against any DB supported by AR" - task :dump => :environment do + task :dump => :load_config do require 'active_record/schema_dumper' File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file| ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end - Rake::Task["db:schema:dump"].reenable + db_namespace["schema:dump"].reenable end desc "Load a schema.rb file into the database" @@ -333,7 +341,7 @@ namespace :db do if File.exists?(file) load(file) else - abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/boot.rb to limit the frameworks that will be loaded} + abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded} end end end @@ -367,7 +375,7 @@ namespace :db do db_string = firebird_db_string(abcs[Rails.env]) sh "isql -a #{db_string} > #{Rails.root}/db/#{Rails.env}_structure.sql" else - raise "Task not supported by '#{abcs["test"]["adapter"]}'" + raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? @@ -381,7 +389,7 @@ namespace :db do task :load => 'db:test:purge' do ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Schema.verbose = false - Rake::Task["db:schema:load"].invoke + db_namespace["schema:load"].invoke end # desc "Recreate the test database from the current environment's database schema" @@ -401,7 +409,7 @@ namespace :db do ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] - `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]}` + `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]} #{abcs["test"]["template"]}` when "sqlite", "sqlite3" dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` @@ -455,7 +463,7 @@ namespace :db do # desc 'Check for pending migrations and load the test schema' task :prepare => 'db:abort_if_pending_migrations' do if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? - Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].invoke + db_namespace[{ :sql => "test:clone_structure", :ruby => "test:load" }[ActiveRecord::Base.schema_format]].invoke end end end @@ -477,6 +485,34 @@ namespace :db do end end +namespace :railties do + namespace :install do + # desc "Copies missing migrations from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2" + task :migrations => :"db:load_config" do + to_load = ENV["FROM"].blank? ? :all : ENV["FROM"].split(",").map {|n| n.strip } + railties = {} + Rails.application.railties.all do |railtie| + next unless to_load == :all || to_load.include?(railtie.railtie_name) + + if railtie.respond_to?(:paths) && (path = railtie.paths["db/migrate"].first) + railties[railtie.railtie_name] = path + end + end + + on_skip = Proc.new do |name, migration| + puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists." + end + + on_copy = Proc.new do |name, migration, old_path| + puts "Copied migration #{migration.basename} from #{name}" + end + + ActiveRecord::Migration.copy( ActiveRecord::Migrator.migrations_paths.first, railties, + :on_skip => on_skip, :on_copy => on_copy) + end + end +end + task 'test:prepare' => 'db:test:prepare' def drop_database(config) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 7f47a812eb..e801bc4afa 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,10 +1,18 @@ +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/module/deprecation' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: extend ActiveSupport::Concern + included do + class_attribute :reflections + self.reflections = {} + end + # Reflection enables to interrogate Active Record classes and objects - # about their associations and aggregations. This information can, + # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type # and displays the associations to other objects. @@ -20,36 +28,27 @@ module ActiveRecord when :composed_of reflection = AggregateReflection.new(macro, name, options, active_record) end - write_inheritable_hash :reflections, name => reflection - reflection - end - # Returns a hash containing all AssociationReflection objects for the current class. - # Example: - # - # Invoice.reflections - # Account.reflections - # - def reflections - read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {}) + 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.select { |reflection| reflection.is_a?(AggregateReflection) } + reflections.values.grep(AggregateReflection) end - # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). + # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). # - # Account.reflect_on_aggregation(:balance) #=> the balance AggregateReflection + # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil 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>, + # 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>, # <tt>:belongs_to</tt>) as the first parameter. # # Example: @@ -58,7 +57,7 @@ module ActiveRecord # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # def reflect_on_all_associations(macro = nil) - association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) } + association_reflections = reflections.values.grep(AssociationReflection) macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections end @@ -78,7 +77,7 @@ module ActiveRecord end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of + # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection attr_reader :active_record @@ -89,23 +88,23 @@ module ActiveRecord # Returns the name of the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> + # <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. + # 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 - # Returns the hash of options used for the macro. + # 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 - # Returns the class for the macro. + # 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 @@ -113,7 +112,7 @@ module ActiveRecord @klass ||= class_name.constantize end - # Returns the class name for the macro. + # 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> @@ -138,12 +137,12 @@ module ActiveRecord end - # Holds all the meta-data about an aggregation as it was specified in the + # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: end - # Holds all the meta-data about an association as it was specified in the + # 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. @@ -198,8 +197,21 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key + end + def primary_key_name - @primary_key_name ||= options[:foreign_key] || derive_primary_key_name + foreign_key + end + deprecate :primary_key_name => :foreign_key + + def foreign_type + @foreign_type ||= options[:foreign_type] || "#{name}_type" + end + + def type + @type ||= "#{options[:as]}_type" end def primary_key_column @@ -207,7 +219,18 @@ module ActiveRecord end def association_foreign_key - @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key + @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key + end + + def association_primary_key + @association_primary_key ||= + options[:primary_key] || + !options[:polymorphic] && klass.primary_key || + 'id' + end + + def active_record_primary_key + @active_record_primary_key ||= options[:primary_key] || active_record.primary_key end def counter_cache_column @@ -239,18 +262,32 @@ module ActiveRecord end def through_reflection - false - end - - def through_reflection_primary_key_name + nil end def source_reflection nil end + # A chain of reflections from this one back to the owner. For more see the explanation in + # ThroughReflection. + def chain + [self] + end + + # An array of arrays of conditions. Each item in the outside array corresponds to a reflection + # in the #chain. The inside arrays are simply conditions (and each condition may itself be + # a hash, array, arel predicate, etc...) + def conditions + conditions = [options[:conditions]].compact + conditions << { type => active_record.base_class.name } if options[:as] + [conditions] + end + + alias :source_macro :macro + def has_inverse? - !@options[:inverse_of].nil? + @options[:inverse_of] end def inverse_of @@ -289,22 +326,36 @@ module ActiveRecord !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) end - def dependent_conditions(record, base_class, extra_conditions) - dependent_conditions = [] - dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}" - dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as] - dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - dependent_conditions << extra_conditions if extra_conditions - dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") - dependent_conditions = dependent_conditions.gsub('@', '\@') - dependent_conditions - end - # Returns +true+ if +self+ is a +belongs_to+ reflection. def belongs_to? macro == :belongs_to end + def association_class + case macro + when :belongs_to + if options[:polymorphic] + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + when :has_and_belongs_to_many + Associations::HasAndBelongsToManyAssociation + when :has_many + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end + when :has_one + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end + end + end + private def derive_class_name class_name = name.to_s.camelize @@ -312,7 +363,7 @@ module ActiveRecord class_name end - def derive_primary_key_name + def derive_foreign_key if belongs_to? "#{name}_id" elsif options[:as] @@ -326,6 +377,8 @@ module ActiveRecord # Holds all the meta-data about a :through association as it was specified # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: + delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection + # Gets the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # @@ -353,6 +406,88 @@ module ActiveRecord @through_reflection ||= active_record.reflect_on_association(options[:through]) end + # Returns an array of reflections which are involved in this association. Each item in the + # array corresponds to a table which will be part of the query for this association. + # + # The chain is built by recursively calling #chain on the source reflection and the through + # reflection. The base case for the recursion is a normal association, which just returns + # [self] as its #chain. + def chain + @chain ||= begin + chain = source_reflection.chain + through_reflection.chain + chain[0] = self # Use self so we don't lose the information from :source_type + chain + end + end + + # Consider the following example: + # + # class Person + # has_many :articles + # has_many :comment_tags, :through => :articles + # end + # + # class Article + # has_many :comments + # has_many :comment_tags, :through => :comments, :source => :tags + # end + # + # class Comment + # has_many :tags + # end + # + # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, + # but only Comment.tags will be represented in the #chain. So this method creates an array + # of conditions corresponding to the chain. Each item in the #conditions array corresponds + # to an item in the #chain, and is itself an array of conditions from an arbitrary number + # of relevant reflections, plus any :source_type or polymorphic :as constraints. + def conditions + @conditions ||= begin + conditions = source_reflection.conditions + + # Add to it the conditions from this reflection if necessary. + conditions.first << options[:conditions] if options[:conditions] + + through_conditions = through_reflection.conditions + + if options[:source_type] + through_conditions.first << { foreign_type => options[:source_type] } + end + + # Recursively fill out the rest of the array from the through reflection + conditions += through_conditions + + # And return + conditions + end + end + + # The macro used by the source association + def source_macro + source_reflection.source_macro + end + + # A through association is nested iff there would be more than one join table + def nested? + chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many + end + + # We want to use the klass from this reflection, rather than just delegate straight to + # the source_reflection, because the source_reflection may be polymorphic. We still + # need to respect the source_reflection's :primary_key option, though. + def association_primary_key + @association_primary_key ||= begin + # Get the "actual" source reflection if the immediate source reflection has a + # source reflection itself + source_reflection = self.source_reflection + while source_reflection.source_reflection + source_reflection = source_reflection.source_reflection + end + + source_reflection.options[:primary_key] || klass.primary_key + end + end + # Gets an array of possible <tt>:through</tt> source reflection names: # # [:singularized, :pluralized] @@ -361,11 +496,23 @@ module ActiveRecord @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } end + def source_options + source_reflection.options + end + + def through_options + through_reflection.options + end + def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end + if through_reflection.options[:polymorphic] + raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + end + if source_reflection.nil? raise HasManyThroughSourceAssociationNotFoundError.new(self) end @@ -375,24 +522,16 @@ module ActiveRecord end if source_reflection.options[:polymorphic] && options[:source_type].nil? - raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) + raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end - unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? - raise HasManyThroughSourceAssociationMacroError.new(self) + if macro == :has_one && through_reflection.collection? + raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end check_validity_of_inverse! end - def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name - end - - def through_reflection_primary_key_name - through_reflection.primary_key_name if through_reflection.belongs_to? - end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 30be723291..8e545f9cad 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -5,13 +5,14 @@ module ActiveRecord class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches + # These are explicitly delegated to improve performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a - delegate :insert, :to => :arel + delegate :table_name, :primary_key, :to => :klass attr_reader :table, :klass, :loaded attr_accessor :extensions @@ -28,6 +29,36 @@ module ActiveRecord @extensions = [] end + def insert(values) + primary_key_value = nil + + if primary_key && Hash === values + primary_key_value = values[values.keys.find { |k| + k.name == primary_key + }] + + if !primary_key_value && connection.prefetch_primary_key?(klass.table_name) + primary_key_value = connection.next_sequence_value(klass.sequence_name) + values[klass.arel_table[klass.primary_key]] = primary_key_value + end + end + + im = arel.create_insert + im.into @table + + if values.empty? # empty insert + im.values = im.create_values [connection.null_insert_value], [] + else + im.insert values + end + + @klass.connection.insert( + im.to_sql, + 'SQL', + primary_key, + primary_key_value) + end + def new(*args, &block) scoping { @klass.new(*args, &block) } end @@ -47,27 +78,30 @@ module ActiveRecord end def respond_to?(method, include_private = false) - return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) || @klass.respond_to?(method, include_private) - - if match = DynamicFinderMatch.match(method) - return true if @klass.send(:all_attributes_exists?, match.attribute_names) - elsif match = DynamicScopeMatch.match(method) - return true if @klass.send(:all_attributes_exists?, match.attribute_names) - else + arel.respond_to?(method, include_private) || + Array.method_defined?(method) || + @klass.respond_to?(method, include_private) || super - end end def to_a return @records if loaded? - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql) + @records = if @readonly_value.nil? && !@klass.locking_enabled? + eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + else + IdentityMap.without do + eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + end + end preload = @preload_values preload += @includes_values unless eager_loading? - preload.each {|associations| @klass.send(:preload_associations, @records, associations) } + preload.each do |associations| + ActiveRecord::Associations::Preloader.new(@records, associations).run + end - # @readonly_value is true only if set explicitly. @implicit_readonly is true if there + # @readonly_value is true only if set explicitly. @implicit_readonly is true if there # are JOINS and no explicit SELECT. readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value @records.each { |record| record.readonly! } if readonly @@ -76,7 +110,9 @@ module ActiveRecord @records end - def as_json(options = nil) to_a end #:nodoc: + def as_json(options = nil) #:nodoc: + to_a.as_json(options) + end # Returns size of the records. def size @@ -85,7 +121,10 @@ module ActiveRecord # Returns true if there are no records. def empty? - loaded? ? @records.empty? : count.zero? + return @records.empty? if loaded? + + c = count + c.respond_to?(:zero?) ? c.zero? : c.empty? end def any? @@ -131,7 +170,7 @@ module ActiveRecord # ==== Parameters # # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. - # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. + # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. # See conditions in the intro. # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # @@ -148,16 +187,29 @@ module ActiveRecord # # # Update all books that match conditions, but limit it to 5 ordered by date # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 + # + # # Conditions from the current relation also works + # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David') + # + # # The same idea applies to limit and order + # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') def update_all(updates, conditions = nil, options = {}) if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else + limit = nil + order = [] # Apply limit and order only if they're both present if @limit_value.present? == @order_values.present? - arel.update(@klass.send(:sanitize_sql_for_assignment, updates)) - else - except(:limit, :order).update_all(updates) + limit = arel.limit + order = arel.orders end + + stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) + stmt.take limit if limit + stmt.order(*order) + stmt.key = table[primary_key] + @klass.connection.update stmt.to_sql end end @@ -214,6 +266,7 @@ module ActiveRecord # # Person.destroy_all("last_login < '2004-04-04'") # Person.destroy_all(:status => "inactive") + # Person.where(:age => 0..18).destroy_all def destroy_all(conditions = nil) if conditions where(conditions).destroy_all @@ -263,12 +316,20 @@ module ActiveRecord # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) + # Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all # - # Both calls delete the affected posts all at once with a single DELETE statement. - # If you need to destroy dependent associations or call your <tt>before_*</tt> or + # Both calls delete the affected posts all at once with a single DELETE statement. + # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) - conditions ? where(conditions).delete_all : arel.delete.tap { reset } + if conditions + where(conditions).delete_all + else + statement = arel.compile_delete + affected = @klass.connection.delete statement.to_sql + reset + affected + end end # Deletes the row with a primary key matching the +id+ argument, using a @@ -292,7 +353,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - where(@klass.primary_key => id_or_array).delete_all + where(primary_key => id_or_array).delete_all end def reload @@ -308,25 +369,20 @@ module ActiveRecord self end - def primary_key - @primary_key ||= table[@klass.primary_key] - end - def to_sql @to_sql ||= arel.to_sql end + def where_values_hash + equalities = @where_values.grep(Arel::Nodes::Equality).find_all { |node| + node.left.relation.name == table_name + } + + Hash[equalities.map { |where| [where.left.name, where.right] }] + end + def scope_for_create - @scope_for_create ||= begin - @create_with_value || Hash[ - @where_values.find_all { |w| - w.respond_to?(:operator) && w.operator == :== - }.map { |where| - [where.operand1.name, - where.operand2.respond_to?(:value) ? - where.operand2.value : where.operand2] - }] - end + @scope_for_create ||= where_values_hash.merge(@create_with_value || {}) end def eager_loading? @@ -338,7 +394,7 @@ module ActiveRecord when Relation other.to_sql == to_sql when Array - to_a == other.to_a + to_a == other end end @@ -357,15 +413,6 @@ module ActiveRecord scoping { @klass.send(method, *args, &block) } elsif arel.respond_to?(method) arel.send(method, *args, &block) - elsif match = DynamicFinderMatch.match(method) - attributes = match.attribute_names - super unless @klass.send(:all_attributes_exists?, attributes) - - if match.finder? - find_by_attributes(match, attributes, *args) - elsif match.instantiator? - find_or_instantiator_by_attributes(match, attributes, *args, &block) - end else super end @@ -374,8 +421,19 @@ module ActiveRecord private def references_eager_loaded_tables? + joined_tables = arel.join_sources.map do |join| + if join.is_a?(Arel::Nodes::StringJoin) + tables_in_string(join.left) + else + [join.left.table_name, join.left.table_alias] + end + end + + joined_tables += [table.name, table.table_alias] + # always convert table names to downcase as in Oracle quoted table names are in uppercase - joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.map(&:downcase).uniq + joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq + (tables_in_string(to_sql) - joined_tables).any? end @@ -383,7 +441,7 @@ module ActiveRecord return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index d7494ebb5a..bf5a60f458 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/object/blank' module ActiveRecord - module Batches # :nodoc: + module Batches # Yields each record that was found by the find +options+. The find is # performed by find_in_batches with a batch size of 1000 (or as # specified by the <tt>:batch_size</tt> option). @@ -39,7 +39,7 @@ module ActiveRecord # ascending on the primary key ("id ASC") to make the batch ordering # work. This also mean that this method only works with integer-based # primary keys. You can't set the limit either, that's used to control - # the the batch sizes. + # the batch sizes. # # Example: # @@ -50,7 +50,7 @@ module ActiveRecord def find_in_batches(options = {}) relation = self - if orders.present? || taken.present? + unless arel.orders.blank? && arel.taken.blank? ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end @@ -65,7 +65,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.except(:order).order(batch_order).limit(batch_size) - records = relation.where(primary_key.gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).all while records.any? yield records @@ -73,7 +73,7 @@ module ActiveRecord break if records.size < batch_size if primary_key_offset = records.last.id - records = relation.where(primary_key.gt(primary_key_offset)).to_a + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a else raise "Primary key not included in the custom select clause" end @@ -83,7 +83,7 @@ module ActiveRecord private def batch_order - "#{@klass.table_name}.#{@klass.primary_key} ASC" + "#{table_name}.#{primary_key} ASC" end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index a679c444cf..c1842b1a96 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -6,32 +6,30 @@ module ActiveRecord # Count operates using three different approaches. # # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. - # * Count using column: By passing a column name to count, it will return a count of all the - # rows for the model with supplied column present + # * Count using column: By passing a column name to count, it will return a count of all the + # rows for the model with supplied column present. # * Count using options will find the row count matched by the options used. # # The third approach, count using options, accepts an option hash as the only parameter. The options are: # - # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. + # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. # See conditions in the intro to ActiveRecord::Base. - # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed) - # or named associations in the same form used for the <tt>:include</tt> option, which will - # perform an INNER JOIN on the associated table(s). - # If the value is a string, then the records will be returned read-only since they will have - # attributes that do not correspond to the table's columns. + # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" + # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will + # perform an INNER JOIN on the associated table(s). If the value is a string, then the records + # will be returned read-only since they will have attributes that do not correspond to the table's columns. # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. - # The symbols named refer to already defined associations. When using named associations, count + # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. + # The symbols named refer to already defined associations. When using named associations, count # returns the number of DISTINCT items for the model you're counting. # See eager loading under Associations. # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, - # want to do a join but not - # include the joined columns. - # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as + # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, + # want to do a join but not include the joined columns. + # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as # SELECT COUNT(DISTINCT posts.id) ... - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an # alternate table name (or even the name of a database view). # # Examples for counting all: @@ -44,16 +42,16 @@ module ActiveRecord # Person.count(:conditions => "age > 26") # # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # # # finds the number of rows matching the conditions and joins. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", - # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") + # Person.count(:conditions => "age > 26 AND job.salary > 60000", + # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') # - # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. + # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. # Use Person.count instead. def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) @@ -95,14 +93,14 @@ module ActiveRecord calculate(:sum, column_name, options) end - # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, + # This calculates aggregate values in the given column. Methods for count, sum, average, + # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. # # There are two basic forms of output: - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float + # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float # for AVG, and the given column's type for everything else. - # * Grouped values: This returns an ordered hash of the values and groups them by the + # * Grouped values: This returns an ordered hash of the values and groups them by the # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. # # values = Person.maximum(:age, :group => 'last_name') @@ -119,29 +117,29 @@ module ActiveRecord # end # # Options: - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. # See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, + # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. - # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". + # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". # (Rarely needed). - # The records will be returned read-only since they will have attributes that do not correspond to the + # The records will be returned read-only since they will have attributes that do not correspond to the # table's columns. # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example + # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example # want to do a join, but not include the joined columns. - # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as + # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as # SELECT COUNT(DISTINCT posts.id) ... # # Examples: # Person.calculate(:count, :all) # The same as Person.count # Person.average(:age) # SELECT AVG(age) FROM people... - # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for + # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for # # everyone with a last name other than 'Drake' # # # Selects the minimum age for any family without any minors - # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) + # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) @@ -163,80 +161,112 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase + distinct = nil + if operation == "count" column_name ||= (select_for_count || :all) - joins = arel.joins(arel) - if joins.present? && joins =~ /LEFT OUTER/i + unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true - column_name = @klass.primary_key if column_name == :all + column_name = primary_key if column_name == :all end - distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i - distinct ||= options[:distinct] - else - distinct = nil + distinct = nil if column_name =~ /\s*DISTINCT\s+/i end distinct = options[:distinct] || distinct - column_name = :all if column_name.blank? && operation == "count" if @group_values.any? - return execute_grouped_calculation(operation, column_name) + execute_grouped_calculation(operation, column_name, distinct) else - return execute_simple_calculation(operation, column_name, distinct) + execute_simple_calculation(operation, column_name, distinct) end end - def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - column = if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped, column_name) + def aggregate_column(column_name) + if @klass.column_names.include?(column_name.to_s) + Arel::Attribute.new(@klass.unscoped.table, column_name) else - Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) + Arel.sql(column_name == :all ? "*" : column_name.to_s) end + end - # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = except(:order).select(operation == 'count' ? column.count(distinct) : column.send(operation)) - type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation) + def operation_over_aggregate_column(column, operation, distinct) + operation == 'count' ? column.count(distinct) : column.send(operation) end - def execute_grouped_calculation(operation, column_name) #:nodoc: - group_attr = @group_values.first - association = @klass.reflect_on_association(group_attr.to_sym) - associated = association && association.macro == :belongs_to # only count belongs_to associations - group_field = associated ? association.primary_key_name : group_attr - group_alias = column_alias_for(group_field) - group_column = column_for(group_field) + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: + column = aggregate_column(column_name) - group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field + # Postgresql doesn't like ORDER BY when there are no GROUP BY + relation = except(:order) + select_value = operation_over_aggregate_column(column, operation, distinct) + + relation.select_values = [select_value] - aggregate_alias = column_alias_for(operation, column_name) + query_builder = relation.arel - select_statement = if operation == 'count' && column_name == :all - "COUNT(*) AS count_all" + if operation == "count" + limit = relation.limit_value + offset = relation.offset_value + + unless limit && offset + query_builder.limit = nil + query_builder.offset = nil + end + end + + type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation) + end + + def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: + group_attr = @group_values + association = @klass.reflect_on_association(group_attr.first.to_sym) + associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attr) + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields).map { |aliaz,field| + [aliaz, column_for(field)] + } + + group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields + + if operation == 'count' && column_name == :all + aggregate_alias = 'count_all' else - Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql + aggregate_alias = column_alias_for(operation, column_name) end - select_statement << ", #{group_field} AS #{group_alias}" + select_values = [ + operation_over_aggregate_column( + aggregate_column(column_name), + operation, + distinct).as(aggregate_alias) + ] + + select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| + "#{field} AS #{aliaz}" + } - relation = except(:group).select(select_statement).group(group) + relation = except(:group).group(group.join(',')) + relation.select_values = select_values calculated_data = @klass.connection.select_all(relation.to_sql) if association - key_ids = calculated_data.collect { |row| row[group_alias] } + key_ids = calculated_data.collect { |row| row[group_aliases.first] } key_records = association.klass.base_class.find(key_ids) - key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + key_records = Hash[key_records.map { |r| [r.id, r] }] end - calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| - key = type_cast_calculated_value(row[group_alias], group_column) - key = key_records[key] if associated - value = row[aggregate_alias] - all[key] = type_cast_calculated_value(value, column_for(column_name), operation) - all - end + ActiveSupport::OrderedHash[calculated_data.map do |row| + key = group_columns.map { |aliaz, column| + type_cast_calculated_value(row[aliaz], column) + } + key = key.first if key.size == 1 + key = key_records[key] if associated + [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] + end] end # Converts the given keys to the value that the database adapter returns as @@ -264,15 +294,11 @@ module ActiveRecord end def type_cast_calculated_value(value, column, operation = nil) - if value.is_a?(String) || value.nil? - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value.try(:to_d) - else type_cast_using_column(value, column) - end - else - value + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value.respond_to?(:to_d) ? value.to_d : value + else type_cast_using_column(value, column) end end @@ -282,7 +308,7 @@ module ActiveRecord def select_for_count if @select_values.present? - select = @select_values.join(", ") + select = @select_values.join(", ") select if select !~ /(,|\*)/ end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b34c11973b..25e23a9d55 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -19,29 +19,29 @@ module ActiveRecord # # All approaches accept an options hash as their last parameter. # - # ==== Parameters + # ==== Options # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>, # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a + # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a # <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, + # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, # it would skip rows 0 through 4. # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), - # named associations in the same form used for the <tt>:include</tt> option, which will perform an + # named associations in the same form used for the <tt>:include</tt> option, which will perform an # <tt>INNER JOIN</tt> on the associated table(s), # or an array containing a mixture of both strings and named associations. - # If the value is a string, then the records will be returned read-only since they will + # If the value is a string, then the records will be returned read-only since they will # have attributes that do not correspond to the table's columns. # Pass <tt>:readonly => false</tt> to override. # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer # to already defined associations. See eager loading under Associations. - # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, + # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed # to an alternate table name (or even the name of a database view). # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". @@ -54,7 +54,7 @@ module ActiveRecord # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) # Person.find([1]) # returns an array for the object with ID = 1 - # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") + # Person.where("administrator = 1").order("created_on DESC").find(1) # # Note that returned records may not be in the same order as the ids you # provide since database rows are unordered. Give an explicit <tt>:order</tt> @@ -63,23 +63,23 @@ module ActiveRecord # ==== Examples # # # find first - # Person.find(:first) # returns the first object fetched by SELECT * FROM people - # Person.find(:first, :conditions => [ "user_name = ?", user_name]) - # Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }]) - # Person.find(:first, :order => "created_on DESC", :offset => 5) + # Person.first # returns the first object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).first + # Person.where(["user_name = :u", { :u => user_name }]).first + # Person.order("created_on DESC").offset(5).first # # # find last - # Person.find(:last) # returns the last object fetched by SELECT * FROM people - # Person.find(:last, :conditions => [ "user_name = ?", user_name]) - # Person.find(:last, :order => "created_on DESC", :offset => 5) + # Person.last # returns the last object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).last + # Person.order("created_on DESC").offset(5).last # # # find all - # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people - # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) - # Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] } - # Person.find(:all, :offset => 10, :limit => 10) - # Person.find(:all, :include => [ :account, :friends ]) - # Person.find(:all, :group => "category") + # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people + # Person.where(["category IN (?)", categories]).limit(50).all + # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all + # Person.offset(10).limit(10).all + # Person.includes([:account, :friends]).all + # Person.group("category").all # # Example for find with a lock: Imagine two concurrent transactions: # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting @@ -88,7 +88,7 @@ module ActiveRecord # expected <tt>person.visits == 4</tt>. # # Person.transaction do - # person = Person.find(1, :lock => true) + # person = Person.lock(true).find(1) # person.visits += 1 # person.save! # end @@ -123,6 +123,11 @@ module ActiveRecord end end + # Same as #first! but raises RecordNotFound if no record is returned + def first!(*args) + self.first(*args) or raise RecordNotFound + end + # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the # same arguments to this method as you can to <tt>find(:last)</tt>. def last(*args) @@ -137,6 +142,11 @@ module ActiveRecord end end + # Same as #last! but raises RecordNotFound if no record is returned + def last!(*args) + self.last(*args) or raise RecordNotFound + end + # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the # same arguments to this method as you can to <tt>find(:all)</tt>. def all(*args) @@ -171,22 +181,25 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id + relation = select("1").limit(1) + case id when Array, Hash - where(id).exists? + relation = relation.where(id) else - relation = select(primary_key).limit(1) - relation = relation.where(primary_key.eq(id)) if id - relation.first ? true : false + relation = relation.where(table[primary_key].eq(id)) if id end + + relation.first ? true : false end protected def find_with_associations including = (@eager_load_values + @includes_values).uniq - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil) - rows = construct_relation_for_association_find(join_dependency).to_a + join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, []) + relation = construct_relation_for_association_find(join_dependency) + rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] @@ -194,13 +207,13 @@ module ActiveRecord def construct_relation_for_association_calculations including = (@eager_load_values + @includes_values).uniq - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel)) + join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first) relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) end def construct_relation_for_association_find(join_dependency) - relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency)) + relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns) apply_join_dependency(relation, join_dependency) end @@ -222,15 +235,15 @@ module ActiveRecord end def construct_limited_ids_condition(relation) - orders = relation.order_values.join(", ") - values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders) + orders = relation.order_values + values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) - ids_array = relation.select(values).collect {|row| row[@klass.primary_key]} - ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array) + ids_array = relation.select(values).collect {|row| row[primary_key]} + ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end def find_by_attributes(match, attributes, *args) - conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h} + conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] result = where(conditions).send(match.finder) if match.bang? && result.blank? @@ -288,19 +301,24 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - record = where(primary_key.eq(id)).first + column = columns_hash[primary_key] + + substitute = connection.substitute_for(column, @bind_values) + relation = where(table[primary_key].eq(substitute)) + relation.bind_values = [[column, id]] + record = relation.first unless record - conditions = arel.send(:where_clauses).join(', ') - conditions = " [WHERE #{conditions}]" if conditions.present? - raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" + conditions = arel.where_sql + conditions = " [#{conditions}]" if conditions + raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}" end record end def find_some(ids) - result = where(primary_key.in(ids)).all + result = where(table[primary_key].in(ids)).all expected_size = if @limit_value && ids.size > @limit_value @@ -317,7 +335,7 @@ module ActiveRecord if result.size == expected_size result else - conditions = arel.send(:where_clauses).join(', ') + conditions = arel.wheres.map { |x| x.value }.join(', ') conditions = " [WHERE #{conditions}]" if conditions.present? error = "Couldn't find all #{@klass.name.pluralize} with IDs " @@ -342,14 +360,8 @@ module ActiveRecord end end - def column_aliases(join_dependency) - join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| - "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") - end - def using_limitable_reflections?(reflections) - reflections.none?(&:collection?) + reflections.none? { |r| r.collection? } end - end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 5cea2328e8..982b3d7e9f 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,33 +1,38 @@ module ActiveRecord - class PredicateBuilder - - def initialize(engine) - @engine = engine - end - - def build_from_hash(attributes, default_table) + class PredicateBuilder # :nodoc: + def self.build_from_hash(engine, attributes, default_table) predicates = attributes.map do |column, value| table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, :engine => @engine) - build_from_hash(value, table) + table = Arel::Table.new(column, engine) + build_from_hash(engine, value, table) else column = column.to_s if column.include?('.') table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, :engine => @engine) + table = Arel::Table.new(table_name, engine) end - attribute = table[column] || Arel::Attribute.new(table, column) + attribute = table[column.to_sym] case value - when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation - values = value.to_a + when ActiveRecord::Relation + value.select_values = [value.klass.arel_table['id']] if value.select_values.empty? + attribute.in(value.arel.ast) + when Array, ActiveRecord::Associations::CollectionProxy + values = value.to_a.map { |x| + x.is_a?(ActiveRecord::Base) ? x.id : x + } attribute.in(values) when Range, Arel::Relation attribute.in(value) + when ActiveRecord::Base + attribute.eq(value.id) + when Class + # FIXME: I think we need to deprecate this behavior + attribute.eq(value.name) else attribute.eq(value) end @@ -36,6 +41,5 @@ module ActiveRecord predicates.flatten end - end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index e71f1cca72..9470e7c6c5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,247 +6,299 @@ module ActiveRecord extend ActiveSupport::Concern attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, + :select_values, :group_values, :order_values, :joins_values, + :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value def includes(*args) - args.reject! { |a| a.blank? } - clone.tap {|r| r.includes_values += args if args.present? } + args.reject! {|a| a.blank? } + + return self if args.empty? + + relation = clone + relation.includes_values = (relation.includes_values + args).flatten.uniq + relation end def eager_load(*args) - clone.tap {|r| r.eager_load_values += args if args.present? } + return self if args.blank? + + relation = clone + relation.eager_load_values += args + relation end def preload(*args) - clone.tap {|r| r.preload_values += args if args.present? } + return self if args.blank? + + relation = clone + relation.preload_values += args + relation end - def select(*args) + def select(value = Proc.new) if block_given? - to_a.select {|*block_args| yield(*block_args) } + to_a.select {|*block_args| value.call(*block_args) } else - clone.tap {|r| r.select_values += args if args.present? } + relation = clone + relation.select_values += Array.wrap(value) + relation end end def group(*args) - clone.tap {|r| r.group_values += args.flatten if args.present? } + return self if args.blank? + + relation = clone + relation.group_values += args.flatten + relation end def order(*args) - clone.tap {|r| r.order_values += args if args.present? } - end + return self if args.blank? - def reorder(*args) - clone.tap {|r| r.order_values = args if args.present? } + relation = clone + relation.order_values += args.flatten + relation end def joins(*args) + return self if args.compact.blank? + + relation = clone + args.flatten! - clone.tap {|r| r.joins_values += args if args.present? } + relation.joins_values += args + + relation + end + + def bind(value) + relation = clone + relation.bind_values += [value] + relation end def where(opts, *rest) - value = build_where(opts, rest) - value ? clone.tap {|r| r.where_values += Array.wrap(value) } : clone + return self if opts.blank? + + relation = clone + relation.where_values += build_where(opts, rest) + relation end def having(*args) - value = build_where(*args) - clone.tap {|r| r.having_values += Array.wrap(value) if value.present? } + return self if args.blank? + + relation = clone + relation.having_values += build_where(*args) + relation end - def limit(value = true) - clone.tap {|r| r.limit_value = value } + def limit(value) + relation = clone + relation.limit_value = value + relation end - def offset(value = true) - clone.tap {|r| r.offset_value = value } + def offset(value) + relation = clone + relation.offset_value = value + relation end def lock(locks = true) + relation = clone + case locks when String, TrueClass, NilClass - clone.tap {|r| r.lock_value = locks || true } + relation.lock_value = locks || true else - clone.tap {|r| r.lock_value = false } + relation.lock_value = false end + + relation end def readonly(value = true) - clone.tap {|r| r.readonly_value = value } + relation = clone + relation.readonly_value = value + relation end - def create_with(value = true) - clone.tap {|r| r.create_with_value = value } + def create_with(value) + relation = clone + relation.create_with_value = value && (@create_with_value || {}).merge(value) + relation end - def from(value = true) - clone.tap {|r| r.from_value = value } + def from(value) + relation = clone + relation.from_value = value + relation end - def extending(*modules, &block) - modules << Module.new(&block) if block_given? - clone.tap {|r| r.send(:apply_modules, *modules) } + def extending(*modules) + modules << Module.new(&Proc.new) if block_given? + + return self if modules.empty? + + relation = clone + relation.send(:apply_modules, modules.flatten) + relation end def reverse_order - order_clause = arel.send(:order_clauses).join(', ') - relation = except(:order) + order_clause = arel.order_clauses - unless order_clauses.blank? - relation.order(reverse_sql_order(order_clause)) - else - relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC") - end + order = order_clause.empty? ? + "#{table_name}.#{primary_key} DESC" : + reverse_sql_order(order_clause).join(', ') + + except(:order).order(Arel.sql(order)) end def arel @arel ||= build_arel end - def custom_join_sql(*joins) - arel = table - joins.each do |join| - next if join.blank? + def build_arel + arel = table.from table - @implicit_readonly = true + build_joins(arel, @joins_values) unless @joins_values.empty? - case join - when Hash, Array, Symbol - if array_of_strings?(join) - join_string = join.join(' ') - arel = arel.join(join_string) - end - else - arel = arel.join(join) - end - end - arel.joins(arel) - end + collapse_wheres(arel, (@where_values - ['']).uniq) - def build_arel - arel = table + arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? - arel = build_joins(arel, @joins_values) unless @joins_values.empty? + arel.take(connection.sanitize_limit(@limit_value)) if @limit_value + arel.skip(@offset_value) if @offset_value - @where_values.uniq.each do |where| - next if where.blank? + arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? - case where - when Arel::SqlLiteral - arel = arel.where(where) - else - sql = where.is_a?(String) ? where : where.to_sql - arel = arel.where(Arel::SqlLiteral.new("(#{sql})")) - end - end + arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty? - arel = arel.having(*@having_values.uniq.select{|h| h.present?}) unless @having_values.empty? + build_select(arel, @select_values.uniq) - arel = arel.take(@limit_value) if @limit_value - arel = arel.skip(@offset_value) if @offset_value + arel.from(@from_value) if @from_value + arel.lock(@lock_value) if @lock_value - arel = arel.group(*@group_values.uniq.select{|g| g.present?}) unless @group_values.empty? + arel + end - arel = arel.order(*@order_values.uniq.select{|o| o.present?}) unless @order_values.empty? + private - arel = build_select(arel, @select_values.uniq) + def custom_join_ast(table, joins) + joins = joins.reject { |join| join.blank? } - arel = arel.from(@from_value) if @from_value - arel = arel.lock(@lock_value) if @lock_value + return [] if joins.empty? - arel + @implicit_readonly = true + + joins.map do |join| + case join + when Array + join = Arel.sql(join.join(' ')) if array_of_strings?(join) + when String + join = Arel.sql(join) + end + table.create_string_join(join) + end + end + + def collapse_wheres(arel, wheres) + equalities = wheres.grep(Arel::Nodes::Equality) + + arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty? + + (wheres - equalities).each do |where| + where = Arel.sql(where) if String === where + arel.where(Arel::Nodes::Grouping.new(where)) + end end def build_where(opts, other = []) case opts when String, Array - @klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other)) + [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - PredicateBuilder.new(table.engine).build_from_hash(attributes, table) + PredicateBuilder.build_from_hash(table.engine, attributes, table) else - opts + [opts] end end - private - - def build_joins(relation, joins) - joined_associations = [] - association_joins = [] - - joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq - - joins.each do |join| - association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) + def build_joins(manager, joins) + buckets = joins.group_by do |join| + case join + when String + 'string_join' + when Hash, Symbol, Array + 'association_join' + when ActiveRecord::Associations::JoinDependency::JoinAssociation + 'stashed_join' + when Arel::Nodes::Join + 'join_node' + else + raise 'unknown class: %s' % join.class.name + end end - stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)} + association_joins = buckets['association_join'] || [] + stashed_association_joins = buckets['stashed_join'] || [] + join_nodes = buckets['join_node'] || [] + string_joins = (buckets['string_join'] || []).map { |x| + x.strip + }.uniq - non_association_joins = (joins - association_joins - stashed_association_joins) - custom_joins = custom_join_sql(*non_association_joins) + join_list = custom_join_ast(manager, string_joins) - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins) + join_dependency = ActiveRecord::Associations::JoinDependency.new( + @klass, + association_joins, + join_list + ) + + join_nodes.each do |join| + join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase) + end join_dependency.graft(*stashed_association_joins) @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - to_join = [] - + # FIXME: refactor this to build an AST join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.join_class, association.association_join.first] - to_join << [association_relation.last, association.join_class, association.association_join.last] - else - to_join << [association_relation, association.join_class, association.association_join] - end + association.join_to(manager) end - to_join.each do |tj| - unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] } - joined_associations << tj - relation = relation.join(tj[0], tj[1]).on(*tj[2]) - end - end + manager.join_sources.concat join_nodes.uniq + manager.join_sources.concat join_list - relation.join(custom_joins) + manager end def build_select(arel, selects) - if selects.present? + unless selects.empty? @implicit_readonly = false - # TODO: fix this ugly hack, we should refactor the callers to get an ARel compatible array. - # Before this change we were passing to ARel the last element only, and ARel is capable of handling an array - if selects.all? {|s| s.is_a?(String) || !s.is_a?(Arel::Expression) } && !(selects.last =~ /^COUNT\(/) - arel.project(*selects) - else - arel.project(selects.last) - end + arel.project(*selects) else - arel.project(@klass.quoted_table_name + '.*') + arel.project(@klass.arel_table[Arel.star]) end end def apply_modules(modules) - values = Array.wrap(modules) - @extensions += values if values.present? - values.each {|extension| extend(extension) } + unless modules.empty? + @extensions += modules + modules.each {|extension| extend(extension) } + end end def reverse_sql_order(order_query) - order_query.split(',').each { |s| - if s.match(/\s(asc|ASC)$/) - s.gsub!(/\s(asc|ASC)$/, ' DESC') - elsif s.match(/\s(desc|DESC)$/) - s.gsub!(/\s(desc|DESC)$/, ' ASC') - else - s.concat(' DESC') - end - }.join(',') + order_query.join(', ').split(',').collect do |s| + s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') + end end def array_of_strings?(o) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 7712ad2569..128e0fbd86 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -3,71 +3,109 @@ require 'active_support/core_ext/object/blank' module ActiveRecord module SpawnMethods def merge(r) + return self unless r + return to_a & r if r.is_a?(Array) + merged_relation = clone - return merged_relation unless r - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - [:joins, :where]).each do |method| + Relation::ASSOCIATION_METHODS.each do |method| value = r.send(:"#{method}_values") - merged_relation.send(:"#{method}_values=", value) if value.present? - end - - merged_relation = merged_relation.joins(r.joins_values) - merged_wheres = @where_values - - r.where_values.each do |w| - if w.is_a?(Arel::Predicates::Equality) - merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name } + unless value.empty? + if method == :includes + merged_relation = merged_relation.includes(value) + else + merged_relation.send(:"#{method}_values=", value) + end end + end - merged_wheres += [w] + (Relation::MULTI_VALUE_METHODS - [:joins, :where]).each do |method| + value = r.send(:"#{method}_values") + merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present? + end + + merged_relation.joins_values += r.joins_values + + merged_wheres = @where_values + r.where_values + + unless @where_values.empty? + # Remove duplicates, last one wins. + seen = Hash.new { |h,table| h[table] = {} } + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false + if w.respond_to?(:operator) && w.operator == :== + name = w.left.name + table = w.left.relation.name + nuke = seen[table][name] + seen[table][name] = true + end + nuke + }.reverse end merged_relation.where_values = merged_wheres - Relation::SINGLE_VALUE_METHODS.reject {|m| m == :lock}.each do |method| - unless (value = r.send(:"#{method}_value")).nil? - merged_relation.send(:"#{method}_value=", value) - end + (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method| + value = r.send(:"#{method}_value") + merged_relation.send(:"#{method}_value=", value) unless value.nil? end merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value + # Apply scope extension modules merged_relation.send :apply_modules, r.extensions merged_relation end - alias :& :merge - + # Removes from the query the condition(s) specified in +skips+. + # + # Example: + # + # Post.order('id asc').except(:order) # discards the order condition + # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order + # def except(*skips) result = self.class.new(@klass, table) - (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) unless skips.include?(method) + ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| + result.send(:"#{method}_values=", send(:"#{method}_values")) end - Relation::SINGLE_VALUE_METHODS.each do |method| - result.send(:"#{method}_value=", send(:"#{method}_value")) unless skips.include?(method) + (Relation::SINGLE_VALUE_METHODS - skips).each do |method| + result.send(:"#{method}_value=", send(:"#{method}_value")) end + # Apply scope extension modules + result.send(:apply_modules, extensions) + result end + # Removes any condition from the query other than the one(s) specified in +onlies+. + # + # Example: + # + # Post.order('id asc').only(:where) # discards the order condition + # Post.order('id asc').only(:where, :order) # uses the specified order + # def only(*onlies) result = self.class.new(@klass, table) - onlies.each do |only| - if (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).include?(only) - result.send(:"#{only}_values=", send(:"#{only}_values")) - elsif Relation::SINGLE_VALUE_METHODS.include?(only) - result.send(:"#{only}_value=", send(:"#{only}_value")) - else - raise "Invalid argument : #{only}" - end + ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method| + result.send(:"#{method}_values=", send(:"#{method}_values")) + end + + (Relation::SINGLE_VALUE_METHODS & onlies).each do |method| + result.send(:"#{method}_value=", send(:"#{method}_value")) end + # Apply scope extension modules + result.send(:apply_modules, extensions) + result end @@ -79,19 +117,16 @@ module ActiveRecord return relation unless options options.assert_valid_keys(VALID_FIND_OPTIONS) + finders = options.dup + finders.delete_if { |key, value| value.nil? && key != :limit } - [:joins, :select, :group, :having, :limit, :offset, :from, :lock, :readonly].each do |finder| - relation = relation.send(finder, options[finder]) if options.has_key?(finder) - end - - # Give precedence to newly-applied orders and groups to play nicely with with_scope - [:group, :order].each do |finder| - relation.send("#{finder}_values=", Array.wrap(options[finder]) + relation.send("#{finder}_values")) if options.has_key?(finder) + ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| + relation = relation.send(finder, finders[finder]) end - relation = relation.where(options[:conditions]) if options.has_key?(:conditions) - relation = relation.includes(options[:include]) if options.has_key?(:include) - relation = relation.extending(options[:extend]) if options.has_key?(:extend) + relation = relation.where(finders[:conditions]) if options.has_key?(:conditions) + relation = relation.includes(finders[:include]) if options.has_key?(:include) + relation = relation.extending(finders[:extend]) if options.has_key?(:extend) relation end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb new file mode 100644 index 0000000000..0465b21e88 --- /dev/null +++ b/activerecord/lib/active_record/result.rb @@ -0,0 +1,34 @@ +module ActiveRecord + ### + # This class encapsulates a Result returned from calling +exec+ on any + # database connection adapter. For example: + # + # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo') + # x # => #<ActiveRecord::Result:0xdeadbeef> + class Result + include Enumerable + + attr_reader :columns, :rows + + def initialize(columns, rows) + @columns = columns + @rows = rows + @hash_rows = nil + end + + def each + hash_rows.each { |row| yield row } + end + + def to_hash + hash_rows + end + + private + def hash_rows + @hash_rows ||= @rows.map { |row| + Hash[@columns.zip(row)] + } + end + end +end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index c1bc3214ea..d815ab05ac 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -30,10 +30,8 @@ module ActiveRecord # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. class Schema < Migration - private_class_method :new - - def self.migrations_path - ActiveRecord::Migrator.migrations_path + def migrations_paths + ActiveRecord::Migrator.migrations_paths end # Eval the given block. All methods available to the current connection @@ -48,11 +46,12 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - instance_eval(&block) + schema = new + schema.instance_eval(&block) unless info[:version].blank? initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], migrations_path) + assume_migrated_upto_version(info[:version], schema.migrations_paths) end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e9af20e1b6..a893c0ad85 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -41,11 +41,11 @@ module ActiveRecord define_params = @version ? ":version => #{@version}" : "" stream.puts <<HEADER -# This file is auto-generated from the current state of the database. Instead +# This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your +# Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations @@ -83,7 +83,7 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) - pk, pk_seq = @connection.pk_and_sequence_for(table) + pk, _ = @connection.pk_and_sequence_for(table) elsif @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end @@ -114,16 +114,16 @@ HEADER column.type.to_s end spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' - spec[:precision] = column.precision.inspect if !column.precision.nil? - spec[:scale] = column.scale.inspect if !column.scale.nil? - spec[:null] = 'false' if !column.null + spec[:precision] = column.precision.inspect if column.precision + spec[:scale] = column.scale.inspect if column.scale + spec[:null] = 'false' unless column.null spec[:default] = default_string(column.default) if column.has_default? (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} spec end.compact # find all migration keys used in this table - keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten + keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map{ |k| k.keys }.flatten # figure out the lengths for each column based on above keys lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } @@ -176,13 +176,15 @@ HEADER def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| - statement_parts = [ ('add_index ' + index.table.inspect) ] - statement_parts << index.columns.inspect - statement_parts << (':name => ' + index.name.inspect) + statement_parts = [ + ('add_index ' + index.table.inspect), + index.columns.inspect, + (':name => ' + index.name.inspect), + ] statement_parts << ':unique => true' if index.unique - index_lengths = index.lengths.compact if index.lengths.is_a?(Array) - statement_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present? + index_lengths = (index.lengths || []).compact + statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? ' ' + statement_parts.join(', ') end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 2d8bd184f3..2bde06f562 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -5,7 +5,7 @@ module ActiveRecord #:nodoc: include ActiveModel::Serializers::JSON def serializable_hash(options = nil) - options ||= {} + options = options.try(:clone) || {} options[:except] = Array.wrap(options[:except]).map { |n| n.to_s } options[:except] |= Array.wrap(self.class.inheritance_column) @@ -22,8 +22,8 @@ module ActiveRecord #:nodoc: end private - # Add associations specified via the <tt>:includes</tt> option. - # + # Add associations specified via the <tt>:include</tt> option. + # # Expects a block that takes as arguments: # +association+ - name of the association # +records+ - the association record(s) to be serialized @@ -45,7 +45,7 @@ module ActiveRecord #:nodoc: send(association) end - unless records.nil? + if records association_options = include_has_options ? include_associations[association] : base_only_or_except opts = options.merge(association_options) yield(association, records, opts) diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index b2d4a48945..8c4adf7116 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -80,7 +80,7 @@ module ActiveRecord #:nodoc: # closure created by a Proc, to_xml can be used to add elements that normally fall # outside of the scope of the model -- for example, generating and appending URLs # associated with models. - # + # # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } # firm.to_xml :procs => [ proc ] # @@ -226,17 +226,17 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type - type = @serializable.class.serialized_attributes.has_key?(name) ? - super : @serializable.class.columns_hash[name].type + klass = @serializable.class + type = if klass.serialized_attributes.key?(name) + super + elsif klass.columns_hash.key?(name) + klass.columns_hash[name].type + else + NilClass + end - case type - when :text - :string - when :time - :datetime - else - type - end + { :text => :string, + :time => :datetime }[type] || type end protected :compute_type end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index becde0fbfd..7e77aefb21 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -9,7 +9,7 @@ module ActiveRecord # +id+ (numeric primary key), # +session_id+ (text, or longtext if your session data exceeds 65K), and # +data+ (text or longtext; careful if your session data exceeds 65KB). - # + # # The +session_id+ column should always be indexed for speedy lookups. # Session data is marshaled to the +data+ column in Base64 format. # If the data you write is larger than the column's size limit, @@ -59,17 +59,17 @@ module ActiveRecord end def drop_table! - connection.execute "DROP TABLE #{table_name}" + connection_pool.clear_table_cache!(table_name) + connection.drop_table table_name end def create_table! - connection.execute <<-end_sql - CREATE TABLE #{table_name} ( - id #{connection.type_to_sql(:primary_key)}, - #{connection.quote_column_name(session_id_column)} VARCHAR(255) UNIQUE, - #{connection.quote_column_name(data_column_name)} TEXT - ) - end_sql + connection_pool.clear_table_cache!(table_name) + connection.create_table(table_name) do |t| + t.string session_id_column, :limit => 255 + t.text data_column_name + end + connection.add_index table_name, session_id_column, :unique => true end end @@ -205,6 +205,7 @@ module ActiveRecord class << self alias :data_column_name :data_column + remove_method :connection def connection @@connection ||= ActiveRecord::Base.connection end @@ -289,6 +290,7 @@ module ActiveRecord self.session_class = Session SESSION_RECORD_KEY = 'rack.session.record' + ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY private def get_session(env, sid) @@ -300,7 +302,7 @@ module ActiveRecord end end - def set_session(env, sid, session_data) + def set_session(env, sid, session_data, options) Base.silence do record = get_session_model(env, sid) record.data = session_data @@ -317,12 +319,15 @@ module ActiveRecord sid end - def destroy(env) + def destroy_session(env, session_id, options) if sid = current_session_id(env) Base.silence do get_session_model(env, sid).destroy + env[SESSION_RECORD_KEY] = nil end end + + generate_sid unless options[:drop] end def get_session_model(env, sid) diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index e61a378d17..29efbbcb8c 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,8 +1,18 @@ module ActiveRecord # = Active Record Test Case - # + # # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: + setup :cleanup_identity_map + + def setup + cleanup_identity_map + end + + def cleanup_identity_map + ActiveRecord::IdentityMap.clear + end + def assert_date_from_db(expected, actual, message = nil) # SybaseAdapter doesn't have a separate column type just for dates, # so the time is in the string and incorrectly formatted @@ -16,12 +26,13 @@ module ActiveRecord def assert_sql(*patterns_to_match) $queries_executed = [] yield + $queries_executed ensure failed_patterns = [] patterns_to_match.each do |pattern| failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql } end - assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" end def assert_queries(num = 1) @@ -36,21 +47,6 @@ module ActiveRecord assert_queries(0, &block) end - def self.use_concurrent_connections - setup :connection_allow_concurrency_setup - teardown :connection_allow_concurrency_teardown - end - - def connection_allow_concurrency_setup - @connection = ActiveRecord::Base.remove_connection - ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true})) - end - - def connection_allow_concurrency_teardown - ActiveRecord::Base.clear_all_connections! - ActiveRecord::Base.establish_connection(@connection) - end - def with_kcode(kcode) if RUBY_VERSION < '1.9' orig_kcode, $KCODE = $KCODE, kcode diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 5531d12a41..1511c71ffc 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,72 +1,83 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord # = Active Record Timestamp - # + # # Active Record automatically timestamps create and update operations if the - # table has fields named <tt>created_at/created_on</tt> or + # table has fields named <tt>created_at/created_on</tt> or # <tt>updated_at/updated_on</tt>. # # Timestamping can be turned off by setting: # - # <tt>ActiveRecord::Base.record_timestamps = false</tt> + # config.active_record.record_timestamps = false # # Timestamps are in the local timezone by default but you can use UTC by setting: # - # <tt>ActiveRecord::Base.default_timezone = :utc</tt> + # config.active_record.default_timezone = :utc # # == Time Zone aware attributes # # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. # - # ActiveRecord::Base.time_zone_aware_attributes = true + # config.active_record.time_zone_aware_attributes = true # # This feature can easily be turned off by assigning value <tt>false</tt> . # - # If your attributes are time zone aware and you desire to skip time zone conversion for certain - # attributes then you can do following: + # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone + # when reading certain attributes then you can do following: # - # Topic.skip_time_zone_conversion_for_attributes = [:written_on] + # class Topic < ActiveRecord::Base + # self.skip_time_zone_conversion_for_attributes = [:written_on] + # end module Timestamp extend ActiveSupport::Concern included do - class_inheritable_accessor :record_timestamps, :instance_writer => false + class_attribute :record_timestamps, :instance_writer => false self.record_timestamps = true end private def create #:nodoc: - if record_timestamps + if self.record_timestamps current_time = current_time_from_proper_timezone - timestamp_attributes_for_create.each do |column| + all_timestamp_attributes.each do |column| write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil? end - - timestamp_attributes_for_update_in_model.each do |column| - write_attribute(column.to_s, current_time) if self.send(column).nil? - end end super end def update(*args) #:nodoc: - record_update_timestamps if !partial_updates? || changed? + if should_record_timestamps? + current_time = current_time_from_proper_timezone + + timestamp_attributes_for_update_in_model.each do |column| + column = column.to_s + next if attribute_changed?(column) + write_attribute(column, current_time) + end + end super end - def record_update_timestamps #:nodoc: - return unless record_timestamps - current_time = current_time_from_proper_timezone - timestamp_attributes_for_update_in_model.inject({}) do |hash, column| - hash[column.to_s] = write_attribute(column.to_s, current_time) - hash - end + def should_record_timestamps? + self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) + end + + def timestamp_attributes_for_create_in_model + timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) } + end + + def timestamp_attributes_for_update_in_model + timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } end - def timestamp_attributes_for_update_in_model #:nodoc: - timestamp_attributes_for_update.select { |elem| respond_to?(elem) } + def all_timestamp_attributes_in_model + timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model end def timestamp_attributes_for_update #:nodoc: @@ -78,9 +89,9 @@ module ActiveRecord end def all_timestamp_attributes #:nodoc: - timestamp_attributes_for_update + timestamp_attributes_for_create + timestamp_attributes_for_create + timestamp_attributes_for_update end - + def current_time_from_proper_timezone #:nodoc: self.class.default_timezone == :utc ? Time.now.utc : Time.now end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index a7709b9489..60d4c256c4 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,6 +11,7 @@ module ActiveRecord included do define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name] end + # = Active Record Transactions # # Transactions are protective blocks where SQL statements are only permanent @@ -130,7 +131,7 @@ module ActiveRecord # # +transaction+ calls can be nested. By default, this makes all database # statements in the nested transaction block become part of the parent - # transaction. For example: + # transaction. For example, the following behavior may be surprising: # # User.transaction do # User.create(:username => 'Kotori') @@ -140,12 +141,15 @@ module ActiveRecord # end # end # - # User.find(:all) # => empty + # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt> + # exception in the nested block does not issue a ROLLBACK. Since these exceptions + # are captured in transaction blocks, the parent block does not see it and the + # real transaction is committed. # - # It is also possible to requires a sub-transaction by passing - # <tt>:requires_new => true</tt>. If anything goes wrong, the - # database rolls back to the beginning of the sub-transaction - # without rolling back the parent transaction. For example: + # In order to get a ROLLBACK for the nested transaction you may ask for a real + # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong, + # the database rolls back to the beginning of the sub-transaction without rolling + # back the parent transaction. If we add it to the previous example: # # User.transaction do # User.create(:username => 'Kotori') @@ -155,12 +159,12 @@ module ActiveRecord # end # end # - # User.find(:all) # => Returns only Kotori + # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.) # # Most databases don't support true nested transactions. At the time of # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested - # transactions by using savepoints. See + # transactions by using savepoints on MySQL and PostgreSQL. See # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html # for more information about savepoints. # @@ -224,8 +228,8 @@ module ActiveRecord end # See ActiveRecord::Transactions::ClassMethods for detailed documentation. - def transaction(&block) - self.class.transaction(&block) + def transaction(options = {}, &block) + self.class.transaction(options, &block) end def destroy #:nodoc: @@ -247,6 +251,7 @@ module ActiveRecord remember_transaction_record_state yield rescue Exception + IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state raise ensure @@ -255,7 +260,7 @@ module ActiveRecord # Call the after_commit callbacks def committed! #:nodoc: - _run_commit_callbacks + run_callbacks :commit ensure clear_transaction_record_state end @@ -263,7 +268,7 @@ module ActiveRecord # Call the after rollback callbacks. The restore_state argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state = false) #:nodoc: - _run_rollback_callbacks + run_callbacks :rollback ensure restore_transaction_record_state(force_restore_state) end @@ -297,8 +302,8 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc @_start_transaction_state ||= {} + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record end unless @_start_transaction_state.include?(:destroyed) @@ -321,16 +326,14 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - if restore_state - @attributes = @attributes.dup if @attributes.frozen? - @new_record = restore_state[:new_record] - @destroyed = restore_state[:destroyed] - if restore_state[:id] - self.id = restore_state[:id] - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end + @attributes = @attributes.dup if @attributes.frozen? + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state.has_key?(:id) + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index b98fd353aa..d73fce9fd0 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Validations + # = Active Record RecordInvalid # # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the # +record+ method to retrieve the record which did not validate. @@ -18,6 +18,13 @@ module ActiveRecord end end + # = Active Record Validations + # + # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt> + # all of which accept the <tt>:on</tt> argument to define the context where the + # validations are active. Active Record will always supply either the context of + # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a + # <tt>new_record?</tt>. module Validations extend ActiveSupport::Concern include ActiveModel::Validations @@ -37,7 +44,7 @@ module ActiveRecord end end - # The validation process on save can be skipped by passing false. The regular Base#save method is + # The validation process on save can be skipped by passing :validate => false. The regular Base#save method is # replaced with this when the validations module is mixed in, which it is by default. def save(options={}) perform_validations(options) ? super : false @@ -49,33 +56,25 @@ module ActiveRecord perform_validations(options) ? super : raise(RecordInvalid.new(self)) end - # Runs all the specified validations and returns true if no errors were added otherwise false. + # Runs all the validations within the specified context. Returns true if no errors are found, + # false otherwise. + # + # If the argument is false (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is true, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. def valid?(context = nil) context ||= (new_record? ? :create : :update) output = super(context) - - deprecated_callback_method(:validate) - deprecated_callback_method(:"validate_on_#{context}") - errors.empty? && output end protected def perform_validations(options={}) - perform_validation = case options - when Hash - options[:validate] != false - else - ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller - options - end - - if perform_validation - valid?(options.is_a?(Hash) ? options[:context] : nil) - else - true - end + perform_validation = options[:validate] != false + perform_validation ? valid?(options[:context]) : true end end end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 15b587de45..3a783aeb00 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -27,13 +27,15 @@ module ActiveRecord # # this would specify a circular dependency and cause infinite recursion. # - # NOTE: This validation will not fail if the association hasn't been assigned. If you want to - # ensure that the association is both present and guaranteed to be valid, you also need to + # NOTE: This validation will not fail if the association hasn't been assigned. If you want to + # ensure that the association is both present and guaranteed to be valid, you also need to # use +validates_presence_of+. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <tt>:on</tt> - Specifies when this validation is active. Runs in all + # validation contexts by default (+nil+), other options are <tt>:create</tt> + # and <tt>:update</tt>. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index bf863c7063..9cd6c26322 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,19 +14,23 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) - table = finder_class.unscoped - table_name = record.class.quoted_table_name - sql, params = mount_sql_and_params(finder_class, table_name, attribute, value) + coder = record.class.serialized_attributes[attribute.to_s] - relation = table.where(sql, *params) + if value && coder + value = coder.dump value + end + + sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) + + relation = finder_class.unscoped.where(sql, *params) Array.wrap(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) relation = relation.where(scope_item => scope_value) end - unless record.new_record? + if record.persisted? # TODO : This should be in Arel relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id)) end @@ -78,24 +82,29 @@ module ActiveRecord end module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. + # Validates whether the value of the specified attributes are unique across the system. # Useful for making sure that only one user # can be named "davidhh". # # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id + # validates_uniqueness_of :user_name # end # - # It can also validate whether the value of the specified attributes are unique based on multiple - # scope parameters. For example, making sure that a teacher can only be on the schedule once + # It can also validate whether the value of the specified attributes are unique based on a scope parameter: + # + # class Person < ActiveRecord::Base + # validates_uniqueness_of :user_name, :scope => :account_id + # end + # + # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once # per semester for a particular class. # # class TeacherSchedule < ActiveRecord::Base # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] # end # - # When the record is created, a check is performed to make sure that no record exists in the database - # with the given value for the specified attribute (that maps to a column). When the record is updated, + # When the record is created, a check is performed to make sure that no record exists in the database + # with the given value for the specified attribute (that maps to a column). When the record is updated, # the same check is made but disregarding the record itself. # # Configuration options: @@ -105,11 +114,11 @@ module ActiveRecord # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or - # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should # return or evaluate to a true or false value. # # === Concurrency and integrity @@ -149,33 +158,32 @@ module ActiveRecord # | # title! # # This could even happen if you use transactions with the 'serializable' - # isolation level. There are several ways to get around this problem: + # isolation level. The best way to work around this problem is to add a unique + # index to the database table using + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the + # rare case that a race condition occurs, the database will guarantee + # the field's uniqueness. + # + # When the database catches such a duplicate insertion, + # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # exception. You can either choose to let this error propagate (which + # will result in the default Rails exception page being shown), or you + # can catch it and restart the transaction (e.g. by telling the user + # that the title already exists, and asking him to re-enter the title). + # This technique is also known as optimistic concurrency control: + # http://en.wikipedia.org/wiki/Optimistic_concurrency_control # - # - By locking the database table before validating, and unlocking it after - # saving. However, table locking is very expensive, and thus not - # recommended. - # - By locking a lock file before validating, and unlocking it after saving. - # This does not work if you've scaled your Rails application across - # multiple web servers (because they cannot share lock files, or cannot - # do that efficiently), and thus not recommended. - # - Creating a unique index on the field, by using - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the - # rare case that a race condition occurs, the database will guarantee - # the field's uniqueness. - # - # When the database catches such a duplicate insertion, - # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid - # exception. You can either choose to let this error propagate (which - # will result in the default Rails exception page being shown), or you - # can catch it and restart the transaction (e.g. by telling the user - # that the title already exists, and asking him to re-enter the title). - # This technique is also known as optimistic concurrency control: - # http://en.wikipedia.org/wiki/Optimistic_concurrency_control - # - # Active Record currently provides no way to distinguish unique - # index constraint errors from other types of database errors, so you - # will have to parse the (database-specific) exception message to detect - # such a case. + # The bundled ActiveRecord::ConnectionAdapters distinguish unique index + # constraint errors from other types of database errors by throwing an + # ActiveRecord::RecordNotUnique exception. + # For other adapters you will have to parse the (database-specific) exception + # message to detect such a case. + # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: + # * ActiveRecord::ConnectionAdapters::MysqlAdapter + # * ActiveRecord::ConnectionAdapters::Mysql2Adapter + # * ActiveRecord::ConnectionAdapters::SQLiteAdapter + # * ActiveRecord::ConnectionAdapters::SQLite3Adapter + # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter # def validates_uniqueness_of(*attr_names) validates_with UniquenessValidator, _merge_attributes(attr_names) diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index a467ffa960..0667be7d23 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,10 +1,10 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end |