aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/has_many_through_association.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/has_many_through_association.rb')
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb159
1 files changed, 97 insertions, 62 deletions
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