diff options
Diffstat (limited to 'activerecord/lib/active_record/relation.rb')
-rw-r--r-- | activerecord/lib/active_record/relation.rb | 274 |
1 files changed, 177 insertions, 97 deletions
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 15fd1a58c8..05ced3299b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,36 +1,35 @@ +# -*- coding: utf-8 -*- require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/module/delegation' +require 'active_support/deprecation' module ActiveRecord # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) - ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order] - include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches + MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, + :order, :joins, :where, :having, :bind, :references, + :extending] - # These are explicitly delegated to improve performance (avoids method_missing) - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a - delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, + :reverse_order, :uniq, :create_with] + + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + + include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :extensions, :default_scoped + attr_accessor :default_scoped alias :loaded? :loaded alias :default_scoped? :default_scoped - def initialize(klass, table) - @klass, @table = klass, table - + def initialize(klass, table, values = {}) + @klass = klass + @table = table + @values = values @implicit_readonly = nil @loaded = false @default_scoped = false - - SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} - (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} - @extensions = [] - @create_with_value = {} end def insert(values) @@ -81,6 +80,8 @@ module ActiveRecord end def initialize_copy(other) + @values = @values.dup + @values[:bind] = @values[:bind].dup if @values[:bind] reset end @@ -94,36 +95,93 @@ module ActiveRecord scoping { @klass.create!(*args, &block) } end - def respond_to?(method, include_private = false) - arel.respond_to?(method, include_private) || - Array.method_defined?(method) || - @klass.respond_to?(method, include_private) || - super + # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. + # + # Expects arguments in the same format as <tt>Base.create</tt>. + # + # ==== Examples + # # Find the first user named Penélope or create a new one. + # User.where(:first_name => 'Penélope').first_or_create + # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # + # # Find the first user named Penélope or create a new one. + # # We already have one so the existing record will be returned. + # User.where(:first_name => 'Penélope').first_or_create + # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # + # # Find the first user named Scarlett or create a new one with a particular last name. + # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson') + # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # + # # Find the first user named Scarlett or create a new one with a different last name. + # # We already have one so the existing record will be returned. + # User.where(:first_name => 'Scarlett').first_or_create do |user| + # user.last_name = "O'Hara" + # end + # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + def first_or_create(attributes = nil, options = {}, &block) + first || create(attributes, options, &block) + end + + # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid. + # + # Expects arguments in the same format as <tt>Base.create!</tt>. + def first_or_create!(attributes = nil, options = {}, &block) + first || create!(attributes, options, &block) + end + + # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>. + # + # Expects arguments in the same format as <tt>Base.new</tt>. + def first_or_initialize(attributes = nil, options = {}, &block) + first || new(attributes, options, &block) + end + + # Runs EXPLAIN on the query or queries triggered by this relation and + # returns the result as a string. The string is formatted imitating the + # ones printed by the database shell. + # + # Note that this method actually runs the queries, since the results of some + # are needed by the next ones when eager loading is going on. + # + # Please see further details in the + # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. + def explain + _, queries = collecting_queries_for_explain { exec_queries } + exec_explain(queries) end def to_a + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan do + exec_queries + end + end + + def exec_queries return @records if loaded? default_scoped = with_default_scope if default_scoped.equal?(self) - @records = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - else - IdentityMap.without do - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - end - end + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) - preload = @preload_values - preload += @includes_values unless eager_loading? + preload = preload_values + preload += includes_values unless eager_loading? preload.each do |associations| ActiveRecord::Associations::Preloader.new(@records, associations).run end # @readonly_value is true only if set explicitly. @implicit_readonly is true if there # are JOINS and no explicit SELECT. - readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value + readonly = readonly_value.nil? ? @implicit_readonly : readonly_value @records.each { |record| record.readonly! } if readonly else @records = default_scoped.to_a @@ -132,6 +190,7 @@ module ActiveRecord @loaded = true @records end + private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -162,7 +221,7 @@ module ActiveRecord if block_given? to_a.many? { |*block_args| yield(*block_args) } else - @limit_value ? to_a.many? : size > 1 + limit_value ? to_a.many? : size > 1 end end @@ -177,7 +236,10 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.send(:with_scope, self, :overwrite) { yield } + previous, klass.current_scope = klass.current_scope, self + yield + ensure + klass.current_scope = previous end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -198,40 +260,26 @@ module ActiveRecord # Customer.update_all :wants_email => true # # # Update all books with 'Rails' in their title - # Book.update_all "author = 'David'", "title LIKE '%Rails%'" - # - # # Update all avatars migrated more than a week ago - # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] - # - # # Update all books that match conditions, but limit it to 5 ordered by date - # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 - # - # # Conditions from the current relation also works # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David') # - # # The same idea applies to limit and order + # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') - def update_all(updates, conditions = nil, options = {}) - IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled? - if conditions || options.present? - where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) - else - stmt = Arel::UpdateManager.new(arel.engine) - - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) - stmt.table(table) - stmt.key = table[primary_key] + def update_all(updates) + stmt = Arel::UpdateManager.new(arel.engine) - if joins_values.any? - @klass.connection.join_to_update(stmt, arel) - else - stmt.take(arel.limit) - stmt.order(*arel.orders) - stmt.wheres = arel.constraints - end + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) + stmt.key = table[primary_key] - @klass.connection.update stmt, 'SQL', bind_values + if joins_values.any? + @klass.connection.join_to_update(stmt, arel) + else + stmt.take(arel.limit) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints end + + @klass.connection.update stmt, 'SQL', bind_values end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -295,7 +343,7 @@ module ActiveRecord end end - # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, + # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. # @@ -322,17 +370,12 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records first, and hence not - # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that - # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns - # the number of rows affected. - # - # ==== Parameters - # - # * +conditions+ - Conditions are specified the same way as with +find+ method. - # - # ==== Example + # Deletes the records matching +conditions+ without instantiating the records + # first, and hence not calling the +destroy+ method nor invoking callbacks. This + # is a single SQL DELETE statement that goes straight to the database, much more + # efficient than +destroy_all+. Be careful with relations though, in particular + # <tt>:dependent</tt> rules defined on associations are not honored. Returns the + # number of rows affected. # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) @@ -341,13 +384,27 @@ module ActiveRecord # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. + # + # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # + # Post.limit(100).delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope def delete_all(conditions = nil) - IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled? + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else - statement = arel.compile_delete - affected = @klass.connection.delete(statement, 'SQL', bind_values) + stmt = Arel::DeleteManager.new(arel.engine) + stmt.from(table) + + if joins_values.any? + @klass.connection.join_to_delete(stmt, arel, table[primary_key]) + else + stmt.wheres = arel.constraints + end + + affected = @klass.connection.delete(stmt, 'SQL', bind_values) reset affected @@ -375,7 +432,6 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled? where(primary_key => id_or_array).delete_all end @@ -393,7 +449,7 @@ module ActiveRecord end def to_sql - @to_sql ||= klass.connection.to_sql(arel) + @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end def where_values_hash @@ -401,7 +457,12 @@ module ActiveRecord node.left.relation.name == table_name } - Hash[equalities.map { |where| [where.left.name, where.right] }] + binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] + + Hash[equalities.map { |where| + name = where.left.name + [name, binds.fetch(name.to_s) { where.right }] + }] end def scope_for_create @@ -410,8 +471,8 @@ module ActiveRecord def eager_loading? @should_eager_load ||= - @eager_load_values.any? || - @includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) + eager_load_values.any? || + includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) end # Joins that are also marked for preloading. In which case we should just eager load them. @@ -419,7 +480,7 @@ module ActiveRecord # represent the same association, but that aren't matched by this. Also, we could have # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] } def joined_includes_values - @includes_values & @joins_values + includes_values & joins_values end def ==(other) @@ -435,6 +496,10 @@ module ActiveRecord to_a.inspect end + def pretty_print(q) + q.pp(self.to_a) + end + def with_default_scope #:nodoc: if default_scoped? && default_scope = klass.send(:build_default_scope) default_scope = default_scope.merge(self) @@ -445,18 +510,12 @@ module ActiveRecord end end - protected + def blank? + to_a.blank? + end - def method_missing(method, *args, &block) - if Array.method_defined?(method) - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) - scoping { @klass.send(method, *args, &block) } - elsif arel.respond_to?(method) - arel.send(method, *args, &block) - else - super - end + def values + @values.dup end private @@ -474,8 +533,30 @@ module ActiveRecord # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq - - (tables_in_string(to_sql) - joined_tables).any? + string_tables = tables_in_string(to_sql) + + if (references_values - joined_tables).any? + true + elsif (string_tables - joined_tables).any? + ActiveSupport::Deprecation.warn( + "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ + "that are referenced in a string SQL snippet. For example: \n" \ + "\n" \ + " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ + "\n" \ + "Currently, Active Record recognises the table in the string, and knows to JOIN the " \ + "comments table to the query, rather than loading comments in a separate query. " \ + "However, doing this without writing a full-blown SQL parser is inherently flawed. " \ + "Since we don't want to write an SQL parser, we are removing this functionality. " \ + "From now on, you must explicitly tell Active Record when you are referencing a table " \ + "from a string:\n" \ + "\n" \ + " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" + ) + true + else + false + end end def tables_in_string(string) @@ -484,6 +565,5 @@ module ActiveRecord # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end - end end |