finder_methods.rb 20.0 KB
Newer Older
X
Xavier Noria 已提交
1
require 'active_support/core_ext/string/filters'
2

3 4
module ActiveRecord
  module FinderMethods
V
Vipul A M 已提交
5 6
    ONE_AS_ONE = '1 AS one'

7
    # 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]).
8
    # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key
9
    # is an integer, find by id coerces its arguments using +to_i+.
P
Pratik Naik 已提交
10
    #
11 12 13 14 15 16
    #   Person.find(1)          # returns the object for ID = 1
    #   Person.find("1")        # returns the object for ID = 1
    #   Person.find("31-sarah") # returns the object for ID = 31
    #   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
E
Emilio Tagua 已提交
17
    #   Person.where("administrator = 1").order("created_on DESC").find(1)
P
Pratik Naik 已提交
18
    #
V
Vijay Dev 已提交
19
    # NOTE: The returned records may not be in the same order as the ids you
20
    # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order
V
Vijay Dev 已提交
21
    # option if you want the results are sorted.
P
Pratik Naik 已提交
22
    #
23
    # ==== Find with lock
P
Pratik Naik 已提交
24 25 26
    #
    # 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
27
    # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
P
Pratik Naik 已提交
28 29 30 31
    # transaction has to wait until the first is finished; we get the
    # expected <tt>person.visits == 4</tt>.
    #
    #   Person.transaction do
E
Emilio Tagua 已提交
32
    #     person = Person.lock(true).find(1)
P
Pratik Naik 已提交
33 34 35
    #     person.visits += 1
    #     person.save!
    #   end
36
    #
37
    # ==== Variations of #find
38
    #
39
    #   Person.where(name: 'Spartacus', rating: 4)
V
Vijay Dev 已提交
40
    #   # returns a chainable list (which can be empty).
41 42
    #
    #   Person.find_by(name: 'Spartacus', rating: 4)
V
Vijay Dev 已提交
43
    #   # returns the first item or nil.
44 45
    #
    #   Person.where(name: 'Spartacus', rating: 4).first_or_initialize
V
Vijay Dev 已提交
46
    #   # returns the first item or returns a new instance (requires you call .save to persist against the database).
47 48
    #
    #   Person.where(name: 'Spartacus', rating: 4).first_or_create
49
    #   # returns the first item or creates it and returns it.
50
    #
51
    # ==== Alternatives for #find
52 53
    #
    #   Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
V
Vijay Dev 已提交
54
    #   # returns a boolean indicating if any record with the given conditions exist.
55
    #
56
    #   Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
V
Vijay Dev 已提交
57
    #   # returns a chainable list of instances with only the mentioned fields.
58 59
    #
    #   Person.where(name: 'Spartacus', rating: 4).ids
60
    #   # returns an Array of ids.
61 62
    #
    #   Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
63
    #   # returns an Array of the required fields.
64
    def find(*args)
65 66
      return super if block_given?
      find_with_ids(*args)
67 68
    end

69
    # Finds the first record matching the specified conditions. There
70
    # is no implied ordering so if order matters, you should specify it
71 72 73 74 75 76
    # yourself.
    #
    # If no record is found, returns <tt>nil</tt>.
    #
    #   Post.find_by name: 'Spartacus', rating: 4
    #   Post.find_by "published_at < ?", 2.weeks.ago
77 78
    def find_by(arg, *args)
      where(arg, *args).take
79 80
    rescue RangeError
      nil
81 82
    end

83 84
    # Like #find_by, except that if no record is found, raises
    # an ActiveRecord::RecordNotFound error.
85 86
    def find_by!(arg, *args)
      where(arg, *args).take!
87
    rescue RangeError
88 89
      raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
                               @klass.name)
90 91
    end

92 93 94 95
    # Gives a record (or N records if a parameter is supplied) without any implied
    # order. The order will depend on the database implementation.
    # If an order is supplied it will be respected.
    #
