association_collection.rb 5.9 KB
Newer Older
1 2
require 'set'

D
Initial  
David Heinemeier Hansson 已提交
3 4
module ActiveRecord
  module Associations
5
    class AssociationCollection < AssociationProxy #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
6
      def to_ary
7 8
        load_target
        @target.to_ary
D
Initial  
David Heinemeier Hansson 已提交
9 10
      end
  
11
      def reset
12
        reset_target!
13
        @loaded = false
D
Initial  
David Heinemeier Hansson 已提交
14 15 16 17 18
      end

      # Add +records+ to this association.  Returns +self+ so method calls may be chained.  
      # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
      def <<(*records)
19 20
        result = true
        load_target
21

22 23 24
        @owner.transaction do
          flatten_deeper(records).each do |record|
            raise_on_type_mismatch(record)
25
            callback(:before_add, record)
26 27
            result &&= insert_record(record) unless @owner.new_record?
            @target << record
28
            callback(:after_add, record)
29
          end
D
Initial  
David Heinemeier Hansson 已提交
30
        end
31

32
        result && self
D
Initial  
David Heinemeier Hansson 已提交
33 34 35 36
      end

      alias_method :push, :<<
      alias_method :concat, :<<
37 38 39
                      
      # Remove all records from this association
      def delete_all
40
        load_target
41
        delete(@target)
42
        reset_target!
43
      end
D
Initial  
David Heinemeier Hansson 已提交
44 45 46 47

      # Remove +records+ from this association.  Does not destroy +records+.
      def delete(*records)
        records = flatten_deeper(records)
48 49 50
        records.each { |record| raise_on_type_mismatch(record) }
        records.reject! { |record| @target.delete(record) if record.new_record? }
        return if records.empty?
51 52
        
        @owner.transaction do
53
          records.each { |record| callback(:before_remove, record) }
54
          delete_records(records)
55 56 57 58
          records.each do |record|
            @target.delete(record)
            callback(:after_remove, record)
          end
59
        end
D
Initial  
David Heinemeier Hansson 已提交
60
      end
61 62 63

      # Removes all records from this association.  Returns +self+ so method calls may be chained.
      def clear
64
        return self if length.zero? # forces load_target if hasn't happened already
65

66
        if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
67 68 69 70
          destroy_all
        else          
          delete_all
        end
71

72 73
        self
      end
D
Initial  
David Heinemeier Hansson 已提交
74 75
      
      def destroy_all
76 77 78 79
        @owner.transaction do
          each { |record| record.destroy }
        end

80
        reset_target!
D
Initial  
David Heinemeier Hansson 已提交
81
      end
82

83 84
      def create(attributes = {})
        # Can't use Base.create since the foreign key may be a protected attribute.
85 86 87 88 89 90 91
        if attributes.is_a?(Array)
          attributes.collect { |attr| create(attr) }
        else
          record = build(attributes)
          record.save unless @owner.new_record?
          record
        end
92 93
      end

94 95 96
      # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
      # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
      # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
D
Initial  
David Heinemeier Hansson 已提交
97
      def size
98 99 100 101 102 103 104 105
        if loaded? && !@reflection.options[:uniq]
          @target.size
        elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
          unsaved_records = Array(@target.detect { |r| r.new_record? }).size
          unsaved_records + count_records
        else
          count_records
        end
D
Initial  
David Heinemeier Hansson 已提交
106
      end
107

108 109 110
      # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
      # whether the collection is empty, use collection.length.zero? instead of collection.empty?
      def length
111
        load_target.size
112
      end
113

D
Initial  
David Heinemeier Hansson 已提交
114
      def empty?
115
        size.zero?
D
Initial  
David Heinemeier Hansson 已提交
116
      end
117

D
Initial  
David Heinemeier Hansson 已提交
118
      def uniq(collection = self)
119 120 121 122 123 124 125 126
        seen = Set.new
        collection.inject([]) do |kept, record|
          unless seen.include?(record.id)
            kept << record
            seen << record.id
          end
          kept
        end
D
Initial  
David Heinemeier Hansson 已提交
127 128
      end

129 130
      # Replace this collection with +other_array+
      # This will perform a diff and delete/add only records that have changed.
131
      def replace(other_array)
132 133 134 135 136
        other_array.each { |val| raise_on_type_mismatch(val) }

        load_target
        other   = other_array.size < 100 ? other_array : other_array.to_set
        current = @target.size < 100 ? @target : @target.to_set
137

138 139 140 141
        @owner.transaction do
          delete(@target.select { |v| !other.include?(v) })
          concat(other_array.select { |v| !current.include?(v) })
        end
142
      end
D
Initial  
David Heinemeier Hansson 已提交
143

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
      protected
        def reset_target!
          @target = Array.new
        end

        def find_target
          records =
            if @reflection.options[:finder_sql]
              @reflection.klass.find_by_sql(@finder_sql)
            else
              find(:all)
            end

          @reflection.options[:uniq] ? uniq(records) : records
        end

D
Initial  
David Heinemeier Hansson 已提交
160
      private
161
        # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
D
Initial  
David Heinemeier Hansson 已提交
162 163 164
        def flatten_deeper(array)
          array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
        end
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
        
        def callback(method, record)
          callbacks_for(method).each do |callback|
            case callback
              when Symbol
                @owner.send(callback, record)
              when Proc, Method
                callback.call(@owner, record)
              else
                if callback.respond_to?(method)
                  callback.send(method, @owner, record)
                else
                  raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
                end
            end
          end
        end
        
        def callbacks_for(callback_name)
184 185
          full_callback_name = "#{callback_name}_for_#{@reflection.name}"
          @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
186 187
        end
        
D
Initial  
David Heinemeier Hansson 已提交
188 189
    end
  end
190
end