moduleActiveRecordmoduleAssociationsclassHasManyThroughAssociation<HasManyAssociation#:nodoc:alias_method:new,:builddefcreate!(attrs=nil)transactiondoself<<(object=attrs?@reflection.klass.send(:with_scope,:create=>attrs){@reflection.create_association!}:@reflection.create_association!)objectendenddefcreate(attrs=nil)transactiondoobject=ifattrs@reflection.klass.send(:with_scope,:create=>attrs){@reflection.create_association}else@reflection.create_associationendraise_on_type_mismatch(object)add_record_to_target_with_callbacks(object)do|r|insert_record(object,false)endobjectendend# 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.defsizereturn@owner.send(:read_attribute,cached_counter_attribute_name)ifhas_cached_counter?return@target.sizeifloaded?returncountendprotecteddeftarget_reflection_has_associated_record?if@reflection.through_reflection.macro==:belongs_to&&@owner[@reflection.through_reflection.primary_key_name].blank?falseelsetrueendenddefconstruct_find_options!(options)options[:select]=construct_select(options[:select])options[:from]||=construct_fromoptions[:joins]=construct_joins(options[:joins])options[:include]=@reflection.source_reflection.options[:include]ifoptions[:include].nil?&&@reflection.source_reflection.options[:include]enddefinsert_record(record,force=true,validate=true)ifrecord.new_record?ifforcerecord.save!elsereturnfalseunlessrecord.save(validate)endendthrough_reflection=@reflection.through_reflectionklass=through_reflection.klass@owner.send(@reflection.through_reflection.name).proxy_target<<klass.send(:with_scope,:create=>construct_join_attributes(record)){through_reflection.create_association!}end# TODO - add dependent option supportdefdelete_records(records)klass=@reflection.through_reflection.klassrecords.eachdo|associate|klass.delete_all(construct_join_attributes(associate))endenddeffind_targetreturn[]unlesstarget_reflection_has_associated_record?@reflection.klass.find(:all,:select=>construct_select,:conditions=>construct_conditions,:from=>construct_from,:joins=>construct_joins,:order=>@reflection.options[:order],:limit=>@reflection.options[:limit],:group=>@reflection.options[:group],:readonly=>@reflection.options[:readonly],:include=>@reflection.options[:include]||@reflection.source_reflection.options[:include])end# Construct attributes for associate pointing to owner.defconstruct_owner_attributes(reflection)ifas=reflection.options[:as]{"#{as}_id"=>@owner.id,"#{as}_type"=>@owner.class.base_class.name.to_s}else{reflection.primary_key_name=>@owner.id}endend# Construct attributes for :through pointing to owner and associate.defconstruct_join_attributes(associate)# TODO: revist this to allow it for deletion, supposing dependent option is supportedraiseActiveRecord::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)endjoin_attributesend# Associate attributes pointing to owner, quoted.defconstruct_quoted_owner_attributes(reflection)ifas=reflection.options[:as]{"#{as}_id"=>owner_quoted_id,"#{as}_type"=>reflection.klass.quote_value(@owner.class.base_class.name.to_s,reflection.klass.columns_hash["#{as}_type"])}elsifreflection.macro==:belongs_to{reflection.klass.primary_key=>@owner[reflection.primary_key_name]}else{reflection.primary_key_name=>owner_quoted_id}endend# Build SQL conditions from attributes, qualified by table name.defconstruct_conditionstable_name=@reflection.through_reflection.quoted_table_nameconditions=construct_quoted_owner_attributes(@reflection.through_reflection).mapdo|attr,value|"#{table_name}.#{attr} = #{value}"endconditions<<sql_conditionsifsql_conditions"("+conditions.join(') AND (')+")"enddefconstruct_from@reflection.quoted_table_nameenddefconstruct_select(custom_select=nil)distinct="DISTINCT "if@reflection.options[:uniq]selected=custom_select||@reflection.options[:select]||"#{distinct}#{@reflection.quoted_table_name}.*"enddefconstruct_joins(custom_joins=nil)polymorphic_join=nilif@reflection.source_reflection.macro==:belongs_toreflection_primary_key=@reflection.klass.primary_keysource_primary_key=@reflection.source_reflection.primary_key_nameif@reflection.options[:source_type]polymorphic_join="AND %s.%s = %s"%[@reflection.through_reflection.quoted_table_name,"#{@reflection.source_reflection.options[:foreign_type]}",@owner.class.quote_value(@reflection.options[:source_type])]endelsereflection_primary_key=@reflection.source_reflection.primary_key_namesource_primary_key=@reflection.through_reflection.klass.primary_keyif@reflection.source_reflection.options[:as]polymorphic_join="AND %s.%s = %s"%[@reflection.quoted_table_name,"#{@reflection.source_reflection.options[:as]}_type",@owner.class.quote_value(@reflection.through_reflection.klass.name)]endend"INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]}#{custom_joins}"%[@reflection.through_reflection.quoted_table_name,@reflection.quoted_table_name,reflection_primary_key,@reflection.through_reflection.quoted_table_name,source_primary_key,polymorphic_join]enddefconstruct_scope{:create=>construct_owner_attributes(@reflection),:find=>{:from=>construct_from,:conditions=>construct_conditions,:joins=>construct_joins,:include=>@reflection.options[:include],:select=>construct_select,:order=>@reflection.options[:order],:limit=>@reflection.options[:limit],:readonly=>@reflection.options[:readonly],}}enddefconstruct_sqlcasewhen@reflection.options[:finder_sql]@finder_sql=interpolate_sql(@reflection.options[:finder_sql])@finder_sql="#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"@finder_sql<<" AND (#{conditions})"ifconditionselse@finder_sql=construct_conditionsendif@reflection.options[:counter_sql]@counter_sql=interpolate_sql(@reflection.options[:counter_sql])elsif@reflection.options[:finder_sql]# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */@reflection.options[:counter_sql]=@reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im){"SELECT #{$1}COUNT(*) FROM"}@counter_sql=interpolate_sql(@reflection.options[:counter_sql])else@counter_sql=@finder_sqlendenddefconditions@conditions=build_conditionsunlessdefined?(@conditions)@conditionsenddefbuild_conditionsassociation_conditions=@reflection.options[:conditions]through_conditions=build_through_conditionssource_conditions=@reflection.source_reflection.options[:conditions]uses_sti=!@reflection.through_reflection.klass.descends_from_active_record?ifassociation_conditions||through_conditions||source_conditions||uses_stiall=[][association_conditions,source_conditions].eachdo|conditions|all<<interpolate_sql(sanitize_sql(conditions))ifconditionsendall<<through_conditionsifthrough_conditionsall<<build_sti_conditionifuses_stiall.map{|sql|"(#{sql})"}*' AND 'endenddefbuild_through_conditionsconditions=@reflection.through_reflection.options[:conditions]ifconditions.is_a?(Hash)interpolate_sql(sanitize_sql(conditions)).gsub(@reflection.quoted_table_name,@reflection.through_reflection.quoted_table_name)elsifconditionsinterpolate_sql(sanitize_sql(conditions))endenddefbuild_sti_condition@reflection.through_reflection.klass.send(:type_condition)endalias_method:sql_conditions,:conditionsdefhas_cached_counter?@owner.attribute_present?(cached_counter_attribute_name)enddefcached_counter_attribute_name"#{@reflection.name}_count"end# NOTE - not sure that we can actually cope with inverses heredefwe_can_set_the_inverse_on_this?(record)falseendendendend