96
    #   Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
97 98 99 100 101 102
    #   Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
    #   Person.where(["name LIKE '%?'", name]).take
    def take(limit = nil)
      limit ? limit(limit).to_a : find_take
    end

103 104
    # Same as #take but raises ActiveRecord::RecordNotFound if no record
    # is found. Note that #take! accepts no arguments.
105
    def take!
106
      take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
107 108
    end

109 110 111
    # Find the first record (or first N records if a parameter is supplied).
    # If no order is defined it will order by primary key.
    #
112
    #   Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1
113
    #   Person.where(["user_name = ?", user_name]).first
A
AvnerCohen 已提交
114
    #   Person.where(["user_name = :u", { u: user_name }]).first
115
    #   Person.order("created_on DESC").offset(5).first
116
    #   Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3
117
    #
118
    def first(limit = nil)
119
      if limit
120
        find_nth_with_limit_and_offset(0, limit, offset: offset_index)
121
      else
122
        find_nth 0
123
      end
124 125
    end

126 127
    # Same as #first but raises ActiveRecord::RecordNotFound if no record
    # is found. Note that #first! accepts no arguments.
P
Pratik Naik 已提交
128
    def first!
129
      find_nth! 0
130 131
    end

132 133 134
    # Find the last record (or last N records if a parameter is supplied).
    # If no order is defined it will order by primary key.
    #
135 136 137
    #   Person.last # returns the last object fetched by SELECT * FROM people
    #   Person.where(["user_name = ?", user_name]).last
    #   Person.order("created_on DESC").offset(5).last
138
    #   Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
139
    #
140
    # Take note that in that last case, the results are sorted in ascending order:
141
    #
142
    #   [#<Person id:2>, #<Person id:3>, #<Person id:4>]
143
    #
144
    # and not:
145
    #
146
    #   [#<Person id:4>, #<Person id:3>, #<Person id:2>]
147
    def last(limit = nil)
148 149
      if limit
        if order_values.empty? && primary_key
150
          order(arel_attribute(primary_key).desc).limit(limit).reverse
151 152 153 154 155 156
        else
          to_a.last(limit)
        end
      else
        find_last
      end
157 158
    end

159 160
    # Same as #last but raises ActiveRecord::RecordNotFound if no record
    # is found. Note that #last! accepts no arguments.
P
Pratik Naik 已提交
161
    def last!
162
      last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
163 164
    end

165 166 167 168 169 170 171
    # Find the second record.
    # If no order is defined it will order by primary key.
    #
    #   Person.second # returns the second object fetched by SELECT * FROM people
    #   Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
    #   Person.where(["user_name = :u", { u: user_name }]).second
    def second
172
      find_nth 1
173 174
    end

175
    # Same as #second but raises ActiveRecord::RecordNotFound if no record
176 177
    # is found.
    def second!
178
      find_nth! 1
179 180 181 182 183 184 185 186 187
    end

    # Find the third record.
    # If no order is defined it will order by primary key.
    #
    #   Person.third # returns the third object fetched by SELECT * FROM people
    #   Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
    #   Person.where(["user_name = :u", { u: user_name }]).third
    def third
188
      find_nth 2
189 190
    end

191
    # Same as #third but raises ActiveRecord::RecordNotFound if no record
192 193
    # is found.
    def third!
194
      find_nth! 2
195 196 197 198 199 200 201 202 203
    end

    # Find the fourth record.
    # If no order is defined it will order by primary key.
    #
    #   Person.fourth # returns the fourth object fetched by SELECT * FROM people
    #   Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
    #   Person.where(["user_name = :u", { u: user_name }]).fourth
    def fourth
204
      find_nth 3
205 206
    end

207
    # Same as #fourth but raises ActiveRecord::RecordNotFound if no record
208 209
    # is found.
    def fourth!
210
      find_nth! 3
