finder_methods.rb 14.4 KB
Newer Older
1
require 'active_support/core_ext/object/blank'
2
require 'active_support/core_ext/hash/indifferent_access'
3

4 5
module ActiveRecord
  module FinderMethods
P
Pratik Naik 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
    # Find operates with four different retrieval approaches:
    #
    # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
    #   If no record can be found for all of the listed ids, then RecordNotFound will be raised.
    # * Find first - This will return the first record matched by the options used. These options can either be specific
    #   conditions or merely an order. If no record can be matched, +nil+ is returned. Use
    #   <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>.
    # * Find last - This will return the last record matched by the options used. These options can either be specific
    #   conditions or merely an order. If no record can be matched, +nil+ is returned. Use
    #   <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>.
    # * Find all - This will return all the records matched by the options used.
    #   If no records are found, an empty array is returned. Use
    #   <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>.
    #
    # All approaches accept an options hash as their last parameter.
    #
    # ==== Parameters
    #
24 25
    # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, 
    #   or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
P
Pratik Naik 已提交
26 27
    # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name".
    # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
28 29
    # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a 
    #   <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
P
Pratik Naik 已提交
30
    # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
31 32
    # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, 
    #   it would skip rows 0 through 4.
P
Pratik Naik 已提交
33
    # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed),
34 35
    #   named associations in the same form used for the <tt>:include</tt> option, which will perform an 
    #   <tt>INNER JOIN</tt> on the associated table(s),
P
Pratik Naik 已提交
36
    #   or an array containing a mixture of both strings and named associations.
37 38
    #   If the value is a string, then the records will be returned read-only since they will 
    #   have attributes that do not correspond to the table's columns.
P
Pratik Naik 已提交
39 40 41
    #   Pass <tt>:readonly => false</tt> to override.
    # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer
    #   to already defined associations. See eager loading under Associations.
42 43 44 45
    # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, 
    #   for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name").
    # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed 
    #   to an alternate table name (or even the name of a database view).
P
Pratik Naik 已提交
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
    # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated.
    # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE".
    #   <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE".
    #
    # ==== Examples
    #
    #   # find by id
    #   Person.find(1)       # returns the object for ID = 1
    #   Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
    #   Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
    #   Person.find([1])     # returns an array for the object with ID = 1
    #   Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
    #
    # Note that returned records may not be in the same order as the ids you
    # provide since database rows are unordered. Give an explicit <tt>:order</tt>
    # to ensure the results are sorted.
    #
    # ==== Examples
    #
    #   # find first
    #   Person.find(:first) # returns the first object fetched by SELECT * FROM people
    #   Person.find(:first, :conditions => [ "user_name = ?", user_name])
    #   Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }])
    #   Person.find(:first, :order => "created_on DESC", :offset => 5)
    #
    #   # find last
    #   Person.find(:last) # returns the last object fetched by SELECT * FROM people
    #   Person.find(:last, :conditions => [ "user_name = ?", user_name])
    #   Person.find(:last, :order => "created_on DESC", :offset => 5)
    #
    #   # find all
    #   Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
    #   Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
    #   Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] }
    #   Person.find(:all, :offset => 10, :limit => 10)
    #   Person.find(:all, :include => [ :account, :friends ])
    #   Person.find(:all, :group => "category")
    #
    # Example for find with a lock: Imagine two concurrent transactions:
    # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
    # in two saves of <tt>person.visits = 3</tt>.  By locking the row, the second
    # transaction has to wait until the first is finished; we get the
    # expected <tt>person.visits == 4</tt>.
    #
    #   Person.transaction do
    #     person = Person.find(1, :lock => true)
    #     person.visits += 1
    #     person.save!
    #   end
95 96
    def find(*args)
      return to_a.find { |*block_args| yield(*block_args) } if block_given?
97

P
Pratik Naik 已提交
98
      options = args.extract_options!
99

P
Pratik Naik 已提交
100 101
      if options.present?
        apply_finder_options(options).find(*args)
102
      else
P
Pratik Naik 已提交
103 104 105 106 107 108
        case args.first
        when :first, :last, :all
          send(args.first)
        else
          find_with_ids(*args)
        end
109 110 111
      end
    end

P
Pratik Naik 已提交
112 113 114
    # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
    # same arguments to this method as you can to <tt>find(:first)</tt>.
    def first(*args)
115 116 117 118 119 120 121 122 123
      if args.any?
        if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
          to_a.first(*args)
        else
          apply_finder_options(args.first).first
        end
      else
        find_first
      end
124 125
    end

P
Pratik Naik 已提交
126 127 128
    # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
    # same arguments to this method as you can to <tt>find(:last)</tt>.
    def last(*args)
129 130 131 132 133 134 135 136 137
      if args.any?
        if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
          to_a.last(*args)
        else
          apply_finder_options(args.first).last
        end
      else
        find_last
      end
138 139
    end

P
Pratik Naik 已提交
140 141 142 143 144 145
    # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the
    # same arguments to this method as you can to <tt>find(:all)</tt>.
    def all(*args)
      args.any? ? apply_finder_options(args.first).to_a : to_a
    end

