require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord module FinderMethods # 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 # Model.find(:first, *args) or its shortcut Model.first(*args). # * 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 # Model.find(:last, *args) or its shortcut Model.last(*args). # * Find all - This will return all the records matched by the options used. # If no records are found, an empty array is returned. Use # Model.find(:all, *args) or its shortcut Model.all(*args). # # All approaches accept an options hash as their last parameter. # # ==== Parameters # # * :conditions - An SQL fragment like "administrator = 1", ["user_name = ?", username], # or ["user_name = :user_name", { :user_name => user_name }]. See conditions in the intro. # * :order - An SQL fragment like "created_at DESC, name". # * :group - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. # * :having - Combined with +:group+ this can be used to filter the records that a # GROUP BY returns. Uses the HAVING SQL-clause. # * :limit - An integer determining the limit on the number of rows that should be returned. # * :offset - An integer determining the offset from where the rows should be fetched. So at 5, # it would skip rows 0 through 4. # * :joins - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), # named associations in the same form used for the :include option, which will perform an # INNER JOIN on the associated table(s), # or an array containing a mixture of both strings and named associations. # 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. # Pass :readonly => false to override. # * :include - Names associations that should be loaded alongside. The symbols named refer # to already defined associations. See eager loading under Associations. # * :select - 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"). # * :from - 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). # * :readonly - Mark the returned records read-only so they cannot be saved or updated. # * :lock - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". # :lock => true 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.where("administrator = 1").order("created_on DESC").find(1) # # Note that returned records may not be in the same order as the ids you # provide since database rows are unordered. Give an explicit :order # to ensure the results are sorted. # # ==== Examples # # # find first # Person.first # returns the first object fetched by SELECT * FROM people # Person.where(["user_name = ?", user_name]).first # Person.where(["user_name = :u", { :u => user_name }]).first # Person.order("created_on DESC").offset(5).first # # # find last # 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 # # # find all # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people # Person.where(["category IN (?)", categories]).limit(50).all # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all # Person.offset(10).limit(10).all # Person.includes([:account, :friends]).all # Person.group("category").all # # Example for find with a lock: Imagine two concurrent transactions: # each will read person.visits == 2, add 1 to it, and save, resulting # in two saves of person.visits = 3. By locking the row, the second # transaction has to wait until the first is finished; we get the # expected person.visits == 4. # # Person.transaction do # person = Person.lock(true).find(1) # person.visits += 1 # person.save! # end def find(*args) return to_a.find { |*block_args| yield(*block_args) } if block_given? options = args.extract_options! if options.present? apply_finder_options(options).find(*args) else case args.first when :first, :last, :all send(args.first) else find_with_ids(*args) end end end # A convenience wrapper for find(:first, *args). You can pass in all the # same arguments to this method as you can to find(:first). def first(*args) 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 end # A convenience wrapper for find(:last, *args). You can pass in all the # same arguments to this method as you can to find(:last). def last(*args) 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 end # A convenience wrapper for find(:all, *args). You can pass in all the # same arguments to this method as you can to find(:all). def all(*args) args.any? ? apply_finder_options(args.first).to_a : to_a end # 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 '5'). # * Array - Finds the record that matches these +find+-style conditions # (such as ['color = ?', 'red']). # * Hash - Finds the record that matches these +find+-style conditions # (such as {:color => 'red'}). # * 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 name = # 'Jamie'), since it would be sanitized and then queried against # the primary key column, like id = 'name = \'Jamie\''. # # ==== Examples # Person.exists?(5) # Person.exists?('5') # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? def exists?(id = nil) id = id.id if ActiveRecord::Base === id join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) relation = relation.except(:select).select("1").limit(1) case id when Array, Hash relation = relation.where(id) else relation = relation.where(table[primary_key.name].eq(id)) if id end connection.select_value(relation.to_sql) ? true : false end protected def find_with_associations join_dependency = construct_join_dependency_for_association_find rows = construct_relation_for_association_find(join_dependency).to_a join_dependency.instantiate(rows) rescue ThrowResult [] end def construct_join_dependency_for_association_find including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil) end def construct_relation_for_association_calculations including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.join_sql) relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) end def construct_relation_for_association_find(join_dependency) relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency)) apply_join_dependency(relation, join_dependency) end def apply_join_dependency(relation, join_dependency) for association in join_dependency.join_associations relation = association.join_relation(relation) end limitable_reflections = using_limitable_reflections?(join_dependency.reflections) 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 def find_by_attributes(match, attributes, *args) conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] 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) protected_attributes_for_create, unprotected_attributes_for_create = {}, {} 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 end conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys record = where(conditions).first unless record 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 yield(record) if block_given? record.save if match.instantiator == :create end record end def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? 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 def find_one(id) id = id.id if ActiveRecord::Base === id record = where(primary_key.eq(id)).first unless record conditions = arel.where_sql conditions = " [#{conditions}]" if conditions raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" end record end def find_some(ids) result = where(primary_key.in(ids)).all expected_size = if @limit_value && ids.size > @limit_value @limit_value else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. if @offset_value && (ids.size - @offset_value < expected_size) expected_size = ids.size - @offset_value end if result.size == expected_size result else conditions = arel.wheres.map { |x| x.value }.join(', ') 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 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 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) reflections.none? { |r| r.collection? } end end end