3 class HasManyThroughAssociation
< HasManyAssociation
#:nodoc:
4 def initialize(owner
, reflection
)
5 reflection
.check_validity
!
9 alias_method
:new, :build
11 def create
!(attrs
= nil)
13 self << (object
= attrs
? @reflection.klass
.send(:with_scope, :create => attrs
) { @reflection.create_association
! } : @reflection.create_association
!)
18 def create(attrs
= nil)
20 self << (object
= attrs
? @reflection.klass
.send(:with_scope, :create => attrs
) { @reflection.create_association
} : @reflection.create_association
)
25 # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
26 # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero,
27 # and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length.
29 return @owner.send(:read_attribute, cached_counter_attribute_name
) if has_cached_counter
?
30 return @target.size
if loaded
?
35 def target_reflection_has_associated_record
?
36 if @reflection.through_reflection
.macro
== :belongs_to && @owner[@reflection.through_reflection
.primary_key_name
].blank
?
43 def construct_find_options
!(options
)
44 options
[:select] = construct_select(options
[:select])
45 options
[:from] ||= construct_from
46 options
[:joins] = construct_joins(options
[:joins])
47 options
[:include] = @reflection.source_reflection
.options
[:include] if options
[:include].nil?
50 def insert_record(record
, force
= true, validate
= true)
55 return false unless record
.save(validate
)
58 through_reflection
= @reflection.through_reflection
59 klass
= through_reflection
.klass
60 @owner.send(@reflection.through_reflection
.name
).proxy_target
<< klass
.send(:with_scope, :create => construct_join_attributes(record
)) { through_reflection
.create_association
! }
63 # TODO - add dependent option support
64 def delete_records(records
)
65 klass
= @reflection.through_reflection
.klass
66 records
.each
do |associate
|
67 klass
.delete_all(construct_join_attributes(associate
))
72 return [] unless target_reflection_has_associated_record
?
73 @reflection.klass
.find(:all,
74 :select => construct_select
,
75 :conditions => construct_conditions
,
76 :from => construct_from
,
77 :joins => construct_joins
,
78 :order => @reflection.options
[:order],
79 :limit => @reflection.options
[:limit],
80 :group => @reflection.options
[:group],
81 :readonly => @reflection.options
[:readonly],
82 :include => @reflection.options
[:include] || @reflection.source_reflection
.options
[:include]
86 # Construct attributes for associate pointing to owner.
87 def construct_owner_attributes(reflection
)
88 if as
= reflection
.options
[:as]
89 { "#{as}_id" => @owner.id
,
90 "#{as}_type" => @owner.class.base_class
.name
.to_s
}
92 { reflection
.primary_key_name
=> @owner.id
}
96 # Construct attributes for :through pointing to owner and associate.
97 def construct_join_attributes(associate
)
98 # TODO: revist this to allow it for deletion, supposing dependent option is supported
99 raise ActiveRecord
::HasManyThroughCantAssociateThroughHasManyReflection.new(@owner, @reflection) if @reflection.source_reflection
.macro
== :has_many
100 join_attributes
= construct_owner_attributes(@reflection.through_reflection
).merge(@reflection.source_reflection
.primary_key_name
=> associate
.id
)
101 if @reflection.options
[:source_type]
102 join_attributes
.merge
!(@reflection.source_reflection
.options
[:foreign_type] => associate
.class.base_class
.name
.to_s
)
107 # Associate attributes pointing to owner, quoted.
108 def construct_quoted_owner_attributes(reflection
)
109 if as
= reflection
.options
[:as]
110 { "#{as}_id" => owner_quoted_id
,
111 "#{as}_type" => reflection
.klass
.quote_value(
112 @owner.class.base_class
.name
.to_s
,
113 reflection
.klass
.columns_hash
["#{as}_type"]) }
114 elsif reflection
.macro
== :belongs_to
115 { reflection
.klass
.primary_key
=> @owner[reflection
.primary_key_name
] }
117 { reflection
.primary_key_name
=> owner_quoted_id
}
121 # Build SQL conditions from attributes, qualified by table name.
122 def construct_conditions
123 table_name
= @reflection.through_reflection
.quoted_table_name
124 conditions
= construct_quoted_owner_attributes(@reflection.through_reflection
).map
do |attr
, value
|
125 "#{table_name}.#{attr} = #{value}"
127 conditions
<< sql_conditions
if sql_conditions
128 "(" + conditions
.join(') AND (') + ")"
132 @reflection.quoted_table_name
135 def construct_select(custom_select
= nil)
136 distinct
= "DISTINCT " if @reflection.options
[:uniq]
137 selected
= custom_select
|| @reflection.options
[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
140 def construct_joins(custom_joins
= nil)
141 polymorphic_join
= nil
142 if @reflection.source_reflection
.macro
== :belongs_to
143 reflection_primary_key
= @reflection.klass
.primary_key
144 source_primary_key
= @reflection.source_reflection
.primary_key_name
145 if @reflection.options
[:source_type]
146 polymorphic_join
= "AND %s.%s = %s" % [
147 @reflection.through_reflection
.quoted_table_name
, "#{@reflection.source_reflection.options[:foreign_type]}",
148 @owner.class.quote_value(@reflection.options
[:source_type])
152 reflection_primary_key
= @reflection.source_reflection
.primary_key_name
153 source_primary_key
= @reflection.through_reflection
.klass
.primary_key
154 if @reflection.source_reflection
.options
[:as]
155 polymorphic_join
= "AND %s.%s = %s" % [
156 @reflection.quoted_table_name
, "#{@reflection.source_reflection.options[:as]}_type",
157 @owner.class.quote_value(@reflection.through_reflection
.klass
.name
)
162 "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
163 @reflection.through_reflection
.quoted_table_name
,
164 @reflection.quoted_table_name
, reflection_primary_key
,
165 @reflection.through_reflection
.quoted_table_name
, source_primary_key
,
171 { :create => construct_owner_attributes(@reflection),
172 :find => { :from => construct_from
,
173 :conditions => construct_conditions
,
174 :joins => construct_joins
,
175 :include => @reflection.options
[:include],
176 :select => construct_select
,
177 :order => @reflection.options
[:order],
178 :limit => @reflection.options
[:limit],
179 :readonly => @reflection.options
[:readonly],
185 when @reflection.options
[:finder_sql]
186 @finder_sql = interpolate_sql(@reflection.options
[:finder_sql])
188 @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
189 @finder_sql << " AND (#{conditions})" if conditions
191 @finder_sql = construct_conditions
194 if @reflection.options
[:counter_sql]
195 @counter_sql = interpolate_sql(@reflection.options
[:counter_sql])
196 elsif @reflection.options
[:finder_sql]
197 # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
198 @reflection.options
[:counter_sql] = @reflection.options
[:finder_sql].sub(/SELECT (\/\
*.*?\
*\
/ )?(.*)\bFROM\b/im
) { "SELECT #{$1}COUNT(*) FROM" }
199 @counter_sql = interpolate_sql(@reflection.options
[:counter_sql])
201 @counter_sql = @finder_sql
206 @conditions = build_conditions
unless defined?(@conditions)
211 association_conditions
= @reflection.options
[:conditions]
212 through_conditions
= build_through_conditions
213 source_conditions
= @reflection.source_reflection
.options
[:conditions]
214 uses_sti
= !@reflection.through_reflection
.klass
.descends_from_active_record
?
216 if association_conditions
|| through_conditions
|| source_conditions
|| uses_sti
219 [association_conditions
, source_conditions
].each
do |conditions
|
220 all
<< interpolate_sql(sanitize_sql(conditions
)) if conditions
223 all
<< through_conditions
if through_conditions
224 all
<< build_sti_condition
if uses_sti
226 all
.map
{ |sql
| "(#{sql})" } * ' AND '
230 def build_through_conditions
231 conditions
= @reflection.through_reflection
.options
[:conditions]
232 if conditions
.is_a
?(Hash
)
233 interpolate_sql(sanitize_sql(conditions
)).gsub(
234 @reflection.quoted_table_name
,
235 @reflection.through_reflection
.quoted_table_name
)
237 interpolate_sql(sanitize_sql(conditions
))
241 def build_sti_condition
242 @reflection.through_reflection
.klass
.send(:type_condition)
245 alias_method
:sql_conditions, :conditions
247 def has_cached_counter
?
248 @owner.attribute_present
?(cached_counter_attribute_name
)
251 def cached_counter_attribute_name
252 "#{@reflection.name}_count"