has_and_belongs_to_many_association.rb 5.8 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2 3
module ActiveRecord
  module Associations
    class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
      def initialize(owner, reflection)
5 6
        super
        construct_sql
D
Initial  
David Heinemeier Hansson 已提交
7
      end
8

9 10
      def build(attributes = {})
        load_target
11
        build_record(attributes)
12 13
      end

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 32 33
          expects_array = args.first.kind_of?(Array)
          ids = args.flatten.compact.uniq

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
D
Initial  
David Heinemeier Hansson 已提交
41
        else
42
          conditions = "#{@finder_sql}"
43

44
          if sanitized_conditions = sanitize_sql(options[:conditions])
J
Jeremy Kemper 已提交
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])
51

52 53 54 55
          if options[:order] && @reflection.options[:order]
            options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
          elsif @reflection.options[:order]
            options[:order] = @reflection.options[:order]
D
Initial  
David Heinemeier Hansson 已提交
56
          end
57

58 59
          merge_options_from_reflection!(options)

60 61
          options[:select]   ||= '*'

62 63
          # Pass through args exactly as we received them.
          args << options
64
          @reflection.klass.find(*args)
D
Initial  
David Heinemeier Hansson 已提交
65 66 67 68 69
        end
      end

      protected
        def count_records
70
          load_target.size
D
Initial  
David Heinemeier Hansson 已提交
71 72
        end

73
        def insert_record(record, force=true)
74
          if record.new_record?
75 76 77 78 79
            if force
              record.save!
            else
              return false unless record.save
            end
80
          end
81

82 83
          if @reflection.options[:insert_sql]
            @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
D
Initial  
David Heinemeier Hansson 已提交
84
          else
85
            columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
86 87 88

            attributes = columns.inject({}) do |attributes, column|
              case column.name
89
                when @reflection.primary_key_name
90
                  attributes[column.name] = @owner.quoted_id
91
                when @reflection.association_foreign_key
92 93
                  attributes[column.name] = record.quoted_id
                else
94
                  if record.attributes.has_key?(column.name)
95
                    value = @owner.send(:quote_value, record[column.name], column)
96 97
                    attributes[column.name] = value unless value.nil?
                  end
98 99 100 101 102
              end
              attributes
            end

            sql =
103
              "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
104
              "VALUES (#{attributes.values.join(', ')})"
105

D
Initial  
David Heinemeier Hansson 已提交
106 107
            @owner.connection.execute(sql)
          end
108

109
          return true
D
Initial  
David Heinemeier Hansson 已提交
110
        end
111

D
Initial  
David Heinemeier Hansson 已提交
112
        def delete_records(records)
113
          if sql = @reflection.options[:delete_sql]
114
            records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
D
Initial  
David Heinemeier Hansson 已提交
115 116
          else
            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})"
D
Initial  
David Heinemeier Hansson 已提交
118 119 120
            @owner.connection.execute(sql)
          end
        end
121

122
        def construct_sql
123
          interpolate_sql_options!(@reflection.options, :finder_sql)
124

125 126
          if @reflection.options[:finder_sql]
            @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 137 138 139 140
          { :find => {  :conditions => @finder_sql,
                        :joins => @join_sql,
                        :readonly => false,
                        :order => @reflection.options[:order],
                        :limit => @reflection.options[:limit] } }
141 142
        end

143
        # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
144 145
        # clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
        # an id column. This will then overwrite the id column of the records coming back.
146
        def finding_with_ambiguous_select?(select_clause)
147 148
          !select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
        end
149 150 151 152 153 154 155 156 157 158 159 160 161

      private
        def create_record(attributes)
          # Can't use Base.create because the foreign key may be a protected attribute.
          ensure_owner_is_not_new
          if attributes.is_a?(Array)
            attributes.collect { |attr| create(attr) }
          else
            record = build(attributes)
            yield(record)
            record
          end
        end
162
    end
D
Initial  
David Heinemeier Hansson 已提交
163
  end
164
end