211 212 213 214 215 216 217 218 219
    end

    # Find the fifth record.
    # If no order is defined it will order by primary key.
    #
    #   Person.fifth # returns the fifth object fetched by SELECT * FROM people
    #   Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
    #   Person.where(["user_name = :u", { u: user_name }]).fifth
    def fifth
220
      find_nth 4
221 222
    end

223
    # Same as #fifth but raises ActiveRecord::RecordNotFound if no record
224 225
    # is found.
    def fifth!
226
      find_nth! 4
227 228 229 230 231 232
    end

    # Find the forty-second record. Also known as accessing "the reddit".
    # If no order is defined it will order by primary key.
    #
    #   Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
233
    #   Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
234 235
    #   Person.where(["user_name = :u", { u: user_name }]).forty_two
    def forty_two
236
      find_nth 41
237 238
    end

239
    # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record
240 241
    # is found.
    def forty_two!
242
      find_nth! 41
243 244
    end

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    # Find the third-to-last record.
    # If no order is defined it will order by primary key.
    #
    #   Person.antepenultimate # returns the third-to-last object fetched by SELECT * FROM people
    #   Person.offset(3).antepenultimate # returns the third-to-last object from OFFSET 3
    #   Person.where(["user_name = :u", { u: user_name }]).antepenultimate
    def antepenultimate
      find_nth -3
    end

    # Same as #antepenultimate but raises ActiveRecord::RecordNotFound if no record
    # is found.
    def antepenultimate!
      find_nth! -3
    end

    # Find the second-to-last record.
    # If no order is defined it will order by primary key.
    #
    #   Person.penultimate # returns the second-to-last object fetched by SELECT * FROM people
    #   Person.offset(3).penultimate # returns the second-to-last object from OFFSET 3
    #   Person.where(["user_name = :u", { u: user_name }]).penultimate
    def penultimate
      find_nth -2
    end

    # Same as #penultimate but raises ActiveRecord::RecordNotFound if no record
    # is found.
    def penultimate!
      find_nth! -2
    end

277 278
    # Returns true if a record exists in the table that matches the +id+ or
    # conditions given, or false otherwise. The argument can take six forms:
P
Pratik Naik 已提交
279 280 281 282 283
    #
    # * 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
284
    #   (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
P
Pratik Naik 已提交
285
    # * Hash - Finds the record that matches these +find+-style conditions
286
    #   (such as <tt>{name: 'David'}</tt>).
287 288
    # * +false+ - Returns always +false+.
    # * No args - Returns +false+ if the table is empty, +true+ otherwise.
P
Pratik Naik 已提交
289
    #
290
    # For more information about specifying conditions as a hash or array,
291
    # see the Conditions section in the introduction to ActiveRecord::Base.
P
Pratik Naik 已提交
292 293 294 295 296 297 298 299
    #
    # 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>.
    #
    #   Person.exists?(5)
    #   Person.exists?('5')
    #   Person.exists?(['name LIKE ?', "%#{query}%"])
300
    #   Person.exists?(id: [1, 4, 8])
301 302
    #   Person.exists?(name: 'David')
    #   Person.exists?(false)
P
Pratik Naik 已提交
303
    #   Person.exists?
E
Egor Lynko 已提交
304
    def exists?(conditions = :none)
305 306
      if Base === conditions
        conditions = conditions.id
X
Xavier Noria 已提交
307 308 309 310
        ActiveSupport::Deprecation.warn(<<-MSG.squish)
          You are passing an instance of ActiveRecord::Base to `exists?`.
          Please pass the id of the object by calling `.id`
        MSG
311 312
      end

E
Egor Lynko 已提交
313
      return false if !conditions
314

315 316
      relation = apply_join_dependency(self, construct_join_dependency)
      return false if ActiveRecord::NullRelation === relation
317

V
Vipul A M 已提交
318
      relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
A
Aaron Patterson 已提交
319

