aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations')
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb52
-rw-r--r--activerecord/lib/active_record/associations/association.rb262
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb558
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb314
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb98
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb74
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb53
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb83
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb75
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb61
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency.rb216
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb258
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb26
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb80
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb549
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb127
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb141
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb121
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb159
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb155
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb46
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb215
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb259
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb24
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb78
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb177
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb126
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb58
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb23
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb9
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb67
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb55
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb281
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb300
42 files changed, 3173 insertions, 2259 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 6428fcec0a..6fc2bfdb31 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -5,32 +5,44 @@ module ActiveRecord
# 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 = nil)
+ 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 # TODO: Is this conditional necessary?
+ 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?
+ initialize_count_for(table_name) if aliases[table_name].nil?
- if @aliases[table_name].zero?
+ if aliases[table_name].zero?
# If it's zero, we can have our table_name
- @aliases[table_name] = 1
+ 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?
+ initialize_count_for(aliased_name) if aliases[aliased_name].nil?
# Update the count
- @aliases[aliased_name] += 1
+ aliases[aliased_name] += 1
- if @aliases[aliased_name] > 1
- "#{truncate(aliased_name)}_#{@aliases[aliased_name]}"
+ if aliases[aliased_name] > 1
+ "#{truncate(aliased_name)}_#{aliases[aliased_name]}"
else
aliased_name
end
@@ -44,29 +56,21 @@ module ActiveRecord
private
def initialize_count_for(name)
- @aliases[name] = 0
+ aliases[name] = 0
- unless @table_joins.nil? || Arel::Table === @table_joins
+ 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.grep(Arel::Nodes::Join).map { |join|
- right = join.right
- case right
- when Arel::Table
- right.name.downcase == name ? 1 : 0
- when String
- # Table names + table aliases
- right.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
- ).size
- else
- 0
- end
+ 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]
+ aliases[name]
end
def truncate(name)
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
new file mode 100644
index 0000000000..86904ea2bc
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -0,0 +1,262 @@
+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
+ # SingularAssociaton
+ # 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 targed_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
+ @association_scope = association_scope if klass
+ end
+
+ def association_scope
+ scope = klass.unscoped
+ scope = scope.create_with(creation_attributes)
+ scope = scope.apply_finder_options(options.slice(:readonly, :include))
+ scope = scope.where(interpolate(options[:conditions]))
+ if select = select_value
+ scope = scope.select(select)
+ end
+ scope = scope.extending(*Array.wrap(options[:extend]))
+ scope.where(construct_owner_conditions)
+ end
+
+ def aliased_table
+ klass.arel_table
+ 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 select_value
+ options[:select]
+ end
+
+ # Implemented by (some) subclasses
+ def creation_attributes
+ { }
+ end
+
+ # Returns a hash linking the owner to the association represented by the reflection
+ def construct_owner_attributes(reflection = reflection)
+ attributes = {}
+ if reflection.macro == :belongs_to
+ attributes[reflection.association_primary_key] = owner[reflection.foreign_key]
+ else
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
+
+ if options[:as]
+ attributes["#{options[:as]}_type"] = owner.class.base_class.name
+ end
+ end
+ attributes
+ end
+
+ # Builds an array of arel nodes from the owner attributes hash
+ def construct_owner_conditions(table = aliased_table, reflection = reflection)
+ conditions = construct_owner_attributes(reflection).map do |attr, value|
+ table[attr].eq(value)
+ end
+ table.create_and(conditions)
+ end
+
+ # Sets the owner attributes on the given record
+ def set_owner_attributes(record)
+ if owner.persisted?
+ construct_owner_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 abb17a11c6..0000000000
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ /dev/null
@@ -1,558 +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:
- 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(@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)
-
- with_scope(:find => @scope[:find].slice(:conditions, :order)) 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?
- args = args[1..-1] if args.first.kind_of?(Hash) && args.first.empty?
- @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+ 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, options = {})
- column_name, options = nil, column_name if column_name.is_a?(Hash)
-
- if @reflection.options[:counter_sql] || @reflection.options[:finder_sql]
- unless 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 @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, @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
- unless length.zero? # forces load_target if it hasn't happened already
- if @reflection.options[:dependent] == :destroy
- destroy_all
- else
- delete_all
- end
- 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 = {}
- 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) }
-
- 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)
- return include_in_memory?(record) if record.new_record?
- load_target if @reflection.options[:finder_sql] && !loaded?
- loaded? ? @target.include?(record) : 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 load_target
- if !@owner.new_record? || foreign_key_present
- begin
- unless 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)
- f.attributes.except(*keys).each do |k,v|
- t.send("#{k}=", v)
- end
- 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))
- super
- elsif @reflection.klass.scopes[method]
- @_named_scopes_cache ||= {}
- @_named_scopes_cache[method] ||= {}
- @_named_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) }
- else
- with_scope(@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
-
- def custom_counter_sql
- if @reflection.options[:counter_sql]
- counter_sql = @reflection.options[:counter_sql]
- else
- # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
- counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
- end
-
- interpolate_sql(counter_sql)
- end
-
- def custom_finder_sql
- interpolate_sql(@reflection.options[:finder_sql])
- 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(custom_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_persisted!
-
- scoped_where = scoped.where_values_hash
- create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create]
- record = @reflection.klass.send(:with_scope, :create => create_scope) 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.send(full_callback_name.to_sym) || []
- end
-
- def ensure_owner_is_persisted!
- unless @owner.persisted?
- 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) && !args.first.empty?) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
- @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
- end
-
- def include_in_memory?(record)
- if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- @owner.send(proxy_reflection.through_reflection.name.to_sym).any? do |source|
- target = source.send(proxy_reflection.source_reflection.name)
- target.respond_to?(:include?) ? target.include?(record) : target == record
- end
- else
- @target.include?(record)
- end
- 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 252ff7e7ea..0000000000
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ /dev/null
@@ -1,314 +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)$|^__|^respond_to_missing|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
- construct_scope
- 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 if @owner.persisted?
- record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
- else
- if @owner.persisted?
- 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
-
- # Construct the scope used for find/create queries on the target
- def construct_scope
- @scope = {
- :find => construct_find_scope,
- :create => construct_create_scope
- }
- end
-
- # Implemented by subclasses
- def construct_find_scope
- raise NotImplementedError
- end
-
- # Implemented by (some) subclasses
- def construct_create_scope
- {}
- 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? && (!@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/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index b438620c8f..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.persisted?
- @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.persisted? && 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) if record.persisted?
- @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,50 +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
- options = @reflection.options.dup.slice(:select, :include, :readonly)
+ 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
- the_target = with_scope(:find => @scope[:find]) do
- @reflection.klass.send(find_method,
- @owner[@reflection.primary_key_name],
- options
- ) if @owner[@reflection.primary_key_name]
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
+ end
end
- set_inverse_instance(the_target, @owner)
- the_target
end
- def construct_find_scope
- { :conditions => conditions }
+ # 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 foreign_key_present
- !@owner[@reflection.primary_key_name].nil?
+ 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 a0df860623..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
- 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)
- if inverse_relationship
- record.send(:"set_#{inverse_relationship.name}_target", instance)
- end
+ def replace_keys(record)
+ super
+ owner[reflection.foreign_type] = record && record.class.base_class.name
end
- def construct_find_scope
- { :conditions => conditions }
+ def different_target?(record)
+ super || record.class != klass
end
- def find_target
- return nil if association_class.nil?
-
- target = association_class.send(:with_scope, :find => @scope[:find]) do
- 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..e40b32826a
--- /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)
+ redefine_destroy
+ reflection
+ end
+
+ private
+
+ def redefine_destroy
+ # Don't use a before_destroy callback since users' before_destroy
+ # callbacks will be executed after the association is wiped out.
+ name = self.name
+ model.send(:include, Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy # def destroy
+ super # super
+ #{name}.clear # posts.clear
+ end # end
+ RUBY
+ })
+ 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/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
deleted file mode 100644
index 78b634a26c..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
+++ /dev/null
@@ -1,216 +0,0 @@
-require 'active_record/associations/class_methods/join_dependency/join_part'
-require 'active_record/associations/class_methods/join_dependency/join_base'
-require 'active_record/associations/class_methods/join_dependency/join_association'
-
-module ActiveRecord
- module Associations
- module ClassMethods
- class JoinDependency # :nodoc:
- 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 |name, assoc|
- association = construct(parent, 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.instance_variable_defined?("@#{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
- collection = record.send(join_part.reflection.name)
- collection.loaded
- collection.target.push(association)
- collection.__send__(:set_inverse_instance, association, record)
- 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)
- association_proxy = record.send("set_#{join_part.reflection.name}_target", association)
- association_proxy.__send__(:set_inverse_instance, association, record)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
deleted file mode 100644
index 5cc96a7aef..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
+++ /dev/null
@@ -1,258 +0,0 @@
-module ActiveRecord
- module Associations
- module ClassMethods
- class JoinDependency # :nodoc:
- class JoinAssociation < JoinPart # :nodoc:
- # 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
-
- attr_reader :aliased_prefix
-
- delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
- delegate :alias_tracker, :to => :join_dependency
-
- 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 }"
-
- setup_tables
- 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)
- # The chain starts with the target table, but we want to end with it here (makes
- # more sense in this context)
- chain = through_reflection_chain.reverse
-
- foreign_table = parent_table
- index = 0
-
- chain.each do |reflection|
- table = @tables[index]
- conditions = []
-
- if reflection.source_reflection.nil?
- case reflection.macro
- when :belongs_to
- key = reflection.association_primary_key
- foreign_key = reflection.primary_key_name
- when :has_many, :has_one
- key = reflection.primary_key_name
- foreign_key = reflection.active_record_primary_key
-
- conditions << polymorphic_conditions(reflection, table)
- when :has_and_belongs_to_many
- # For habtm, we need to deal with the join table at the same time as the
- # target table (because unlike a :through association, there is no reflection
- # to represent the join table)
- table, join_table = table
-
- join_key = reflection.primary_key_name
- join_foreign_key = reflection.active_record.primary_key
-
- relation = relation.join(join_table, join_type).on(
- join_table[join_key].
- eq(foreign_table[join_foreign_key])
- )
-
- # We've done the first join now, so update the foreign_table for the second
- foreign_table = join_table
-
- key = reflection.klass.primary_key
- foreign_key = reflection.association_foreign_key
- end
- else
- case reflection.source_reflection.macro
- when :belongs_to
- key = reflection.association_primary_key
- foreign_key = reflection.primary_key_name
-
- conditions << source_type_conditions(reflection, foreign_table)
- when :has_many, :has_one
- key = reflection.primary_key_name
- foreign_key = reflection.source_reflection.active_record_primary_key
- when :has_and_belongs_to_many
- table, join_table = table
-
- join_key = reflection.primary_key_name
- join_foreign_key = reflection.klass.primary_key
-
- relation = relation.join(join_table, join_type).on(
- join_table[join_key].
- eq(foreign_table[join_foreign_key])
- )
-
- foreign_table = join_table
-
- key = reflection.klass.primary_key
- foreign_key = reflection.association_foreign_key
- end
- end
-
- conditions << table[key].eq(foreign_table[foreign_key])
-
- conditions << reflection_conditions(index, table)
- conditions << sti_conditions(reflection, table)
-
- ands = relation.create_and(conditions.flatten.compact)
-
- join = relation.create_join(
- relation.froms.first,
- table,
- relation.create_on(ands),
- join_type)
-
- relation = relation.from(join)
-
- # The current table in this iteration becomes the foreign table in the next
- foreign_table = table
- index += 1
- end
-
- relation
- end
-
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
- def table
- if @tables.last.is_a?(Array)
- @tables.last.first
- else
- @tables.last
- end
- end
-
- def aliased_table_name
- table.table_alias || table.name
- end
-
- protected
-
- def table_alias_for(reflection, join = false)
- name = alias_tracker.pluralize(reflection.name)
- name << "_#{parent_table_name}"
- name << "_join" if join
- name
- end
-
- private
-
- # Generate aliases and Arel::Table instances for each of the tables which we will
- # later generate joins for. We must do this in advance in order to correctly allocate
- # the proper alias.
- def setup_tables
- @tables = through_reflection_chain.map do |reflection|
- aliased_table_name = alias_tracker.aliased_name_for(
- reflection.table_name,
- table_alias_for(reflection, reflection != self.reflection)
- )
-
- table = Arel::Table.new(reflection.table_name, :as => aliased_table_name)
-
- # For habtm, we have two Arel::Table instances related to a single reflection, so
- # we just store them as a pair in the array.
- if reflection.macro == :has_and_belongs_to_many ||
- (reflection.source_reflection &&
- reflection.source_reflection.macro == :has_and_belongs_to_many)
-
- join_table_name = (reflection.source_reflection || reflection).options[:join_table]
-
- aliased_join_table_name = alias_tracker.aliased_name_for(
- join_table_name,
- table_alias_for(reflection, true)
- )
-
- join_table = Arel::Table.new(join_table_name, :as => aliased_join_table_name)
-
- [table, join_table]
- else
- table
- end
- end
-
- # The joins are generated from the through_reflection_chain in reverse order, so
- # reverse the tables too (but it's important to generate the aliases in the 'forward'
- # order, which is why we only do the reversal now.
- @tables.reverse!
-
- @tables
- end
-
- def reflection_conditions(index, table)
- @reflection.through_conditions.reverse[index].map do |condition|
- Arel.sql(sanitize_sql(condition, table.table_alias || table.name))
- end
- end
-
- def sanitize_sql(condition, table_name)
- active_record.send(:sanitize_sql, condition, table_name)
- end
-
- def sti_conditions(reflection, table)
- unless reflection.klass.descends_from_active_record?
- sti_column = table[reflection.klass.inheritance_column]
- sti_condition = sti_column.eq(reflection.klass.sti_name)
- subclasses = reflection.klass.descendants
-
- subclasses.inject(sti_condition) { |attr,subclass|
- attr.or(sti_column.eq(subclass.sti_name))
- }
- end
- end
-
- def source_type_conditions(reflection, foreign_table)
- if reflection.options[:source_type]
- foreign_table[reflection.source_reflection.options[:foreign_type]].
- eq(reflection.options[:source_type])
- end
- end
-
- def polymorphic_conditions(reflection, table)
- if reflection.options[:as]
- table["#{reflection.options[:as]}_type"].
- eq(reflection.active_record.base_class.name)
- end
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb
deleted file mode 100644
index 67567f06df..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module ActiveRecord
- module Associations
- module ClassMethods
- 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
-end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb
deleted file mode 100644
index cd16ae5a8b..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-module ActiveRecord
- module Associations
- module ClassMethods
- 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
-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..f3761bd2c7
--- /dev/null
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -0,0 +1,549 @@
+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 association_scope
+ options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset)
+ super.apply_finder_options(options)
+ 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 select_value
+ super || uniq_select_value
+ end
+
+ def uniq_select_value
+ options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
+ end
+
+ 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 e2ce9aefcf..028630977d 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,134 +1,73 @@
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)
+
+ 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
+ )
- def reset_column_information
- @reflection.reset_column_information
+ owner.connection.insert stmt.to_sql
+ end
+
+ record
end
- def has_primary_key?
- @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
+ def association_scope
+ super.joins(construct_joins)
end
- protected
- def construct_find_options!(options)
- options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins])
- options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
- options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*'))
- 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.map do |column|
- name = column.name
- value = case name.to_s
- when @reflection.primary_key_name.to_s
- @owner.id
- when @reflection.association_foreign_key.to_s
- record.id
- when *timestamps
- timezone
- else
- @owner.send(:quote_value, record[name], column) if record.has_attribute?(name)
- end
- [relation[name], value] unless value.nil?
- end
-
- stmt = relation.compile_insert Hash[attributes]
- @owner.connection.insert stmt.to_sql
- end
-
- 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])
- stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
- and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
+ 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
+ owner.connection.delete stmt.to_sql
end
end
def construct_joins
- "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}"
- end
+ right = join_table
+ left = reflection.klass.arel_table
- def construct_conditions
- sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
- sql << " AND (#{conditions})" if conditions
- sql
- end
+ condition = left[reflection.klass.primary_key].eq(
+ right[reflection.association_foreign_key])
- def construct_find_scope
- {
- :conditions => construct_conditions,
- :joins => construct_joins,
- :readonly => false,
- :order => @reflection.options[:order],
- :include => @reflection.options[:include],
- :limit => @reflection.options[:limit]
- }
+ right.create_join(right, right.create_on(condition))
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
+ def construct_owner_conditions
+ super(join_table)
end
- private
- def create_record(attributes, &block)
- # Can't use Base.create because the foreign key may be a protected attribute.
- ensure_owner_is_persisted!
- if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr) }
- else
- build_record(attributes, &block)
- end
+ def select_value
+ super || reflection.klass.arel_table[Arel.star]
end
- def record_timestamp_columns(record)
- if record.record_timestamps
- record.send(:all_timestamp_attributes).map { |x| x.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 851b10e300..cebf3e477a 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -5,15 +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:
- protected
- def owner_quoted_id(reflection = @reflection)
- if reflection.options[:primary_key]
- @owner.class.quote_value(@owner.send(reflection.options[:primary_key]))
- else
- @owner.quoted_id
- end
- end
+ class HasManyAssociation < CollectionAssociation #:nodoc:
+
+ def insert_record(record, validate = true)
+ set_owner_attributes(record)
+ record.save(:validate => validate)
+ end
+
+ private
# Returns the number of records in this collection.
#
@@ -30,87 +29,73 @@ 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.options[:finder_sql]
- @reflection.klass.count_by_sql(custom_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(@scope[:find].slice(:conditions, :joins, :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
+ @target ||= [] and loaded! if count == 0
- [@reflection.options[:limit], count].compact.min
+ [options[:limit], count].compact.min
end
- def has_cached_counter?
- @owner.attribute_present?(cached_counter_attribute_name)
+ def has_cached_counter?(reflection = reflection)
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
end
- def cached_counter_attribute_name
- "#{@reflection.name}_count"
+ def cached_counter_attribute_name(reflection = reflection)
+ "#{reflection.name}_count"
end
- def insert_record(record, force = false, validate = true)
- set_belongs_to_association_for(record)
- force ? record.save! : record.save(:validate => validate)
- 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)
- stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
- and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id }))
- ).compile_update(relation[@reflection.primary_key_name] => nil)
- @owner.connection.update stmt.to_sql
-
- @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_conditions
- if @reflection.options[:as]
- 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)}"
+ # 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
- sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- end
- sql << " AND (#{conditions})" if conditions
- sql
- end
+ keys = records.map { |r| r[reflection.association_primary_key] }
+ scope = scoped.where(reflection.association_primary_key => keys)
- def construct_find_scope
- {
- :conditions => construct_conditions,
- :readonly => false,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :include => @reflection.options[:include]
- }
- end
-
- def construct_create_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- create_scoping
+ if method == :delete_all
+ update_counter(-scope.delete_all)
+ else
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
+ end
+ end
end
- def we_can_set_the_inverse_on_this?(record)
- @reflection.inverse_of
- end
+ alias creation_attributes construct_owner_attributes
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 0b00132ad9..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,109 +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
-
- def build(attributes = {}, &block)
- ensure_not_nested
- super
- end
+ include ThroughAssociation
alias_method :new, :build
- def create!(attrs = nil)
- create_record(attrs, true)
- end
-
- def create(attrs = nil)
- create_record(attrs, false)
- end
-
- def destroy(*records)
- transaction do
- delete_records(flatten_deeper(records))
- super
- end
- 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
+ if has_cached_counter?
+ owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif loaded?
+ target.size
+ else
+ count
+ end
+ end
+
+ 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
- protected
- def create_record(attrs, force = true)
+ 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
+
+ private
+
+ 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
- ensure_owner_is_persisted!
- transaction do
- object = @reflection.klass.new(attrs)
- add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
- object
+ 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)
+ def delete_records(records, method)
ensure_not_nested
- if record.new_record?
- if force
- record.save!
- else
- return false unless record.save(:validate => validate)
- end
- end
+ through = owner.association(through_reflection.name)
+ scope = through.scoped.where(construct_join_attributes(*records))
- through_association = @owner.send(@reflection.through_reflection.name)
- through_association.create!(construct_join_attributes(record))
- end
+ 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
- # TODO - add dependent option support
- def delete_records(records)
- ensure_not_nested
+ delete_through_records(through, records)
- klass = @reflection.through_reflection.klass
- records.each do |associate|
- klass.delete_all(construct_join_attributes(associate))
+ if through_reflection.macro == :has_many && update_through_counter?(method)
+ update_counter(-count, through_reflection)
end
- end
- def find_target
- return [] unless target_reflection_has_associated_record?
- with_scope(@scope) { @reflection.klass.find(:all) }
+ 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 d581939f04..e13f97125f 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,135 +1,76 @@
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
- class HasOneAssociation < AssociationProxy #:nodoc:
- def create(attrs = {}, replace_existing = true)
- new_record(replace_existing) do |reflection|
- attrs = merge_with_conditions(attrs)
- reflection.create_association(attrs)
- end
- 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
+
+ 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 if @target.persisted?
- @owner.clear_association_cache
+ target.delete
when :destroy
- @target.destroy if @target.persisted?
- @owner.clear_association_cache
+ target.destroy
when :nullify
- @target[@reflection.primary_key_name] = nil
- @target.save if @owner.persisted? && @target.persisted?
- end
- else
- @target[@reflection.primary_key_name] = nil
- @target.save if @owner.persisted? && @target.persisted?
+ 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.persisted? || obj.nil? || dont_save
- return (obj.save ? self : false)
- else
- return (obj.nil? ? nil : self)
- end
end
- protected
- def owner_quoted_id(reflection = @reflection)
- if reflection.options[:primary_key]
- @owner.class.quote_value(@owner.send(reflection.options[:primary_key]))
- else
- @owner.quoted_id
- end
- end
+ def association_scope
+ super.order(options[:order])
+ end
private
- def find_target
- options = @reflection.options.dup.slice(:select, :order, :include, :readonly)
- the_target = with_scope(:find => @scope[:find]) do
- @reflection.klass.find(:first, options)
- end
- set_inverse_instance(the_target, @owner)
- the_target
- end
-
- def construct_find_scope
- if @reflection.options[:as]
- 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
- sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- end
- sql << " AND (#{conditions})" if conditions
- { :conditions => sql }
- end
+ alias creation_attributes construct_owner_attributes
- def construct_create_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- create_scoping
+ # 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 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 => @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 if @owner.persisted?
- 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 e9dc32efd3..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,42 +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:
- ensure_not_nested
+ def create_through_record(record)
+ ensure_not_nested
- klass = @reflection.through_reflection.klass
+ 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(@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..890e77fca9
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -0,0 +1,259 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ class JoinAssociation < JoinPart # :nodoc:
+ # 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, :through_reflection_chain, :to => :reflection
+ delegate :table, :table_name, :to => :parent, :prefix => :parent
+ delegate :alias_tracker, :to => :join_dependency
+
+ 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 }"
+
+ setup_tables
+ 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)
+ # The chain starts with the target table, but we want to end with it here (makes
+ # more sense in this context)
+ chain = through_reflection_chain.reverse
+
+ foreign_table = parent_table
+ index = 0
+
+ chain.each do |reflection|
+ table = tables[index]
+ conditions = []
+
+ if reflection.source_reflection.nil?
+ case reflection.macro
+ when :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+ when :has_many, :has_one
+ key = reflection.foreign_key
+ foreign_key = reflection.active_record_primary_key
+
+ conditions << polymorphic_conditions(reflection, table)
+ when :has_and_belongs_to_many
+ # For habtm, we need to deal with the join table at the same time as the
+ # target table (because unlike a :through association, there is no reflection
+ # to represent the join table)
+ table, join_table = table
+
+ join_key = reflection.foreign_key
+ join_foreign_key = reflection.active_record.primary_key
+
+ relation = relation.join(join_table, join_type).on(
+ join_table[join_key].
+ eq(foreign_table[join_foreign_key])
+ )
+
+ # We've done the first join now, so update the foreign_table for the second
+ foreign_table = join_table
+
+ key = reflection.klass.primary_key
+ foreign_key = reflection.association_foreign_key
+ end
+ else
+ case reflection.source_reflection.macro
+ when :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+
+ conditions << source_type_conditions(reflection, foreign_table)
+ when :has_many, :has_one
+ key = reflection.foreign_key
+ foreign_key = reflection.source_reflection.active_record_primary_key
+ when :has_and_belongs_to_many
+ table, join_table = table
+
+ join_key = reflection.foreign_key
+ join_foreign_key = reflection.klass.primary_key
+
+ relation = relation.join(join_table, join_type).on(
+ join_table[join_key].
+ eq(foreign_table[join_foreign_key])
+ )
+
+ foreign_table = join_table
+
+ key = reflection.klass.primary_key
+ foreign_key = reflection.association_foreign_key
+ end
+ end
+
+ conditions << table[key].eq(foreign_table[foreign_key])
+
+ conditions << reflection_conditions(index, table)
+ conditions << sti_conditions(reflection, table)
+
+ ands = relation.create_and(conditions.flatten.compact)
+
+ join = relation.create_join(
+ table,
+ relation.create_on(ands),
+ join_type)
+
+ relation = relation.from(join)
+
+ # The current table in this iteration becomes the foreign table in the next
+ foreign_table = table
+ index += 1
+ end
+
+ relation
+ end
+
+ def join_relation(joining_relation)
+ self.join_type = Arel::OuterJoin
+ joining_relation.joins(self)
+ end
+
+ def table
+ if tables.last.is_a?(Array)
+ tables.last.first
+ else
+ tables.last
+ end
+ end
+
+ def aliased_table_name
+ table.table_alias || table.name
+ end
+
+ protected
+
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{parent_table_name}"
+ name << "_join" if join
+ name
+ end
+
+ private
+
+ # Generate aliases and Arel::Table instances for each of the tables which we will
+ # later generate joins for. We must do this in advance in order to correctly allocate
+ # the proper alias.
+ def setup_tables
+ @tables = through_reflection_chain.map do |reflection|
+ table = alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != self.reflection)
+ )
+
+ # For habtm, we have two Arel::Table instances related to a single reflection, so
+ # we just store them as a pair in the array.
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection && reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table = alias_tracker.aliased_table_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ )
+
+ [table, join_table]
+ else
+ table
+ end
+ end
+
+ # The joins are generated from the through_reflection_chain in reverse order, so
+ # reverse the tables too (but it's important to generate the aliases in the 'forward'
+ # order, which is why we only do the reversal now.
+ @tables.reverse!
+ end
+
+ def process_conditions(conditions, table_name)
+ if conditions.respond_to?(:to_proc)
+ conditions = instance_eval(&conditions)
+ end
+
+ Arel.sql(sanitize_sql(conditions, table_name))
+ end
+
+ def sanitize_sql(condition, table_name)
+ active_record.send(:sanitize_sql, condition, table_name)
+ end
+
+ def reflection_conditions(index, table)
+ reflection.through_conditions.reverse[index].map do |condition|
+ process_conditions(condition, table.table_alias || table.name)
+ end
+ end
+
+ def sti_conditions(reflection, table)
+ unless reflection.klass.descends_from_active_record?
+ sti_column = table[reflection.klass.inheritance_column]
+ sti_condition = sti_column.eq(reflection.klass.sti_name)
+ subclasses = reflection.klass.descendants
+
+ # TODO: use IN (...), or possibly AR::Base#type_condition
+ subclasses.inject(sti_condition) { |attr,subclass|
+ attr.or(sti_column.eq(subclass.sti_name))
+ }
+ end
+ end
+
+ def source_type_conditions(reflection, foreign_table)
+ if reflection.options[:source_type]
+ foreign_table[reflection.source_reflection.foreign_type].
+ eq(reflection.options[:source_type])
+ end
+ end
+
+ def polymorphic_conditions(reflection, table)
+ if reflection.options[:as]
+ table[reflection.type].
+ eq(reflection.active_record.base_class.name)
+ 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/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..e794f05340
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
@@ -0,0 +1,58 @@
+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
+ def associated_records_by_owner
+ super.each do |owner_key, rows|
+ rows.map! { |row| 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..0d8e45adb5
--- /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
+ 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..ed24373cba
--- /dev/null
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -0,0 +1,281 @@
+require 'enumerator'
+
+module ActiveRecord
+ # = Active Record Through Association
+ module Associations
+ module ThroughAssociation #:nodoc:
+
+ delegate :source_options, :through_options, :source_reflection, :through_reflection,
+ :through_reflection_chain, :through_conditions, :to => :reflection
+
+ protected
+
+ def target_scope
+ super.merge(through_reflection.klass.scoped)
+ end
+
+ def association_scope
+ scope = super.joins(construct_joins)
+ scope = scope.where(reflection_conditions(0))
+
+ unless options[:include]
+ scope = scope.includes(source_options[:include])
+ end
+
+ scope
+ end
+
+ private
+
+ # This scope affects the creation of the associated records (not the join records). At the
+ # moment we only support creating on a :through association when the source reflection is a
+ # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
+ # this scope has can legitimately be empty.
+ def creation_attributes
+ { }
+ end
+
+ # TODO: Needed?
+ def aliased_through_table
+ name = through_reflection.table_name
+
+ reflection.table_name == name ?
+ through_reflection.klass.arel_table.alias(name + "_join") :
+ through_reflection.klass.arel_table
+ end
+
+ def construct_owner_conditions
+ reflection = through_reflection_chain.last
+
+ if reflection.macro == :has_and_belongs_to_many
+ table = tables[reflection].first
+ else
+ table = Array.wrap(tables[reflection]).first
+ end
+
+ super(table, reflection)
+ end
+
+ def construct_joins
+ joins, right_index = [], 1
+
+ # Iterate over each pair in the through reflection chain, joining them together
+ through_reflection_chain.each_cons(2) do |left, right|
+ left_table, right_table = tables[left], tables[right]
+
+ if left.source_reflection.nil?
+ case left.macro
+ when :belongs_to
+ joins << inner_join(
+ right_table,
+ left_table[left.association_primary_key],
+ right_table[left.foreign_key],
+ reflection_conditions(right_index)
+ )
+ when :has_many, :has_one
+ joins << inner_join(
+ right_table,
+ left_table[left.foreign_key],
+ right_table[right.association_primary_key],
+ polymorphic_conditions(left, left),
+ reflection_conditions(right_index)
+ )
+ when :has_and_belongs_to_many
+ joins << inner_join(
+ right_table,
+ left_table.first[left.foreign_key],
+ right_table[right.klass.primary_key],
+ reflection_conditions(right_index)
+ )
+ end
+ else
+ case left.source_reflection.macro
+ when :belongs_to
+ joins << inner_join(
+ right_table,
+ left_table[left.association_primary_key],
+ right_table[left.foreign_key],
+ source_type_conditions(left),
+ reflection_conditions(right_index)
+ )
+ when :has_many, :has_one
+ if right.macro == :has_and_belongs_to_many
+ join_table, right_table = tables[right]
+ end
+
+ joins << inner_join(
+ right_table,
+ left_table[left.foreign_key],
+ right_table[left.source_reflection.active_record_primary_key],
+ polymorphic_conditions(left, left.source_reflection),
+ reflection_conditions(right_index)
+ )
+
+ if right.macro == :has_and_belongs_to_many
+ joins << inner_join(
+ join_table,
+ right_table[right.klass.primary_key],
+ join_table[right.association_foreign_key]
+ )
+ end
+ when :has_and_belongs_to_many
+ join_table, left_table = tables[left]
+
+ joins << inner_join(
+ join_table,
+ left_table[left.klass.primary_key],
+ join_table[left.association_foreign_key]
+ )
+
+ joins << inner_join(
+ right_table,
+ join_table[left.foreign_key],
+ right_table[right.klass.primary_key],
+ reflection_conditions(right_index)
+ )
+ end
+ end
+
+ right_index += 1
+ end
+
+ joins
+ end
+
+ # 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
+
+ def alias_tracker
+ @alias_tracker ||= AliasTracker.new
+ end
+
+ # TODO: It is decidedly icky to have an array for habtm entries, and no array for others
+ def tables
+ @tables ||= begin
+ Hash[
+ through_reflection_chain.map do |reflection|
+ table = alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, reflection != self.reflection)
+ )
+
+ if reflection.macro == :has_and_belongs_to_many ||
+ (reflection.source_reflection &&
+ reflection.source_reflection.macro == :has_and_belongs_to_many)
+
+ join_table = alias_tracker.aliased_table_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ )
+
+ [reflection, [join_table, table]]
+ else
+ [reflection, table]
+ end
+ end
+ ]
+ end
+ end
+
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{self.reflection.name}"
+ name << "_join" if join
+ name
+ end
+
+ def inner_join(table, left_column, right_column, *conditions)
+ conditions << left_column.eq(right_column)
+
+ table.create_join(
+ table,
+ table.create_on(table.create_and(conditions.flatten.compact)))
+ end
+
+ def reflection_conditions(index)
+ reflection = through_reflection_chain[index]
+ conditions = through_conditions[index].dup
+
+ # TODO: maybe this should go in Reflection#through_conditions directly?
+ unless reflection.klass.descends_from_active_record?
+ conditions << reflection.klass.send(:type_condition)
+ end
+
+ unless conditions.empty?
+ conditions.map! do |condition|
+ condition = reflection.klass.send(:sanitize_sql, interpolate(condition), reflection.table_name)
+ condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
+ condition
+ end
+
+ Arel::Nodes::And.new(conditions)
+ end
+ end
+
+ def polymorphic_conditions(reflection, polymorphic_reflection)
+ if polymorphic_reflection.options[:as]
+ tables[reflection][polymorphic_reflection.type].
+ eq(polymorphic_reflection.active_record.base_class.name)
+ end
+ end
+
+ def source_type_conditions(reflection)
+ if reflection.options[:source_type]
+ tables[reflection.through_reflection][reflection.foreign_type].
+ eq(reflection.options[:source_type])
+ end
+ end
+
+ # TODO: Think about this in the context of 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 f6d02a215f..0000000000
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ /dev/null
@@ -1,300 +0,0 @@
-require 'enumerator'
-
-module ActiveRecord
- # = Active Record Through Association Scope
- module Associations
- module ThroughAssociationScope
-
- protected
-
- def construct_find_scope
- {
- :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
-
- def construct_create_scope
- @reflection.nested? ? {} : construct_owner_attributes(@reflection)
- end
-
- # Build SQL conditions from attributes, qualified by table name.
- def construct_conditions
- reflection = @reflection.through_reflection_chain.last
-
- if reflection.macro == :has_and_belongs_to_many
- table_alias = table_aliases[reflection].first
- else
- table_alias = table_aliases[reflection]
- end
-
- parts = construct_quoted_owner_attributes(reflection).map do |attr, value|
- "#{table_alias}.#{attr} = #{value}"
- end
- parts += reflection_conditions(0)
-
- "(" + parts.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(reflection),
- "#{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(reflection) }
- end
- end
-
- def construct_select(custom_select = nil)
- distinct = "DISTINCT " if @reflection.options[:uniq]
- custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
- end
-
- def construct_joins(custom_joins = nil)
- "#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}"
- end
-
- def construct_through_joins
- joins, right_index = [], 1
-
- # Iterate over each pair in the through reflection chain, joining them together
- @reflection.through_reflection_chain.each_cons(2) do |left, right|
- right_table_and_alias = table_name_and_alias(right.quoted_table_name, table_aliases[right])
-
- if left.source_reflection.nil?
- case left.macro
- when :belongs_to
- joins << inner_join_sql(
- right_table_and_alias,
- table_aliases[left], left.association_primary_key,
- table_aliases[right], left.primary_key_name,
- reflection_conditions(right_index)
- )
- when :has_many, :has_one
- joins << inner_join_sql(
- right_table_and_alias,
- table_aliases[left], left.primary_key_name,
- table_aliases[right], right.association_primary_key,
- polymorphic_conditions(left, left),
- reflection_conditions(right_index)
- )
- when :has_and_belongs_to_many
- joins << inner_join_sql(
- right_table_and_alias,
- table_aliases[left].first, left.primary_key_name,
- table_aliases[right], right.klass.primary_key,
- reflection_conditions(right_index)
- )
- end
- else
- case left.source_reflection.macro
- when :belongs_to
- joins << inner_join_sql(
- right_table_and_alias,
- table_aliases[left], left.association_primary_key,
- table_aliases[right], left.primary_key_name,
- source_type_conditions(left),
- reflection_conditions(right_index)
- )
- when :has_many, :has_one
- if right.macro == :has_and_belongs_to_many
- join_table, right_table = table_aliases[right]
- right_table_and_alias = table_name_and_alias(right.quoted_table_name, right_table)
- else
- right_table = table_aliases[right]
- end
-
- joins << inner_join_sql(
- right_table_and_alias,
- table_aliases[left], left.primary_key_name,
- right_table, left.source_reflection.active_record_primary_key,
- polymorphic_conditions(left, left.source_reflection),
- reflection_conditions(right_index)
- )
-
- if right.macro == :has_and_belongs_to_many
- joins << inner_join_sql(
- table_name_and_alias(
- quote_table_name(right.options[:join_table]),
- join_table
- ),
- right_table, right.klass.primary_key,
- join_table, right.association_foreign_key
- )
- end
- when :has_and_belongs_to_many
- join_table, left_table = table_aliases[left]
-
- joins << inner_join_sql(
- table_name_and_alias(
- quote_table_name(left.source_reflection.options[:join_table]),
- join_table
- ),
- left_table, left.klass.primary_key,
- join_table, left.association_foreign_key
- )
-
- joins << inner_join_sql(
- right_table_and_alias,
- join_table, left.primary_key_name,
- table_aliases[right], right.klass.primary_key,
- reflection_conditions(right_index)
- )
- end
- end
-
- right_index += 1
- end
-
- joins.join(" ")
- end
-
- def alias_tracker
- @alias_tracker ||= AliasTracker.new
- end
-
- def table_aliases
- @table_aliases ||= begin
- @reflection.through_reflection_chain.inject({}) do |aliases, reflection|
- table_alias = quote_table_name(alias_tracker.aliased_name_for(
- reflection.table_name,
- table_alias_for(reflection, reflection != @reflection)
- ))
-
- if reflection.macro == :has_and_belongs_to_many ||
- (reflection.source_reflection &&
- reflection.source_reflection.macro == :has_and_belongs_to_many)
-
- join_table_alias = quote_table_name(alias_tracker.aliased_name_for(
- (reflection.source_reflection || reflection).options[:join_table],
- table_alias_for(reflection, true)
- ))
-
- aliases[reflection] = [join_table_alias, table_alias]
- else
- aliases[reflection] = table_alias
- end
-
- aliases
- end
- end
- end
-
- def table_alias_for(reflection, join = false)
- name = alias_tracker.pluralize(reflection.name)
- name << "_#{@reflection.name}"
- name << "_join" if join
- name
- end
-
- def quote_table_name(table_name)
- @reflection.klass.connection.quote_table_name(table_name)
- end
-
- def table_name_and_alias(table_name, table_alias)
- "#{table_name} #{table_alias if table_alias != table_name}".strip
- end
-
- def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, *conditions)
- conditions << "#{on_left_table}.#{on_left_key} = #{on_right_table}.#{on_right_key}"
- conditions = conditions.flatten.compact
- conditions = conditions.map { |sql| "(#{sql})" } * ' AND '
-
- "INNER JOIN #{table} ON #{conditions}"
- end
-
- def reflection_conditions(index)
- reflection = @reflection.through_reflection_chain[index]
- reflection_conditions = @reflection.through_conditions[index]
-
- conditions = []
-
- if reflection.options[:as].nil? && # reflection.klass is a Module if :as is used
- reflection.klass.finder_needs_type_condition?
- conditions << reflection.klass.send(:type_condition).to_sql
- end
-
- reflection_conditions.each do |condition|
- sanitized_condition = reflection.klass.send(:sanitize_sql, condition)
- interpolated_condition = interpolate_sql(sanitized_condition)
-
- if condition.is_a?(Hash)
- interpolated_condition.gsub!(
- @reflection.quoted_table_name,
- reflection.quoted_table_name
- )
- end
-
- conditions << interpolated_condition
- end
-
- conditions
- end
-
- def polymorphic_conditions(reflection, polymorphic_reflection)
- if polymorphic_reflection.options[:as]
- "%s.%s = %s" % [
- table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type",
- @owner.class.quote_value(polymorphic_reflection.active_record.base_class.name)
- ]
- end
- end
-
- def source_type_conditions(reflection)
- if reflection.options[:source_type]
- "%s.%s = %s" % [
- table_aliases[reflection.through_reflection],
- reflection.source_reflection.options[:foreign_type].to_s,
- @owner.class.quote_value(reflection.options[:source_type])
- ]
- end
- 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.
- # This method is used when adding records to the association. Since this only makes sense for
- # non-nested through associations, that's the only case we have to worry about here.
- 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 ensure_not_nested
- if @reflection.nested?
- raise HasManyThroughNestedAssociationsAreReadonly.new(@owner, @reflection)
- end
- end
- end
- end
-end