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
        record = @reflection.klass.new(attributes)
12 13 14 15
        @target << record
        record
      end

16
      def create(attributes = {})
17
        # Can't use Base.create because the foreign key may be a protected attribute.
18
        ensure_owner_is_not_new
19 20 21 22
        if attributes.is_a?(Array)
          attributes.collect { |attr| create(attr) }
        else
          record = build(attributes)
23
          insert_record(record) unless @owner.new_record?
24 25 26
          record
        end
      end
27 28 29 30 31 32 33 34 35 36 37 38
      
      def create!(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)
          insert_record(record, true) unless @owner.new_record?
          record
        end        
      end
39

40
      def find_first
41
        load_target.first
42
      end
43

44
      def find(*args)
45
        options = args.extract_options!
46 47

        # If using a custom finder_sql, scan the entire collection.
48
        if @reflection.options[:finder_sql]
49 50 51
          expects_array = args.first.kind_of?(Array)
          ids = args.flatten.compact.uniq

52
          if ids.size == 1
53
            id = ids.first.to_i
54
            record = load_target.detect { |record| id == record.id }
55
            expects_array ? [record] : record
56
          else
57
            load_target.select { |record| ids.include?(record.id) }
58
          end
D
Initial  
David Heinemeier Hansson 已提交
59
        else
60
          conditions = "#{@finder_sql}"
61

62
          if sanitized_conditions = sanitize_sql(options[:conditions])
J
Jeremy Kemper 已提交
63
            conditions << " AND (#{sanitized_conditions})"
64
          end
65

66
          options[:conditions] = conditions
67
          options[:joins]      = @join_sql
68
          options[:readonly]   = finding_with_ambiguous_select?(options[:select])
69

70 71 72 73
          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 已提交
74
          end
75

76 77
          merge_options_from_reflection!(options)

78 79
          options[:select]   ||= '*'

80 81
          # Pass through args exactly as we received them.
          args << options
82
          @reflection.klass.find(*args)
D
Initial  
David Heinemeier Hansson 已提交
83 84 85 86 87
        end
      end

      protected
        def count_records
88
          load_target.size
D
Initial  
David Heinemeier Hansson 已提交
89 90
        end

91
        def insert_record(record, force=true)
92
          if record.new_record?
93 94 95 96 97
            if force
              record.save!
            else
              return false unless record.save
            end
98
          end
99

100 101
          if @reflection.options[:insert_sql]
            @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
D
Initial  
David Heinemeier Hansson 已提交
102
          else
103
            columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
104 105 106

            attributes = columns.inject({}) do |attributes, column|
              case column.name
107
                when @reflection.primary_key_name
108
                  attributes[column.name] = @owner.quoted_id
109
                when @reflection.association_foreign_key
110 111
                  attributes[column.name] = record.quoted_id
                else
112
                  if record.attributes.has_key?(column.name)
113
                    value = @owner.send(:quote_value, record[column.name], column)
114 115
                    attributes[column.name] = value unless value.nil?
                  end
116 117 118 119 120
              end
              attributes
            end

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

D
Initial  
David Heinemeier Hansson 已提交
124 125
            @owner.connection.execute(sql)
          end
126

127
          return true
D
Initial  
David Heinemeier Hansson 已提交
128
        end
129

D
Initial  
David Heinemeier Hansson 已提交
130
        def delete_records(records)
131
          if sql = @reflection.options[:delete_sql]
132
            records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
D
Initial  
David Heinemeier Hansson 已提交
133 134
          else
            ids = quoted_record_ids(records)
135
            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 已提交
136 137 138
            @owner.connection.execute(sql)
          end
        end
139

140
        def construct_sql
141
          interpolate_sql_options!(@reflection.options, :finder_sql)
142

143 144
          if @reflection.options[:finder_sql]
            @finder_sql = @reflection.options[:finder_sql]
145
          else
146
            @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
147
            @finder_sql << " AND (#{conditions})" if conditions
148
          end
149

150
          @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}"
151
        end
152

153 154 155 156
        def construct_scope
          { :find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false } }
        end

157
        # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
158 159
        # 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.
160
        def finding_with_ambiguous_select?(select_clause)
161 162
          !select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
        end
163
    end
D
Initial  
David Heinemeier Hansson 已提交
164
  end
165
end