require 'active_support/core_ext/object/blank' 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.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 :order # 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 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.find(1, :lock => true) # person.visits += 1 # person.save! # end def find(*args, &block) return to_a.find(&block) 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) args.any? ? apply_finder_options(args.first).first : find_first 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) args.any? ? apply_finder_options(args.first).last : find_last 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) 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 end protected 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 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)) 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 = 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) guard_protected_attributes = false if args[0].is_a?(Hash) guard_protected_attributes = true attributes_for_create = args[0].with_indifferent_access conditions = attributes_for_create.slice(*attributes).symbolize_keys else attributes_for_create = conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h} end record = where(conditions).first unless record record = @klass.new { |r| r.send(:attributes=, attributes_for_create, guard_protected_attributes) } yield(record) if block_given? record.save if match.instantiator == :create end record end def find_with_ids(*ids, &block) return to_a.find(&block) 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) record = where(primary_key.eq(id)).first unless record conditions = arel.send(:where_clauses).join(', ') conditions = " [WHERE #{conditions}]" if conditions.present? 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.send(:where_clauses).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.collect(&:collection?).length.zero? end end end