diff options
Diffstat (limited to 'activerecord/lib/active_record/relation')
7 files changed, 167 insertions, 67 deletions
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index fb4388d4b2..4d14506965 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord module Batches @@ -9,8 +8,8 @@ module ActiveRecord # In that case, batch processing methods allow you to work # with the records in batches, thereby greatly reducing memory consumption. # - # The <tt>find_each</tt> method uses <tt>find_in_batches</tt> with a batch size of 1000 (or as - # specified by the <tt>:batch_size</tt> option). + # The #find_each method uses #find_in_batches with a batch size of 1000 (or as + # specified by the +:batch_size+ option). # # Person.all.find_each do |person| # person.do_awesome_stuff @@ -20,7 +19,7 @@ module ActiveRecord # person.party_all_night! # end # - # You can also pass the <tt>:start</tt> option to specify + # You can also pass the +:start+ option to specify # an offset to control the starting point. def find_each(options = {}) find_in_batches(options) do |records| @@ -29,14 +28,14 @@ module ActiveRecord end # Yields each batch of records that was found by the find +options+ as - # an array. The size of each batch is set by the <tt>:batch_size</tt> + # an array. The size of each batch is set by the +:batch_size+ # option; the default is 1000. # # You can control the starting point for the batch processing by - # supplying the <tt>:start</tt> option. This is especially useful if you + # supplying the +:start+ option. This is especially useful if you # want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and - # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt> + # worker 2 handle from 10,000 and beyond (by setting the +:start+ # option on that worker). # # It's not possible to set the order. That is automatically set to @@ -67,7 +66,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.reorder(batch_order).limit(batch_size) - records = relation.where(table[primary_key].gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).to_a while records.any? records_size = records.size diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index e40b958b54..d93e7c8997 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' module ActiveRecord diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 64dda4f35a..ab8b36c8ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,7 +1,6 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord - module Delegation + module Delegation # :nodoc: # Set up common delegations for performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 974cd326ef..84aaa39fed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord @@ -133,19 +132,6 @@ module ActiveRecord last or raise RecordNotFound end - # Runs the query on the database and returns records with the used query - # methods. - # - # 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 - def all - 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 six forms: # @@ -285,7 +271,7 @@ module ActiveRecord end def find_some(ids) - result = where(table[primary_key].in(ids)).all + result = where(table[primary_key].in(ids)).to_a expected_size = if limit_value && ids.size > limit_value @@ -324,7 +310,7 @@ module ActiveRecord @records.first else @first ||= - if order_values.empty? && primary_key + if with_default_scope.order_values.empty? && primary_key order(arel_table[primary_key].asc).limit(1).to_a.first else limit(1).to_a.first diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 36f98c6480..e5b50673da 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,9 +1,8 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActiveRecord class Relation - class HashMerger + class HashMerger # :nodoc: attr_reader :relation, :hash def initialize(relation, hash) @@ -28,7 +27,7 @@ module ActiveRecord end end - class Merger + class Merger # :nodoc: attr_reader :relation, :values def initialize(relation, other) @@ -98,15 +97,13 @@ module ActiveRecord merged_wheres = relation.where_values + values[:where] unless relation.where_values.empty? - # Remove duplicates, last one wins. - seen = Hash.new { |h,table| h[table] = {} } + # Remove equalities with duplicated left-hand. Last one wins. + seen = {} merged_wheres = merged_wheres.reverse.reject { |w| nuke = false if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - table = w.left.relation.name - nuke = seen[table][name] - seen[table][name] = true + nuke = seen[w.left] + seen[w.left] = true end nuke }.reverse diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 6f49548aab..f6bacf4822 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/blank' module ActiveRecord module QueryMethods @@ -35,16 +34,39 @@ module ActiveRecord CODE end - def create_with_value + 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.empty? ? self : spawn.includes!(*args) end + # Like #includes, but modifies the relation in place. def includes!(*args) args.reject! {|a| a.blank? } @@ -52,19 +74,31 @@ module ActiveRecord 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) args.blank? ? self : spawn.eager_load!(*args) end + # 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) args.blank? ? self : spawn.preload!(*args) end + # Like #preload, but modifies relation in place. def preload!(*args) self.preload_values += args self @@ -82,6 +116,7 @@ module ActiveRecord args.blank? ? self : spawn.references!(*args) end + # Like #references, but modifies relation in place. def references!(*args) args.flatten! @@ -93,7 +128,7 @@ module ActiveRecord # # 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. @@ -101,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 @@ -110,31 +145,46 @@ 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 - spawn.select!(value) + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn.select!(*fields) end end - def select!(value) - self.select_values += Array.wrap(value) + # 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) args.blank? ? self : spawn.group!(*args) end + # Like #group, but modifies relation in place. def group!(*args) args.flatten! @@ -142,10 +192,21 @@ module ActiveRecord 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) args.blank? ? self : spawn.order!(*args) end + # Like #order, but modifies relation in place. def order!(*args) args.flatten! @@ -153,7 +214,7 @@ module ActiveRecord references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? - self.order_values += args + self.order_values = args + self.order_values self end @@ -165,11 +226,12 @@ 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) args.blank? ? self : spawn.reorder!(*args) end + # Like #reorder, but modifies relation in place. def reorder!(*args) args.flatten! @@ -178,10 +240,15 @@ module ActiveRecord 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) args.compact.blank? ? self : spawn.joins!(*args) end + # Like #joins, but modifies relation in place. def joins!(*args) args.flatten! @@ -301,10 +368,15 @@ module ActiveRecord 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) 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 @@ -321,6 +393,7 @@ module ActiveRecord spawn.limit!(value) end + # Like #limit, but modifies relation in place. def limit!(value) self.limit_value = value self @@ -337,15 +410,19 @@ module ActiveRecord 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) spawn.lock!(locks) end + # Like #lock, but modifies relation in place. def lock!(locks = true) case locks when String, TrueClass, NilClass @@ -358,11 +435,11 @@ module ActiveRecord 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. @@ -387,22 +464,47 @@ module ActiveRecord # end # def none - scoped.extending(NullRelation) + 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) 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) 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 @@ -425,6 +527,7 @@ module ActiveRecord 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 @@ -444,6 +547,7 @@ module ActiveRecord spawn.uniq!(value) end + # Like #uniq, but modifies relation in place. def uniq!(value = true) self.uniq_value = value self @@ -462,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 @@ -480,7 +584,7 @@ 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 @@ -493,10 +597,11 @@ module ActiveRecord end end + # Like #extending, but modifies relation in place. def extending!(*modules, &block) modules << Module.new(&block) if block_given? - self.extending_values = modules.flatten + self.extending_values += modules.flatten extend(*extending_values) if extending_values.any? self @@ -509,17 +614,20 @@ module ActiveRecord 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? @@ -581,7 +689,8 @@ module ActiveRecord when String, Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash - PredicateBuilder.build_from_hash(table.engine, opts, table) + attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + PredicateBuilder.build_from_hash(table.engine, attributes, table) else [opts] end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 80d087a9ea..5394c1b28b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' require 'active_record/relation/merger' @@ -24,6 +23,13 @@ module ActiveRecord # # Returns the intersection of all published posts with the 5 most recently created posts. # # (This is just an example. You'd probably want to do this with a single query!) # + # Procs will be evaluated by merge: + # + # Post.where(published: true).merge(-> { joins(:comments) }) + # # => Post.where(published: true).joins(:comments) + # + # This is mainly intended for sharing common conditions between multiple associations. + # def merge(other) if other.is_a?(Array) to_a & other @@ -34,9 +40,14 @@ module ActiveRecord end end + # Like #merge, but applies changes in place. def merge!(other) - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + if !other.is_a?(Relation) && other.respond_to?(:to_proc) + instance_exec(&other) + else + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge + end end # Removes from the query the condition(s) specified in +skips+. |