finder_methods.rb 13.5 KB
Newer Older
1 2
module ActiveRecord
  module FinderMethods
3 4 5
    # 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. If the primary key
    # is an integer, find by id coerces its arguments using +to_i+.
P
Pratik Naik 已提交
6 7
    #
    #   Person.find(1)       # returns the object for ID = 1
8
    #   Person.find("1")     # returns the object for ID = 1
P
Pratik Naik 已提交
9 10 11
    #   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 已提交
12
    #   Person.where("administrator = 1").order("created_on DESC").find(1)
P
Pratik Naik 已提交
13
    #
14 15 16
    # NOTE: An RecordNotFound will be raised if one or more ids are not returned.
    #
    # NOTE: that returned records may not be in the same order as the ids you
17
    # provide since database rows are unordered. Give an explicit <tt>order</tt>
P
Pratik Naik 已提交
18 19
    # to ensure the results are sorted.
    #
20
    # ==== Find with lock
P
Pratik Naik 已提交
21 22 23
    #
    # 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
24
    # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
P
Pratik Naik 已提交
25 26 27 28
    # transaction has to wait until the first is finished; we get the
    # expected <tt>person.visits == 4</tt>.
    #
    #   Person.transaction do
E
Emilio Tagua 已提交
29
    #     person = Person.lock(true).find(1)
P
Pratik Naik 已提交
30 31 32
    #     person.visits += 1
    #     person.save!
    #   end
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
    #
    # ==== Variations of +find+
    # 
    #   Person.where(name: 'Spartacus', rating: 4)
    #   # returns a chainable list (which can be empty)
    #
    #   Person.find_by(name: 'Spartacus', rating: 4)
    #   # returns the first item or nil
    #
    #   Person.where(name: 'Spartacus', rating: 4).first_or_initialize
    #   # returns the first item or returns a new instance (requires you call .save to persist against the database)
    #
    #   Person.where(name: 'Spartacus', rating: 4).first_or_create
    #   # returns the first item or creates it and returns it, available since rails 3.2.1
    #
    #   
    # ==== Alternatives for +find+
    #
    #   Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
    #   # returns true or false
    #   
    #   Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
    #   # returns a chainable list of instances with only the mentioned fields
    #
    #   Person.where(name: 'Spartacus', rating: 4).ids
    #   # returns an Array of ids, available since rails 3.2.1
    #
    #   Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
    #   # returns an Array of the required fields, available since rails 3.1
    #
    #   Person.arel_table
    #   # returns an instance of <tt>Arel::Table</tt>, which allows a comprehensive variety of filters
65
    def find(*args)
66 67
      if block_given?
        to_a.find { |*block_args| yield(*block_args) }
68
      else
69
        find_with_ids(*args)
70 71 72
      end
    end

73
    # Finds the first record matching the specified conditions. There
74
    # is no implied ordering so if order matters, you should specify it
75 76 77 78 79 80 81
    # 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
    def find_by(*args)
82
      where(*args).take
83 84 85 86 87
    end

    # Like <tt>find_by</tt>, except that if no record is found, raises
    # an <tt>ActiveRecord::RecordNotFound</tt> error.
    def find_by!(*args)
88
      where(*args).take!
89 90
    end

91 92 93 94
    # 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.
    #
95
    #   Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
96 97 98 99 100 101 102 103 104 105 106 107
    #   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

    # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
    # is found. Note that <tt>take!</tt> accepts no arguments.
    def take!
      take or raise RecordNotFound
    end

108 109 110
    # Find the first record (or first N records if a parameter is supplied).
    # If no order is defined it will order by primary key.
    #
111 112
    #   Person.first # returns the first object fetched by SELECT * FROM people
    #   Person.where(["user_name = ?", user_name]).first
A
AvnerCohen 已提交
113
    #   Person.where(["user_name = :u", { u: user_name }]).first
114
    #   Person.order("created_on DESC").offset(5).first
115
    #   Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