E
Egor Lynko 已提交
320
      case conditions
P
Pratik Naik 已提交
321
      when Array, Hash
E
Egor Lynko 已提交
322
        relation = relation.where(conditions)
P
Pratik Naik 已提交
323
      else
324
        unless conditions == :none
325
          relation = relation.where(primary_key => conditions)
326
        end
P
Pratik Naik 已提交
327
      end
328

S
Sean Griffin 已提交
329
      connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false
330 331
    end

332
    # This method is called whenever no records are found with either a single
333
    # id or multiple ids and raises a ActiveRecord::RecordNotFound exception.
334 335 336 337 338 339 340
    #
    # The error message is different depending on whether a single id or
    # multiple ids are provided. If multiple ids are provided, then the number
    # of results obtained should be provided in the +result_size+ argument and
    # the expected number of results should be provided in the +expected_size+
    # argument.
    def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
341
      conditions = arel.where_sql(@klass.arel_engine)
342 343 344
      conditions = " [#{conditions}]" if conditions

      if Array(ids).size == 1
345
        error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
346
      else
347
        error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
348 349 350 351 352 353
        error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
      end

      raise RecordNotFound, error
    end

354
    private
355

356 357 358 359
    def offset_index
      offset_value || 0
    end

360
    def find_with_associations
361 362 363 364 365 366 367 368 369 370
      # NOTE: the JoinDependency constructed here needs to know about
      #       any joins already present in `self`, so pass them in
      #
      # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
      # incorrect SQL is generated. In that case, the join dependency for
      # SpecialCategorizations is constructed without knowledge of the
      # preexisting join in joins_values to categorizations (by way of
      # the `has_many :through` for categories).
      #
      join_dependency = construct_join_dependency(joins_values)
371 372

      aliases  = join_dependency.aliases
373
      relation = select aliases.columns
A
Aaron Patterson 已提交
374 375
      relation = apply_join_dependency(relation, join_dependency)

376 377
      if block_given?
        yield relation
378
      else
379 380 381
        if ActiveRecord::NullRelation === relation
          []
        else
382
          arel = relation.arel
S
Sean Griffin 已提交
383
          rows = connection.select_all(arel, 'SQL', relation.bound_attributes)
A
Aaron Patterson 已提交
384
          join_dependency.instantiate(rows, aliases)
385
        end
386
      end
387 388
    end

389
    def construct_join_dependency(joins = [])
390
      including = eager_load_values + includes_values
391
      ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
392 393
    end

394
    def construct_relation_for_association_calculations
395 396
      from = arel.froms.first
      if Arel::Table === from
397
        apply_join_dependency(self, construct_join_dependency(joins_values))
398 399 400 401 402 403
      else
        # FIXME: as far as I can tell, `from` will always be an Arel::Table.
        # There are no tests that test this branch, but presumably it's
        # possible for `from` to be a list?
        apply_join_dependency(self, construct_join_dependency(from))
      end
404 405
    end

P
Pratik Naik 已提交
406
    def apply_join_dependency(relation, join_dependency)
407
      relation = relation.except(:includes, :eager_load, :preload)
408
      relation = relation.joins join_dependency
409

410 411 412
      if using_limitable_reflections?(join_dependency.reflections)
        relation
      else
413 414
        if relation.limit_value
          limited_ids = limited_ids_for(relation)
415
          limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
416
        end
417
        relation.except(:limit, :offset)
418 419 420
      end
    end

421
    def limited_ids_for(relation)
422 423
      values = @klass.connection.columns_for_distinct(
        "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
424

425
      relation = relation.except(:select).select(values).distinct!
426
      arel = relation.arel
427

S
Sean Griffin 已提交
428
      id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes)
429
      id_rows.map {|row| row[primary_key]}
430 431
    end

432
    def using_limitable_reflections?(reflections)
433
      reflections.none?(&:collection?)
434 435 436 437
    end

    protected

