Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/tags/rel_2-0-2/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb

Revision 8313, 5.9 kB (checked in by david, 10 months ago)

Fixed that habtm associations should be able to set :select as part of their definition and have that honored [DHH]

Line 
1 module ActiveRecord
2   module Associations
3     class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4       def initialize(owner, reflection)
5         super
6         construct_sql
7       end
8
9       def build(attributes = {})
10         load_target
11         build_record(attributes)
12       end
13
14       def create(attributes = {})
15         create_record(attributes) { |record| insert_record(record) }
16       end
17      
18       def create!(attributes = {})
19         create_record(attributes) { |record| insert_record(record, true) }
20       end
21
22       def find_first
23         load_target.first
24       end
25
26       def find(*args)
27         options = args.extract_options!
28
29         # If using a custom finder_sql, scan the entire collection.
30         if @reflection.options[:finder_sql]
31           expects_array = args.first.kind_of?(Array)
32           ids = args.flatten.compact.uniq
33
34           if ids.size == 1
35             id = ids.first.to_i
36             record = load_target.detect { |record| id == record.id }
37             expects_array ? [record] : record
38           else
39             load_target.select { |record| ids.include?(record.id) }
40           end
41         else
42           conditions = "#{@finder_sql}"
43
44           if sanitized_conditions = sanitize_sql(options[:conditions])
45             conditions << " AND (#{sanitized_conditions})"
46           end
47
48           options[:conditions] = conditions
49           options[:joins]      = @join_sql
50           options[:readonly]   = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
51
52           if options[:order] && @reflection.options[:order]
53             options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
54           elsif @reflection.options[:order]
55             options[:order] = @reflection.options[:order]
56           end
57
58           merge_options_from_reflection!(options)
59
60           options[:select] ||= (@reflection.options[:select] || '*')
61
62           # Pass through args exactly as we received them.
63           args << options
64           @reflection.klass.find(*args)
65         end
66       end
67
68       protected
69         def count_records
70           load_target.size
71         end
72
73         def insert_record(record, force=true)
74           if record.new_record?
75             if force
76               record.save!
77             else
78               return false unless record.save
79             end
80           end
81
82           if @reflection.options[:insert_sql]
83             @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
84           else
85             columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
86
87             attributes = columns.inject({}) do |attributes, column|
88               case column.name
89                 when @reflection.primary_key_name
90                   attributes[column.name] = @owner.quoted_id
91                 when @reflection.association_foreign_key
92                   attributes[column.name] = record.quoted_id
93                 else
94                   if record.attributes.has_key?(column.name)
95                     value = @owner.send(:quote_value, record[column.name], column)
96                     attributes[column.name] = value unless value.nil?
97                   end
98               end
99               attributes
100             end
101
102             sql =
103               "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
104               "VALUES (#{attributes.values.join(', ')})"
105
106             @owner.connection.execute(sql)
107           end
108
109           return true
110         end
111
112         def delete_records(records)
113           if sql = @reflection.options[:delete_sql]
114             records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
115           else
116             ids = quoted_record_ids(records)
117             sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
118             @owner.connection.execute(sql)
119           end
120         end
121
122         def construct_sql
123           interpolate_sql_options!(@reflection.options, :finder_sql)
124
125           if @reflection.options[:finder_sql]
126             @finder_sql = @reflection.options[:finder_sql]
127           else
128             @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
129             @finder_sql << " AND (#{conditions})" if conditions
130           end
131
132           @join_sql = "INNER JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
133         end
134
135         def construct_scope
136           { :find => {  :conditions => @finder_sql,
137                         :joins => @join_sql,
138                         :readonly => false,
139                         :order => @reflection.options[:order],
140                         :limit => @reflection.options[:limit] } }
141         end
142
143         # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
144         # clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
145         # an id column. This will then overwrite the id column of the records coming back.
146         def finding_with_ambiguous_select?(select_clause)
147           !select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
148         end
149
150       private
151         def create_record(attributes)
152           # Can't use Base.create because the foreign key may be a protected attribute.
153           ensure_owner_is_not_new
154           if attributes.is_a?(Array)
155             attributes.collect { |attr| create(attr) }
156           else
157             record = build(attributes)
158             yield(record)
159             record
160           end
161         end
162     end
163   end
164 end
Note: See TracBrowser for help on using the browser.