aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/relation.rb')
-rw-r--r--activerecord/lib/active_record/relation.rb317
1 files changed, 186 insertions, 131 deletions
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 0df895eb67..daafb0b645 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,34 +1,32 @@
# -*- coding: utf-8 -*-
+require 'arel/collectors/bind'
module ActiveRecord
# = Active Record Relation
class Relation
- JoinOperation = Struct.new(:relation, :join_class, :on)
-
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :where, :having, :bind, :references,
- :extending]
+ :extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :uniq, :create_with]
+ :reverse_order, :distinct, :create_with, :uniq]
+ INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
- attr_accessor :default_scoped
alias :model :klass
alias :loaded? :loaded
- alias :default_scoped? :default_scoped
def initialize(klass, table, values = {})
- @klass = klass
- @table = table
- @values = values
- @implicit_readonly = nil
- @loaded = false
- @default_scoped = false
+ @klass = klass
+ @table = table
+ @values = values
+ @offsets = {}
+ @loaded = false
+ @predicate_builder = PredicateBuilder.new(klass, table)
end
def initialize_copy(other)
@@ -39,7 +37,7 @@ module ActiveRecord
reset
end
- def insert(values)
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -56,16 +54,7 @@ module ActiveRecord
im = arel.create_insert
im.into @table
- conn = @klass.connection
-
- substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
- binds = substitutes.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
- end
-
- substitutes.each_with_index do |tuple, i|
- tuple[1] = conn.substitute_at(binds[i][0], i)
- end
+ substitutes, binds = substitute_values values
if values.empty? # empty insert
im.values = Arel.sql(connection.empty_insert_statement_value)
@@ -73,7 +62,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -82,6 +71,40 @@ module ActiveRecord
binds)
end
+ def _update_record(values, id, id_was) # :nodoc:
+ substitutes, binds = substitute_values values
+
+ scope = @klass.unscoped
+
+ if @klass.finder_needs_type_condition?
+ scope.unscope!(where: @klass.inheritance_column)
+ end
+
+ relation = scope.where(@klass.primary_key => (id_was || id))
+ bvs = binds + relation.bind_values
+ um = relation
+ .arel
+ .compile_update(substitutes, @klass.primary_key)
+
+ @klass.connection.update(
+ um,
+ 'SQL',
+ bvs,
+ )
+ end
+
+ def substitute_values(values) # :nodoc:
+ binds = values.map do |arel_attr, value|
+ [@klass.columns_hash[arel_attr.name], value]
+ end
+
+ substitutes = values.each_with_index.map do |(arel_attr, _), i|
+ [arel_attr, @klass.connection.substitute_at(binds[i][0])]
+ end
+
+ [substitutes, binds]
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -141,34 +164,58 @@ module ActiveRecord
first || new(attributes, &block)
end
- # Finds the first record with the given attributes, or creates a record with the attributes
- # if one is not found.
+ # Finds the first record with the given attributes, or creates a record
+ # with the attributes if one is not found:
#
- # ==== Examples
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# # We already have one so the existing record will be returned.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<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.
+ # # Find the first user named "Scarlett" or create a new one with
+ # # a particular last name.
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
- # # => <User id: 2, first_name: 'Scarlett', 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.
+ # This method accepts a block, which is passed down to +create+. The last example
+ # above can be alternatively written this way:
+ #
+ # # Find the first user named "Scarlett" or create a new one with a
+ # # different last name.
# User.find_or_create_by(first_name: 'Scarlett') do |user|
- # user.last_name = "O'Hara"
+ # user.last_name = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
+ #
+ # This method always returns a record, but if creation was attempted and
+ # failed due to validation errors it won't be persisted, you get what
+ # +create+ returns in such situation.
+ #
+ # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # there are no results an INSERT is attempted. If there are other threads
+ # or processes there is a race condition between both calls and it could
+ # be the case that you end up with two similar records.
+ #
+ # Whether that is a problem or not depends on the logic of the
+ # application, but in the particular case in which rows have a UNIQUE
+ # constraint an exception may be raised, just retry:
+ #
+ # begin
+ # CreditAccount.find_or_create_by(user_id: user.id)
+ # rescue ActiveRecord::RecordNotUnique
+ # retry
+ # end
+ #
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
+ # is raised if the created record is invalid.
def find_or_create_by!(attributes, &block)
find_by(attributes) || create!(attributes, &block)
end
@@ -188,8 +235,8 @@ module ActiveRecord
# Please see further details in the
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
- _, queries = collecting_queries_for_explain { exec_queries }
- exec_explain(queries)
+ #TODO: Fix for binds.
+ exec_explain(collecting_queries_for_explain { exec_queries })
end
# Converts relation objects to Array.
@@ -198,21 +245,30 @@ module ActiveRecord
@records
end
+ # Serializes the relation objects Array.
+ def encode_with(coder)
+ coder.represent_seq(nil, to_a)
+ end
+
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
# Returns size of the records.
def size
- loaded? ? @records.length : count
+ loaded? ? @records.length : count(:all)
end
# Returns true if there are no records.
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ c = count(:all)
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -236,8 +292,9 @@ module ActiveRecord
# Scope all queries to the current scope.
#
# Comment.where(post_id: 1).scoping do
- # Comment.first # SELECT * FROM comments WHERE post_id = 1
+ # Comment.first
# end
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
#
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
@@ -248,10 +305,11 @@ module ActiveRecord
klass.current_scope = previous
end
- # Updates all records with details given if they match a set of conditions supplied, limits and order can
- # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
- # or validations.
+ # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
+ # statement and sends it straight to the database. It does not instantiate the involved models and it does not
+ # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through
+ # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL
+ # database.
#
# ==== Parameters
#
@@ -270,7 +328,7 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
- stmt = Arel::UpdateManager.new(arel.engine)
+ stmt = Arel::UpdateManager.new
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
stmt.table(table)
@@ -284,7 +342,8 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- @klass.connection.update stmt, 'SQL', bind_values
+ bvs = arel.bind_values + bind_values
+ @klass.connection.update stmt, 'SQL', bvs
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -308,7 +367,7 @@ module ActiveRecord
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
- object.update_attributes(attributes)
+ object.update(attributes)
object
end
end
@@ -342,7 +401,7 @@ module ActiveRecord
if conditions
where(conditions).destroy_all
else
- to_a.each {|object| object.destroy }.tap { reset }
+ to_a.each(&:destroy).tap { reset }
end
end
@@ -388,17 +447,26 @@ module ActiveRecord
# 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:
+ # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
#
# Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
def delete_all(conditions = nil)
- raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
+ if MULTI_VALUE_METHODS.include?(method)
+ send("#{method}_values").any?
+ else
+ send("#{method}_value")
+ end
+ }
+ if invalid_methods.any?
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
+ end
if conditions
where(conditions).delete_all
else
- stmt = Arel::DeleteManager.new(arel.engine)
+ stmt = Arel::DeleteManager.new
stmt.from(table)
if joins_values.any?
@@ -444,17 +512,7 @@ module ActiveRecord
#
# Post.where(published: true).load # => #<ActiveRecord::Relation>
def load
- unless loaded?
- # 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 { exec_queries }
- end
+ exec_queries unless loaded?
self
end
@@ -466,34 +524,55 @@ module ActiveRecord
end
def reset
- @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
+ @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
@should_eager_load = @join_dependency = nil
@records = []
+ @offsets = {}
self
end
# Returns sql statement for the relation.
#
- # Users.where(name: 'Oscar').to_sql
+ # User.where(name: 'Oscar').to_sql
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
- end
-
- # Returns a hash of where conditions
- #
- # Users.where(name: 'Oscar').where_values_hash
- # # => {name: "oscar"}
- def where_values_hash
- equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
- node.left.relation.name == table_name
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ arel = relation.arel
+ binds = (arel.bind_values + relation.bind_values).dup
+ binds.map! { |bv| connection.quote(*bv.reverse) }
+ collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
+ collect.substitute_binds(binds).join
+ end
+ end
+
+ # Returns a hash of where conditions.
+ #
+ # User.where(name: 'Oscar').where_values_hash
+ # # => {name: "Oscar"}
+ def where_values_hash(relation_table_name = table_name)
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ node.left.relation.name == relation_table_name
}
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 }]
+ [name, binds.fetch(name.to_s) {
+ case where.right
+ when Array then where.right.map(&:val)
+ else
+ where.right.val
+ end
+ }]
}]
end
@@ -516,9 +595,17 @@ module ActiveRecord
includes_values & joins_values
end
+ # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
+ # to maintain backwards compatibility. Use +distinct_value+ instead.
+ def uniq_value
+ distinct_value
+ end
+
# Compares two relations for equality.
def ==(other)
case other
+ when Associations::CollectionProxy, AssociationRelation
+ self == other.to_a
when Relation
other.to_sql == to_sql
when Array
@@ -530,16 +617,6 @@ module ActiveRecord
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)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
@@ -556,32 +633,32 @@ module ActiveRecord
"#<#{self.class.name} [#{entries.join(', ')}]>"
end
- private
+ protected
- def exec_queries
- default_scoped = with_default_scope
+ attr_reader :predicate_builder
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+ private
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
+ def exec_queries
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
- # @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
- @records.each { |record| record.readonly! } if readonly
- else
- @records = default_scoped.to_a
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = build_preloader
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each(&:readonly!) if readonly_value
+
@loaded = true
@records
end
+ def build_preloader
+ ActiveRecord::Associations::Preloader.new
+ end
+
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
@@ -594,38 +671,16 @@ module ActiveRecord
joined_tables += [table.name, table.table_alias]
# 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
- string_tables = tables_in_string(to_sql)
+ joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
- 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
+ (references_values - joined_tables).any?
end
def tables_in_string(string)
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# 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_']
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
end
end
end