438
    def find_with_ids(*ids)
439 440
      raise UnknownPrimaryKey.new(@klass) if primary_key.nil?

P
Pratik Naik 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453 454
      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
455 456
    rescue RangeError
      raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
P
Pratik Naik 已提交
457 458
    end

459
    def find_one(id)
460 461
      if ActiveRecord::Base === id
        id = id.id
X
Xavier Noria 已提交
462 463 464 465
        ActiveSupport::Deprecation.warn(<<-MSG.squish)
          You are passing an instance of ActiveRecord::Base to `find`.
          Please pass the id of the object by calling `.id`
        MSG
466
      end
A
Aaron Patterson 已提交
467

468
      relation = where(primary_key => id)
469
      record = relation.take
470

471
      raise_record_not_found_exception!(id, 0, 1) unless record
472 473 474 475 476

      record
    end

    def find_some(ids)
477 478
      return find_some_ordered(ids) unless order_values.present?

479
      result = where(primary_key => ids).to_a
480 481

      expected_size =
482 483
        if limit_value && ids.size > limit_value
          limit_value
484 485 486 487 488
        else
          ids.size
        end

      # 11 ids with limit 3, offset 9 should give 2 results.
489 490
      if offset_value && (ids.size - offset_value < expected_size)
        expected_size = ids.size - offset_value
491 492 493 494 495
      end

      if result.size == expected_size
        result
      else
496
        raise_record_not_found_exception!(ids, result.size, expected_size)
497 498 499
      end
    end

500 501 502 503 504 505 506
    def find_some_ordered(ids)
      ids = ids.slice(offset_value || 0, limit_value || ids.size) || []

      result = except(:limit, :offset).where(primary_key => ids).to_a

      if result.size == ids.size
        pk_type = @klass.type_for_attribute(primary_key)
507

508
        records_by_id = result.index_by(&:id)
509
        ids.map { |id| records_by_id.fetch(pk_type.cast(id)) }
510
      else
511
        raise_record_not_found_exception!(ids, result.size, ids.size)
512 513 514
      end
    end

515 516
    def find_take
      if loaded?
517
        @records.first
518
      else
519
        @take ||= limit(1).to_a.first
520 521 522
      end
    end

523
    def find_nth(index, offset = nil)
524 525 526 527 528 529 530 531 532
      # TODO: once the offset argument is removed we rely on offset_index
      # within find_nth_with_limit, rather than pass it in via
      # find_nth_with_limit_and_offset
      if offset
        ActiveSupport::Deprecation.warn(<<-MSG.squish)
          Passing an offset argument to find_nth is deprecated,
          please use Relation#offset instead.
        MSG
      end
P
Pratik Naik 已提交
533
      if loaded?
534
        @records[index]
P
Pratik Naik 已提交
535
      else
536
        offset ||= offset_index
537
        @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first
538 539 540
      end
    end

541
    def find_nth!(index)
542
      find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
543 544
    end

545 546 547
    def find_nth_with_limit(index, limit)
      # TODO: once the offset argument is removed from find_nth,
      # find_nth_with_limit_and_offset can be merged into this method
548
      relation = if order_values.empty? && primary_key
549
                   order(arel_attribute(primary_key).asc)
550 551 552 553
                 else
                   self
                 end

554
      relation = relation.offset(index) unless index.zero?
555
      relation.limit(limit).to_a
P
Pratik Naik 已提交
556 557
    end

558 559 560 561 562 563 564 565 566 567 568 569 570
    def find_last
      if loaded?
        @records.last
      else
        @last ||=
          if limit_value
            to_a.last
          else
            reverse_order.limit(1).to_a.first
          end
      end
    end

571 572 573 574 575 576 577 578 579 580
    private

    def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc:
      if loaded?
        @records[index, limit]
      else
        index += offset
        find_nth_with_limit(index, limit)
      end
    end
581 582
  end
end