116 117 118 119 120 121 122 123 124 125 126 127 128
    #
    # ==== Rails 3
    #
    #   Person.first # SELECT "users".* FROM "users" LIMIT 1
    #
    # NOTE: Rails 3 may not +order+ this query by be the primary key.
    # The order will depend on the database implementation.
    # In order to ensure that behavior use <tt>User.order(:id).first</tt> instead.
    #
    # ==== Rails 4
    #
    #   Person.first # SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
    #
129
    def first(limit = nil)
130 131
      if limit
        if order_values.empty? && primary_key
132
          order(arel_table[primary_key].asc).limit(limit).to_a
133 134 135 136 137 138
        else
          limit(limit).to_a
        end
      else
        find_first
      end
139 140
    end

141 142
    # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
    # is found. Note that <tt>first!</tt> accepts no arguments.
P
Pratik Naik 已提交
143
    def first!
144
      first or raise RecordNotFound
145 146
    end

147 148 149
    # Find the last record (or last N records if a parameter is supplied).
    # If no order is defined it will order by primary key.
    #
150 151 152
    #   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
153
    #   Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
154
    #
155
    # Take note that in that last case, the results are sorted in ascending order:
156
    #
157
    #   [#<Person id:2>, #<Person id:3>, #<Person id:4>]
158
    #
159
    # and not:
160
    #
161
    #   [#<Person id:4>, #<Person id:3>, #<Person id:2>]
162 163
    def last(limit = nil)
      if limit
164
        if order_values.empty? && primary_key
165
          order(arel_table[primary_key].desc).limit(limit).reverse
166
        else
167
          to_a.last(limit)
168 169 170 171
        end
      else
        find_last
      end
172 173
    end

174 175
    # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
    # is found. Note that <tt>last!</tt> accepts no arguments.
P
Pratik Naik 已提交
176
    def last!
177
      last or raise RecordNotFound
178 179
    end

S
Steve Klabnik 已提交
180 181
    # Returns truthy if a record exists in the table that matches the +id+ or
    # conditions given, or falsy otherwise. The argument can take six forms:
P
Pratik Naik 已提交
182 183 184 185 186 187 188
    #
    # * 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
189 190 191
    #   (such as <tt>{color: 'red'}</tt>).
    # * +false+ - Returns always +false+.
    # * No args - Returns +false+ if the table is empty, +true+ otherwise.
P
Pratik Naik 已提交
192 193 194 195 196 197 198 199 200 201 202
    #
    # 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>.
    #
    #   Person.exists?(5)
    #   Person.exists?('5')
    #   Person.exists?(['name LIKE ?', "%#{query}%"])
203 204
    #   Person.exists?(name: 'David')
    #   Person.exists?(false)
P
Pratik Naik 已提交
205
    #   Person.exists?
E
Egor Lynko 已提交
206
    def exists?(conditions = :none)
J
Jon Leighton 已提交
207
      conditions = conditions.id if Base === conditions
E
Egor Lynko 已提交
208
      return false if !conditions
209

210
      join_dependency = construct_join_dependency
211
      relation = construct_relation_for_association_find(join_dependency)
212
      relation = relation.except(:select, :order).select("1 AS one").limit(1)
A
Aaron Patterson 已提交
213

E
Egor Lynko 已提交
214
      case conditions
P
Pratik Naik 已提交
215
      when Array, Hash
E
Egor Lynko 已提交
216
        relation = relation.where(conditions)
P
Pratik Naik 已提交
217
      else
E
Egor Lynko 已提交
218
        relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
P
Pratik Naik 已提交
219
      end
220

221
      connection.select_value(relation, "#{name} Exists", relation.bind_values)
222 223
    rescue ThrowResult
      false
224 225
    end

226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
    # This method is called whenever no records are found with either a single
    # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
    #
    # 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:
      conditions = arel.where_sql
      conditions = " [#{conditions}]" if conditions

      if Array(ids).size == 1
        error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}"
      else
        error = "Couldn't find all #{@klass.name.pluralize} with IDs "
        error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
      end

      raise RecordNotFound, error
    end

