aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/association_collection.rb
blob: 6283f3dc281ee66b9ed5a2dfadd3c683d6bea466 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
module ActiveRecord
  module Associations
    class AssociationCollection #:nodoc:
      alias_method :proxy_respond_to?, :respond_to?
      instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }

      def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
        @owner = owner
        @options = options
        @association_name = association_name
        @association_class = eval(association_class_name)
        @association_class_primary_key_name = association_class_primary_key_name
      end
      
      def method_missing(symbol, *args, &block)
        load_collection
        @collection.send(symbol, *args, &block)
      end
  
      def to_ary
        load_collection
        @collection.to_ary
      end
  
      def respond_to?(symbol, include_priv = false)
        proxy_respond_to?(symbol, include_priv) || [].respond_to?(symbol, include_priv)
      end

      def loaded?
        !@collection.nil?
      end
      
      def reload
        @collection = nil
      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)
        @owner.transaction do
          flatten_deeper(records).each do |record|
            raise_on_type_mismatch(record)
            insert_record(record)
            @collection << record if loaded?
          end
        end

        self
      end

      alias_method :push, :<<
      alias_method :concat, :<<

      # Remove +records+ from this association.  Does not destroy +records+.
      def delete(*records)
        records = flatten_deeper(records)
        
        @owner.transaction do
          records.each { |record| raise_on_type_mismatch(record) }
          delete_records(records)
          records.each { |record| @collection.delete(record) } if loaded?
        end
      end
      
      def destroy_all
        @owner.transaction do
          each { |record| record.destroy }
        end

        @collection = []
      end
      
      def size
        if loaded? then @collection.size else count_records end
      end
      
      def empty?
        size == 0
      end
      
      def uniq(collection = self)
        collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
      end
      
      alias_method :length, :size

      protected
        def loaded?
          not @collection.nil?
        end

        def quoted_record_ids(records)
          records.map { |record| record.quoted_id }.join(',')
        end

        def interpolate_sql_options!(options, *keys)
          keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
        end

        def interpolate_sql(sql, record = nil)
          @owner.send(:interpolate_sql, sql, record)
        end

        def sanitize_sql(sql)
          @association_class.send(:sanitize_sql, sql)
        end

        def extract_options_from_args!(args)
          @owner.send(:extract_options_from_args!, args)
        end

      private
        def load_collection
          if loaded?
            @collection
          else
            begin
              @collection = find_all_records
            rescue ActiveRecord::RecordNotFound
              @collection = []
            end
          end
        end

        def raise_on_type_mismatch(record)
          raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
        end

        # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
        def flatten_deeper(array)
          array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
        end
    end
  end
end