has_and_belongs_to_many_association.rb 5.4 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2 3 4 5
module ActiveRecord
  module Associations
    class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
      def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
        super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6

7
        @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
8
        association_table_name = options[:table_name] || @association_class.table_name
D
Initial  
David Heinemeier Hansson 已提交
9
        @join_table = join_table
10
        @order = options[:order] || "t.#{@association_class.primary_key}"
D
Initial  
David Heinemeier Hansson 已提交
11 12 13 14

        interpolate_sql_options!(options, :finder_sql, :delete_sql)
        @finder_sql = options[:finder_sql] ||
              "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
15
              "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
16
              "j.#{association_class_primary_key_name} = #{@owner.quoted_id} " +
17
              (options[:conditions] ? " AND " + interpolate_sql(options[:conditions]) : "") + " " +
D
Initial  
David Heinemeier Hansson 已提交
18 19 20 21 22 23 24 25 26 27 28
              "ORDER BY #{@order}"
      end
 
      # Removes all records from this association.  Returns +self+ so method calls may be chained.
      def clear
        return self if size == 0 # forces load_collection if hasn't happened already

        if sql = @options[:delete_sql]
          each { |record| @owner.connection.execute(sql) }
        elsif @options[:conditions] 
          sql = 
29
            "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} " +
D
Initial  
David Heinemeier Hansson 已提交
30 31 32
            "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})"
          @owner.connection.execute(sql)
        else
33
          sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id}"
D
Initial  
David Heinemeier Hansson 已提交
34 35 36 37 38 39 40
          @owner.connection.execute(sql)
        end

        @collection = []
        self
      end

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
      def find_first
        load_collection.first
      end

      def find(*args)
        # Return an Array if multiple ids are given.
        expects_array = args.first.kind_of?(Array)

        ids = args.flatten.compact.uniq

        # If no block is given, raise RecordNotFound.
        if ids.empty?
          raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID#{conditions}"

        # If using a custom finder_sql, scan the entire collection.
        elsif @options[:finder_sql]
          if ids.size == 1
            id = ids.first
            record = load_collection.detect { |record| id == record.id }
            expects_array? ? [record] : record
          else
            load_collection.select { |record| ids.include?(record.id) }
          end

        # Otherwise, construct a query.
D
Initial  
David Heinemeier Hansson 已提交
66
        else
67 68 69 70 71 72 73 74
          ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
          records = find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
          if records.size == ids.size
            if ids.size == 1 and !expects_array
              records.first
            else
              records
            end
D
Initial  
David Heinemeier Hansson 已提交
75
          else
76
            raise RecordNotFound, "Couldn't find #{@association_class.name} with ID in (#{ids_list})"
D
Initial  
David Heinemeier Hansson 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
          end
        end
      end

      def push_with_attributes(record, join_attributes = {})
        raise_on_type_mismatch(record)
        insert_record_with_join_attributes(record, join_attributes)
        join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
        @collection << record if loaded?
        self
      end
      
      alias :concat_with_attributes :push_with_attributes

      def size
        @options[:uniq] ? count_records : super
      end
      
      protected
        def find_all_records(sql = @finder_sql)
          records = @association_class.find_by_sql(sql)
          @options[:uniq] ? uniq(records) : records
        end
100

D
Initial  
David Heinemeier Hansson 已提交
101
        def count_records
102
          load_collection.size
D
Initial  
David Heinemeier Hansson 已提交
103 104 105 106 107 108
        end

        def insert_record(record)
          if @options[:insert_sql]
            @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
          else
109 110
            sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) " + 
                  "VALUES (#{@owner.quoted_id},#{record.quoted_id})"
D
Initial  
David Heinemeier Hansson 已提交
111 112 113 114 115
            @owner.connection.execute(sql)
          end
        end
        
        def insert_record_with_join_attributes(record, join_attributes)
116
          attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
D
Initial  
David Heinemeier Hansson 已提交
117 118 119 120 121
          sql = 
            "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
            "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})"
          @owner.connection.execute(sql)
        end
122
        
D
Initial  
David Heinemeier Hansson 已提交
123 124 125 126 127
        def delete_records(records)
          if sql = @options[:delete_sql]
            records.each { |record| @owner.connection.execute(sql) }
          else
            ids = quoted_record_ids(records)
128
            sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_foreign_key} IN (#{ids})"
D
Initial  
David Heinemeier Hansson 已提交
129 130 131 132 133
            @owner.connection.execute(sql)
          end
        end
      end
  end
134
end