diff options
Diffstat (limited to 'activerecord/lib/active_record/relation/query_methods.rb')
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 587 |
1 files changed, 449 insertions, 138 deletions
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b6d762c2e2..f6bacf4822 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,66 +1,134 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/blank' module ActiveRecord module QueryMethods extend ActiveSupport::Concern - attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, - :where_values, :having_values, :bind_values, - :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reordering_value, :reverse_order_value, - :uniq_value, :references_values + Relation::MULTI_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = values # @values[:select] = values + end # end + CODE + end + + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value # def readonly_value + @values[:#{name}] # @values[:readonly] + end # end + CODE + end + + Relation::SINGLE_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value=(value) # def readonly_value=(value) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = value # @values[:readonly] = value + end # end + CODE + end + def create_with_value # :nodoc: + @values[:create_with] || {} + end + + alias extensions extending_values + + # Specify relationships to be included in the result set. For + # example: + # + # users = User.includes(:address) + # users.each do |user| + # user.address.city + # end + # + # allows you to access the +address+ attribute of the +User+ model without + # firing an additional query. This will often result in a + # performance improvement over a simple +join+. + # + # === conditions + # + # If you want to add conditions to your included models you'll have + # to explicitly reference them. For example: + # + # User.includes(:posts).where('posts.name = ?', 'example') + # + # Will throw an error, but this will work: + # + # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - args.reject! {|a| a.blank? } + args.empty? ? self : spawn.includes!(*args) + end - return self if args.empty? + # Like #includes, but modifies the relation in place. + def includes!(*args) + args.reject! {|a| a.blank? } - relation = clone - relation.includes_values = (relation.includes_values + args).flatten.uniq - relation + self.includes_values = (includes_values + args).flatten.uniq + self end + # Forces eager loading by performing a LEFT OUTER JOIN on +args+: + # + # User.eager_load(:posts) + # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... + # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = + # "users"."id" def eager_load(*args) - return self if args.blank? + args.blank? ? self : spawn.eager_load!(*args) + end - relation = clone - relation.eager_load_values += args - relation + # Like #eager_load, but modifies relation in place. + def eager_load!(*args) + self.eager_load_values += args + self end + # Allows preloading of +args+, in the same way that +includes+ does: + # + # User.preload(:posts) + # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - return self if args.blank? + args.blank? ? self : spawn.preload!(*args) + end - relation = clone - relation.preload_values += args - relation + # Like #preload, but modifies relation in place. + def preload!(*args) + self.preload_values += args + self end # Used to indicate that an association is referenced by an SQL string, and should # therefore be JOINed in any query rather than loaded separately. # - # For example: - # # User.includes(:posts).where("posts.name = 'foo'") # # => Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - return self if args.blank? + args.blank? ? self : spawn.references!(*args) + end + + # Like #references, but modifies relation in place. + def references!(*args) + args.flatten! - relation = clone - relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq - relation + self.references_values = (references_values + args.map!(&:to_s)).uniq + self end # Works in two unique ways. # # First: takes a block so it can be used just like Array#select. # - # Model.scoped.select { |m| m.field == value } + # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, # converting them into an array and iterating through them using Array#select. @@ -68,8 +136,8 @@ module ActiveRecord # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: # - # >> Model.select(:field) - # => [#<Model field:value>] + # Model.select(:field) + # # => [#<Model field:value>] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query @@ -77,44 +145,77 @@ module ActiveRecord # # The argument to the method can also be an array of fields. # - # >> Model.select([:field, :other_field, :and_one_more]) - # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # Model.select(:field, :other_field, :and_one_more) + # # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # # Accessing attributes of an object that do not have fields retrieved by a select # will throw <tt>ActiveModel::MissingAttributeError</tt>: # - # >> Model.select(:field).first.other_field - # => ActiveModel::MissingAttributeError: missing attribute: other_field - def select(value = Proc.new) + # Model.select(:field).first.other_field + # # => ActiveModel::MissingAttributeError: missing attribute: other_field + def select(*fields) if block_given? - to_a.select {|*block_args| value.call(*block_args) } + to_a.select { |*block_args| yield(*block_args) } else - relation = clone - relation.select_values += Array.wrap(value) - relation + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn.select!(*fields) end end + # Like #select, but modifies relation in place. + def select!(*fields) + self.select_values += fields.flatten + self + end + + # Allows to specify a group attribute: + # + # User.group(:name) + # => SELECT "users".* FROM "users" GROUP BY name + # + # Returns an array with distinct records based on the +group+ attribute: + # + # User.select([:id, :name]) + # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo"> + # + # User.group(:name) + # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] def group(*args) - return self if args.blank? + args.blank? ? self : spawn.group!(*args) + end + + # Like #group, but modifies relation in place. + def group!(*args) + args.flatten! - relation = clone - relation.group_values += args.flatten - relation + self.group_values += args + self end + # Allows to specify an order attribute: + # + # User.order('name') + # => SELECT "users".* FROM "users" ORDER BY name + # + # User.order('name DESC') + # => SELECT "users".* FROM "users" ORDER BY name DESC + # + # User.order('name DESC, email') + # => SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) - return self if args.blank? + args.blank? ? self : spawn.order!(*args) + end + + # Like #order, but modifies relation in place. + def order!(*args) + args.flatten! - args = args.flatten references = args.reject { |arg| Arel::Node === arg } - .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } - .compact + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? - relation = clone - relation = relation.references(references) if references.any? - relation.order_values += args - relation + self.order_values = args + self.order_values + self end # Replaces any existing order defined on the relation with the specified order. @@ -125,89 +226,226 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY id ASC, name ASC'. - # + # generates a query with 'ORDER BY name ASC, id ASC'. def reorder(*args) - return self if args.blank? + args.blank? ? self : spawn.reorder!(*args) + end - relation = clone - relation.reordering_value = true - relation.order_values = args.flatten - relation + # Like #reorder, but modifies relation in place. + def reorder!(*args) + args.flatten! + + self.reordering_value = true + self.order_values = args + self end + # Performs a joins on +args+: + # + # User.joins(:posts) + # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" def joins(*args) - return self if args.compact.blank? - - relation = clone + args.compact.blank? ? self : spawn.joins!(*args) + end + # Like #joins, but modifies relation in place. + def joins!(*args) args.flatten! - relation.joins_values += args - relation + self.joins_values += args + self end def bind(value) - relation = clone - relation.bind_values += [value] - relation + spawn.bind!(value) + end + + def bind!(value) + self.bind_values += [value] + self end + # Returns a new relation, which is the result of filtering the current relation + # according to the conditions in the arguments. + # + # #where accepts conditions in one of several formats. In the examples below, the resulting + # SQL is given as an illustration; the actual query generated may be different depending + # on the database adapter. + # + # === string + # + # A single string, without additional arguments, is passed to the query + # constructor as a SQL fragment, and used in the where clause of the query. + # + # Client.where("orders_count = '2'") + # # SELECT * from clients where orders_count = '2'; + # + # Note that building your own string from user input may expose your application + # to injection attacks if not done properly. As an alternative, it is recommended + # to use one of the following methods. + # + # === array + # + # If an array is passed, then the first element of the array is treated as a template, and + # the remaining elements are inserted into the template to generate the condition. + # Active Record takes care of building the query to avoid injection attacks, and will + # convert from the ruby type to the database type where needed. Elements are inserted + # into the string in the order in which they appear. + # + # User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # Alternatively, you can use named placeholders in the template, and pass a hash as the + # second element of the array. The names in the template are replaced with the corresponding + # values from the hash. + # + # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # This can make for more readable code in complex queries. + # + # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently + # than the previous methods; you are responsible for ensuring that the values in the template + # are properly quoted. The values are passed to the connector for quoting, but the caller + # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, + # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # + # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # If #where is called with multiple arguments, these are treated as if they were passed as + # the elements of a single array. + # + # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # When using strings to specify conditions, you can use any operator available from + # the database. While this provides the most flexibility, you can also unintentionally introduce + # dependencies on the underlying database. If your code is intended for general consumption, + # test with multiple database backends. + # + # === hash + # + # #where will also accept a hash condition, in which the keys are fields and the values + # are values to be searched for. + # + # Fields can be symbols or strings. Values can be single values, arrays, or ranges. + # + # User.where({ name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com' + # + # User.where({ name: ["Alice", "Bob"]}) + # # SELECT * FROM users WHERE name IN ('Alice', 'Bob') + # + # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) + # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') + # + # === Joins + # + # If the relation is the result of a join, you may create a condition which uses any of the + # tables in the join. For string and array conditions, use the table name in the condition. + # + # User.joins(:posts).where("posts.created_at < ?", Time.now) + # + # For hash conditions, you can either use the table name in the key, or use a sub-hash. + # + # User.joins(:posts).where({ "posts.published" => true }) + # User.joins(:posts).where({ :posts => { :published => true } }) + # + # === empty condition + # + # If the condition returns true for blank?, then where is a no-op and returns the current relation. def where(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.where!(opts, *rest) + end - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.where_values += build_where(opts, rest) - relation + # #where! is identical to #where, except that instead of returning a new relation, it adds + # the condition to the existing relation. + def where!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts + + self.where_values += build_where(opts, rest) + self end + # Allows to specify a HAVING clause. Note that you can't use HAVING + # without also specifying a GROUP clause. + # + # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.having!(opts, *rest) + end + + # Like #having, but modifies relation in place. + def having!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.having_values += build_where(opts, rest) - relation + self.having_values += build_where(opts, rest) + self end + # Specifies a limit for the number of records to retrieve. + # + # User.limit(10) # generated SQL has 'LIMIT 10' + # + # User.limit(10).limit(20) # generated SQL has 'LIMIT 20' def limit(value) - relation = clone - relation.limit_value = value - relation + spawn.limit!(value) + end + + # Like #limit, but modifies relation in place. + def limit!(value) + self.limit_value = value + self end + # Specifies the number of rows to skip before returning rows. + # + # User.offset(10) # generated SQL has "OFFSET 10" + # + # Should be used with order. + # + # User.offset(10).order("name ASC") def offset(value) - relation = clone - relation.offset_value = value - relation + spawn.offset!(value) + end + + # Like #offset, but modifies relation in place. + def offset!(value) + self.offset_value = value + self end + # Specifies locking settings (default to +true+). For more information + # on locking, please see +ActiveRecord::Locking+. def lock(locks = true) - relation = clone + spawn.lock!(locks) + end + # Like #lock, but modifies relation in place. + def lock!(locks = true) case locks when String, TrueClass, NilClass - relation.lock_value = locks || true + self.lock_value = locks || true else - relation.lock_value = false + self.lock_value = false end - relation + self end # Returns a chainable relation with zero records, specifically an - # instance of the NullRelation class. + # instance of the <tt>ActiveRecord::NullRelation</tt> class. # - # The returned NullRelation inherits from Relation and implements the - # Null Object pattern so it is an object with defined null behavior: - # it always returns an empty array of records and does not query the database. + # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the + # Null Object pattern. It is an object with defined null behavior and always returns an empty + # array of records without quering the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. # - # This is useful in scenarios where you need a chainable response to a method - # or a scope that could return zero results. + # Used in cases where a method or scope could return zero records but the + # result needs to be chainable. # # For example: # @@ -226,25 +464,73 @@ module ActiveRecord # end # def none - NullRelation.new(@klass, @table) + extending(NullRelation) end + # Sets readonly attributes for the returned relation. If value is + # true (default), attempting to update a record will result in an error. + # + # users = User.readonly + # users.first.save + # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord def readonly(value = true) - relation = clone - relation.readonly_value = value - relation + spawn.readonly!(value) end + # Like #readonly, but modifies relation in place. + def readonly!(value = true) + self.readonly_value = value + self + end + + # Sets attributes to be used when creating new records from a + # relation object. + # + # users = User.where(name: 'Oscar') + # users.new.name # => 'Oscar' + # + # users = users.create_with(name: 'DHH') + # users.new.name # => 'DHH' + # + # You can pass +nil+ to +create_with+ to reset attributes: + # + # users = users.create_with(nil) + # users.new.name # => 'Oscar' def create_with(value) - relation = clone - relation.create_with_value = value ? create_with_value.merge(value) : {} - relation + spawn.create_with!(value) + end + + # Like #create_with but modifies the relation in place. Raises + # +ImmutableRelation+ if the relation has already been loaded. + # + # users = User.all.create_with!(name: 'Oscar') + # users.new.name # => 'Oscar' + def create_with!(value) + self.create_with_value = value ? create_with_value.merge(value) : {} + self end - def from(value) - relation = clone - relation.from_value = value - relation + # Specifies table from which the records will be fetched. For example: + # + # Topic.select('title').from('posts') + # #=> SELECT title FROM posts + # + # Can accept other relation objects. For example: + # + # Topic.select('title').from(Topic.approved) + # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # + # Topic.select('a.title').from(Topic.approved, :a) + # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # + def from(value, subquery_name = nil) + spawn.from!(value, subquery_name) + end + + # Like #from, but modifies relation in place. + def from!(value, subquery_name = nil) + self.from_value = [value, subquery_name] + self end # Specifies whether the records should be unique or not. For example: @@ -258,9 +544,13 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - relation = clone - relation.uniq_value = value - relation + spawn.uniq!(value) + end + + # Like #uniq, but modifies relation in place. + def uniq!(value = true) + self.uniq_value = value + self end # Used to extend a scope with additional methods, either through @@ -276,16 +566,16 @@ module ActiveRecord # end # end # - # scope = Model.scoped.extending(Pagination) + # scope = Model.all.extending(Pagination) # scope.page(params[:page]) # # You can also pass a list of modules: # - # scope = Model.scoped.extending(Pagination, SomethingElse) + # scope = Model.all.extending(Pagination, SomethingElse) # # === Using a block # - # scope = Model.scoped.extending do + # scope = Model.all.extending do # def page(number) # # pagination code goes here # end @@ -294,54 +584,71 @@ module ActiveRecord # # You can also use a block and a module list: # - # scope = Model.scoped.extending(Pagination) do + # scope = Model.all.extending(Pagination) do # def per_page(number) # # pagination code goes here # end # end - def extending(*modules) - modules << Module.new(&Proc.new) if block_given? + def extending(*modules, &block) + if modules.any? || block + spawn.extending!(*modules, &block) + else + self + end + end - return self if modules.empty? + # Like #extending, but modifies relation in place. + def extending!(*modules, &block) + modules << Module.new(&block) if block_given? - relation = clone - relation.send(:apply_modules, modules.flatten) - relation + self.extending_values += modules.flatten + extend(*extending_values) if extending_values.any? + + self end + # Reverse the existing order clause on the relation. + # + # User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC' def reverse_order - relation = clone - relation.reverse_order_value = !relation.reverse_order_value - relation + spawn.reverse_order! + end + + # Like #reverse_order, but modifies relation in place. + def reverse_order! + self.reverse_order_value = !reverse_order_value + self end + # Returns the Arel object associated with the relation. def arel @arel ||= with_default_scope.build_arel end + # Like #arel, but ignores the default scope of the model. def build_arel - arel = table.from table + arel = Arel::SelectManager.new(table.engine, table) - build_joins(arel, @joins_values) unless @joins_values.empty? + build_joins(arel, joins_values) unless joins_values.empty? - collapse_wheres(arel, (@where_values - ['']).uniq) + collapse_wheres(arel, (where_values - ['']).uniq) - arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? + arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty? - arel.take(connection.sanitize_limit(@limit_value)) if @limit_value - arel.skip(@offset_value) if @offset_value + arel.take(connection.sanitize_limit(limit_value)) if limit_value + arel.skip(offset_value.to_i) if offset_value - arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? + arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? - order = @order_values - order = reverse_sql_order(order) if @reverse_order_value + order = order_values + order = reverse_sql_order(order) if reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? - build_select(arel, @select_values.uniq) + build_select(arel, select_values.uniq) - arel.distinct(@uniq_value) - arel.from(@from_value) if @from_value - arel.lock(@lock_value) if @lock_value + arel.distinct(uniq_value) + arel.from(build_from) if from_value + arel.lock(lock_value) if lock_value arel end @@ -389,6 +696,17 @@ module ActiveRecord end end + def build_from + opts, name = from_value + case opts + when Relation + name ||= 'subquery' + opts.arel.as(name.to_s) + else + opts + end + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -443,13 +761,6 @@ module ActiveRecord end end - def apply_modules(modules) - unless modules.empty? - @extensions += modules - modules.each {|extension| extend(extension) } - end - end - def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? |