Updated README.rdoc again
[feedcatcher.git] / vendor / rails / activerecord / lib / active_record / associations / has_and_belongs_to_many_association.rb
1 module ActiveRecord
2 module Associations
3 class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4 def create(attributes = {})
5 create_record(attributes) { |record| insert_record(record) }
6 end
7
8 def create!(attributes = {})
9 create_record(attributes) { |record| insert_record(record, true) }
10 end
11
12 def columns
13 @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
14 end
15
16 def reset_column_information
17 @reflection.reset_column_information
18 end
19
20 protected
21 def construct_find_options!(options)
22 options[:joins] = @join_sql
23 options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
24 options[:select] ||= (@reflection.options[:select] || '*')
25 end
26
27 def count_records
28 load_target.size
29 end
30
31 def insert_record(record, force = true, validate = true)
32 if record.new_record?
33 if force
34 record.save!
35 else
36 return false unless record.save(validate)
37 end
38 end
39
40 if @reflection.options[:insert_sql]
41 @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
42 else
43 attributes = columns.inject({}) do |attrs, column|
44 case column.name.to_s
45 when @reflection.primary_key_name.to_s
46 attrs[column.name] = owner_quoted_id
47 when @reflection.association_foreign_key.to_s
48 attrs[column.name] = record.quoted_id
49 else
50 if record.has_attribute?(column.name)
51 value = @owner.send(:quote_value, record[column.name], column)
52 attrs[column.name] = value unless value.nil?
53 end
54 end
55 attrs
56 end
57
58 sql =
59 "INSERT INTO #{@owner.connection.quote_table_name @reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
60 "VALUES (#{attributes.values.join(', ')})"
61
62 @owner.connection.insert(sql)
63 end
64
65 return true
66 end
67
68 def delete_records(records)
69 if sql = @reflection.options[:delete_sql]
70 records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
71 else
72 ids = quoted_record_ids(records)
73 sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
74 @owner.connection.delete(sql)
75 end
76 end
77
78 def construct_sql
79 if @reflection.options[:finder_sql]
80 @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
81 else
82 @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
83 @finder_sql << " AND (#{conditions})" if conditions
84 end
85
86 @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
87
88 if @reflection.options[:counter_sql]
89 @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
90 elsif @reflection.options[:finder_sql]
91 # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
92 @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
93 @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
94 else
95 @counter_sql = @finder_sql
96 end
97 end
98
99 def construct_scope
100 { :find => { :conditions => @finder_sql,
101 :joins => @join_sql,
102 :readonly => false,
103 :order => @reflection.options[:order],
104 :include => @reflection.options[:include],
105 :limit => @reflection.options[:limit] } }
106 end
107
108 # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
109 # clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
110 # an id column. This will then overwrite the id column of the records coming back.
111 def finding_with_ambiguous_select?(select_clause)
112 !select_clause && columns.size != 2
113 end
114
115 private
116 def create_record(attributes, &block)
117 # Can't use Base.create because the foreign key may be a protected attribute.
118 ensure_owner_is_not_new
119 if attributes.is_a?(Array)
120 attributes.collect { |attr| create(attr) }
121 else
122 build_record(attributes, &block)
123 end
124 end
125 end
126 end
127 end