P
Pratik Naik 已提交
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
    # Returns true if a record exists in the table that matches the +id+ or
    # conditions given, or false otherwise. The argument can take five forms:
    #
    # * Integer - Finds the record with this primary key.
    # * String - Finds the record with a primary key corresponding to this
    #   string (such as <tt>'5'</tt>).
    # * Array - Finds the record that matches these +find+-style conditions
    #   (such as <tt>['color = ?', 'red']</tt>).
    # * Hash - Finds the record that matches these +find+-style conditions
    #   (such as <tt>{:color => 'red'}</tt>).
    # * No args - Returns false if the table is empty, true otherwise.
    #
    # For more information about specifying conditions as a Hash or Array,
    # see the Conditions section in the introduction to ActiveRecord::Base.
    #
    # Note: You can't pass in a condition as a string (like <tt>name =
    # 'Jamie'</tt>), since it would be sanitized and then queried against
    # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
    #
    # ==== Examples
    #   Person.exists?(5)
    #   Person.exists?('5')
    #   Person.exists?(:name => "David")
    #   Person.exists?(['name LIKE ?', "%#{query}%"])
    #   Person.exists?
P
Pratik Naik 已提交
171
    def exists?(id = nil)
172 173
      id = id.id if ActiveRecord::Base === id

P
Pratik Naik 已提交
174 175 176 177 178 179 180 181
      case id
      when Array, Hash
        where(id).exists?
      else
        relation = select(primary_key).limit(1)
        relation = relation.where(primary_key.eq(id)) if id
        relation.first ? true : false
      end
182 183 184 185
    end

    protected

186 187 188 189 190 191 192 193 194
    def find_with_associations
      including = (@eager_load_values + @includes_values).uniq
      join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
      rows = construct_relation_for_association_find(join_dependency).to_a
      join_dependency.instantiate(rows)
    rescue ThrowResult
      []
    end

195 196 197
    def construct_relation_for_association_calculations
      including = (@eager_load_values + @includes_values).uniq
      join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
P
Pratik Naik 已提交
198 199
      relation = except(:includes, :eager_load, :preload)
      apply_join_dependency(relation, join_dependency)
200 201
    end

202
    def construct_relation_for_association_find(join_dependency)
203
      relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency))
P
Pratik Naik 已提交
204 205
      apply_join_dependency(relation, join_dependency)
    end
206

P
Pratik Naik 已提交
207
    def apply_join_dependency(relation, join_dependency)
208 209 210 211
      for association in join_dependency.join_associations
        relation = association.join_relation(relation)
      end

212
      limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231

      if !limitable_reflections && relation.limit_value
        limited_id_condition = construct_limited_ids_condition(relation.except(:select))
        relation = relation.where(limited_id_condition)
      end

      relation = relation.except(:limit, :offset) unless limitable_reflections

      relation
    end

    def construct_limited_ids_condition(relation)
      orders = relation.order_values.join(", ")
      values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders)

      ids_array = relation.select(values).collect {|row| row[@klass.primary_key]}
      ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array)
    end

232 233 234 235 236 237 238 239 240 241 242 243
    def find_by_attributes(match, attributes, *args)
      conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h}
      result = where(conditions).send(match.finder)

      if match.bang? && result.blank?
        raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
      else
        result
      end
    end

    def find_or_instantiator_by_attributes(match, attributes, *args)
244
      protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
245 246 247 248 249 250
      args.each_with_index do |arg, i|
        if arg.is_a?(Hash)
          protected_attributes_for_create = args[i].with_indifferent_access
        else
          unprotected_attributes_for_create[attributes[i]] = args[i]
        end
251 252
      end

253 254
      conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys

255 256 257
      record = where(conditions).first

      unless record
258 259 260 261
        record = @klass.new do |r|
          r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
          r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
        end
262 263 264 265 266 267 268
        yield(record) if block_given?
        record.save if match.instantiator == :create
      end

      record
    end

269 270
    def find_with_ids(*ids)
      return to_a.find { |*block_args| yield(*block_args) } if block_given?
P
Pratik Naik 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287

      expects_array = ids.first.kind_of?(Array)
      return ids.first if expects_array && ids.first.empty?

      ids = ids.flatten.compact.uniq

      case ids.size
      when 0
        raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
      when 1
        result = find_one(ids.first)
        expects_array ? [ result ] : result
      else
        find_some(ids)
      end
    end

288
    def find_one(id)
A
Aaron Patterson 已提交
289 290
      id = id.id if ActiveRecord::Base === id

291
      record = where(primary_key.eq(id)).first
292 293

      unless record
P
Pratik Naik 已提交
294
        conditions = arel.send(:where_clauses).join(', ')
295 296 297 298 299 300 301 302
        conditions = " [WHERE #{conditions}]" if conditions.present?
        raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
      end

      record
    end

    def find_some(ids)
303
      result = where(primary_key.in(ids)).all
304 305

      expected_size =
306 307
        if @limit_value && ids.size > @limit_value
          @limit_value
308 309 310 311 312
        else
          ids.size
        end

      # 11 ids with limit 3, offset 9 should give 2 results.
313 314
      if @offset_value && (ids.size - @offset_value < expected_size)
        expected_size = ids.size - @offset_value
315 316 317 318 319
      end

      if result.size == expected_size
        result
      else
P
Pratik Naik 已提交
320
        conditions = arel.send(:where_clauses).join(', ')
321 322 323 324 325 326 327 328
        conditions = " [WHERE #{conditions}]" if conditions.present?

        error = "Couldn't find all #{@klass.name.pluralize} with IDs "
        error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
        raise RecordNotFound, error
      end
    end

P
Pratik Naik 已提交
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
    def find_first
      if loaded?
        @records.first
      else
        @first ||= limit(1).to_a[0]
      end
    end

    def find_last
      if loaded?
        @records.last
      else
        @last ||= reverse_order.limit(1).to_a[0]
      end
    end

345 346 347 348 349 350
    def column_aliases(join_dependency)
      join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
          "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
    end

    def using_limitable_reflections?(reflections)
351
      reflections.none?(&:collection?)
352 353
    end

354 355
  end
end