relation.rb 6.0 KB
Newer Older
1 2
module ActiveRecord
  class Relation
3 4 5 6 7
    JoinOperation = Struct.new(:relation, :join_class, :on)
    ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
    MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
    SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from]

8
    include FinderMethods, Calculations, SpawnMethods, QueryMethods
9

10
    delegate :length, :collect, :map, :each, :all?, :include?, :to => :to_a
P
Pratik Naik 已提交
11
    delegate :insert, :update, :to => :arel
12

13
    attr_reader :table, :klass
14

15 16 17
    def initialize(klass, table)
      @klass, @table = klass, table
      (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
18 19
    end

P
Pratik Naik 已提交
20
    def new(*args, &block)
21 22 23
      with_create_scope { @klass.new(*args, &block) }
    end

24 25
    alias build new

26 27 28 29 30 31
    def create(*args, &block)
      with_create_scope { @klass.create(*args, &block) }
    end

    def create!(*args, &block)
      with_create_scope { @klass.create!(*args, &block) }
P
Pratik Naik 已提交
32 33
    end

34
    def respond_to?(method, include_private = false)
35
      return true if arel.respond_to?(method, include_private) || Array.method_defined?(method)
36 37 38 39 40 41 42 43

      if match = DynamicFinderMatch.match(method)
        return true if @klass.send(:all_attributes_exists?, match.attribute_names)
      elsif match = DynamicScopeMatch.match(method)
        return true if @klass.send(:all_attributes_exists?, match.attribute_names)
      else
        super
      end
44 45
    end

P
Pratik Naik 已提交
46 47 48
    def to_a
      return @records if loaded?

49
      @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
P
Pratik Naik 已提交
50

51
      preload = @preload_values
52
      preload +=  @includes_values unless eager_loading?
53 54
      preload.each {|associations| @klass.send(:preload_associations, @records, associations) } 

55 56 57
      # @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT.
      readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
      @records.each { |record| record.readonly! } if readonly
P
Pratik Naik 已提交
58 59 60 61 62

      @loaded = true
      @records
    end

63 64 65 66 67 68 69 70
    def size
      loaded? ? @records.length : count
    end

    def empty?
      loaded? ? @records.empty? : count.zero?
    end

P
Pratik Naik 已提交
71 72 73 74 75 76 77 78 79 80 81 82
    def any?
      if block_given?
        to_a.any? { |*block_args| yield(*block_args) }
      else
        !empty?
      end
    end

    def many?
      if block_given?
        to_a.many? { |*block_args| yield(*block_args) }
      else
83
        arel.send(:taken).present? ? to_a.many? : size > 1
P
Pratik Naik 已提交
84 85 86
      end
    end

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
    # Destroys the records matching +conditions+ by instantiating each
    # record and calling its +destroy+ method. Each object's callbacks are
    # executed (including <tt>:dependent</tt> association options and
    # +before_destroy+/+after_destroy+ Observer methods). Returns the
    # collection of objects that were destroyed; each will be frozen, to
    # reflect that no changes should be made (since they can't be
    # persisted).
    #
    # Note: Instantiation, callback execution, and deletion of each
    # record can be time consuming when you're removing many records at
    # once. It generates at least one SQL +DELETE+ query per record (or
    # possibly more, to enforce your callbacks). If you want to delete many
    # rows quickly, without concern for their associations or callbacks, use
    # +delete_all+ instead.
    #
    # ==== Parameters
    #
    # * +conditions+ - A string, array, or hash that specifies which records
    #   to destroy. If omitted, all records are destroyed. See the
    #   Conditions section in the introduction to ActiveRecord::Base for
    #   more information.
    #
    # ==== Examples
    #
    #   Person.destroy_all("last_login < '2004-04-04'")
    #   Person.destroy_all(:status => "inactive")
    def destroy_all(conditions = nil)
      if conditions
        where(conditions).destroy_all
      else
        to_a.each {|object| object.destroy}
        reset
      end
P
Pratik Naik 已提交
120 121
    end

P
Pratik Naik 已提交
122
    def delete_all
123
      arel.delete.tap { reset }
P
Pratik Naik 已提交
124 125
    end

126 127 128 129
    def delete(id_or_array)
      where(@klass.primary_key => id_or_array).delete_all
    end

P
Pratik Naik 已提交
130 131 132 133
    def loaded?
      @loaded
    end

134
    def reload
P
Pratik Naik 已提交
135
      reset
136 137
      to_a # force reload
      self
P
Pratik Naik 已提交
138 139 140
    end

    def reset
141
      @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
142
      @should_eager_load = @join_dependency = nil
P
Pratik Naik 已提交
143
      @records = []
144 145 146
      self
    end

147 148 149 150
    def primary_key
      @primary_key ||= table[@klass.primary_key]
    end

P
Pratik Naik 已提交
151
    def to_sql
152
      @to_sql ||= arel.to_sql
P
Pratik Naik 已提交
153 154
    end

P
Pratik Naik 已提交
155 156 157 158 159 160 161 162 163
    def scope_for_create
      @scope_for_create ||= begin
        @create_with_value || wheres.inject({}) do |hash, where|
          hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality)
          hash
        end
      end
    end

164 165 166 167
    def eager_loading?
      @should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?))
    end

168
    protected
169 170

    def method_missing(method, *args, &block)
171
      if Array.method_defined?(method)
172
        to_a.send(method, *args, &block)
173 174
      elsif arel.respond_to?(method)
        arel.send(method, *args, &block)
175 176 177 178 179
      elsif match = DynamicFinderMatch.match(method)
        attributes = match.attribute_names
        super unless @klass.send(:all_attributes_exists?, attributes)

        if match.finder?
180
          find_by_attributes(match, attributes, *args)
181 182
        elsif match.instantiator?
          find_or_instantiator_by_attributes(match, attributes, *args, &block)
183
        end
184 185
      else
        super
186
      end
187 188
    end

189 190
    private

191
    def with_create_scope
192
      @klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield }
193 194
    end

195
    def references_eager_loaded_tables?
196
      joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq
197
      (tables_in_string(to_sql) - joined_tables).any?
198 199 200 201 202 203 204
    end

    def tables_in_string(string)
      return [] if string.blank?
      string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.uniq
    end

205 206
  end
end