248 249
    protected

250
    def find_with_associations
251
      join_dependency = construct_join_dependency
252
      relation = construct_relation_for_association_find(join_dependency)
253
      rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
254 255 256 257 258
      join_dependency.instantiate(rows)
    rescue ThrowResult
      []
    end

259
    def construct_join_dependency(joins = [])
260
      including = (eager_load_values + includes_values).uniq
261
      ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
262 263
    end

264
    def construct_relation_for_association_calculations
265
      apply_join_dependency(self, construct_join_dependency(arel.froms.first))
266 267
    end

268
    def construct_relation_for_association_find(join_dependency)
269
      relation = except(:select).select(join_dependency.columns)
P
Pratik Naik 已提交
270 271
      apply_join_dependency(relation, join_dependency)
    end
272

P
Pratik Naik 已提交
273
    def apply_join_dependency(relation, join_dependency)
274
      relation = relation.except(:includes, :eager_load, :preload)
275
      relation = join_dependency.join_relation(relation)
276

277 278 279
      if using_limitable_reflections?(join_dependency.reflections)
        relation
      else
280
        relation.where!(construct_limited_ids_condition(relation)) if relation.limit_value
281
        relation.except(:limit, :offset)
282 283 284 285
      end
    end

    def construct_limited_ids_condition(relation)
286 287
      values = @klass.connection.columns_for_distinct(
        "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
288

289
      relation = relation.except(:select).select(values).distinct!
290 291 292

      id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
      ids_array = id_rows.map {|row| row[primary_key]}
293

294
      ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
295 296
    end

297
    def find_with_ids(*ids)
P
Pratik Naik 已提交
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
      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

314
    def find_one(id)
A
Aaron Patterson 已提交
315 316
      id = id.id if ActiveRecord::Base === id

317
      column = columns_hash[primary_key]
318
      substitute = connection.substitute_at(column, bind_values.length)
319
      relation = where(table[primary_key].eq(substitute))
320
      relation.bind_values += [[column, id]]
321
      record = relation.take
322

323
      raise_record_not_found_exception!(id, 0, 1) unless record
324 325 326 327 328

      record
    end

    def find_some(ids)
J
Jon Leighton 已提交
329
      result = where(table[primary_key].in(ids)).to_a
330 331

      expected_size =
332 333
        if limit_value && ids.size > limit_value
          limit_value
334 335 336 337 338
        else
          ids.size
        end

      # 11 ids with limit 3, offset 9 should give 2 results.
339 340
      if offset_value && (ids.size - offset_value < expected_size)
        expected_size = ids.size - offset_value
341 342 343 344 345
      end

      if result.size == expected_size
        result
      else
346
        raise_record_not_found_exception!(ids, result.size, expected_size)
347 348 349
      end
    end

350 351
    def find_take
      if loaded?
352
        @records.first
353
      else
354
        @take ||= limit(1).to_a.first
355 356 357
      end
    end

P
Pratik Naik 已提交
358 359 360 361
    def find_first
      if loaded?
        @records.first
      else
362
        @first ||=
363
          if with_default_scope.order_values.empty? && primary_key
364
            order(arel_table[primary_key].asc).limit(1).to_a.first
365
          else
366
            limit(1).to_a.first
367
          end
P
Pratik Naik 已提交
368 369 370 371 372 373 374
      end
    end

    def find_last
      if loaded?
        @records.last
      else
N
Nick Howard 已提交
375 376 377 378
        @last ||=
          if offset_value || limit_value
            to_a.last
          else
379
            reverse_order.limit(1).to_a.first
N
Nick Howard 已提交
380
          end
P
Pratik Naik 已提交
381 382 383
      end
    end

384
    def using_limitable_reflections?(reflections)
385
      reflections.none? { |r| r.collection? }
386
    end
387 388
  end
end