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.rb143
1 files changed, 102 insertions, 41 deletions
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ae9afad48a..ecefaa633c 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/module/delegation'
module ActiveRecord
# = Active Record Relation
@@ -6,13 +7,13 @@ module ActiveRecord
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, :create_with, :from, :reorder]
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
# 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, :to => :klass
+ delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass
attr_reader :table, :klass, :loaded
attr_accessor :extensions, :default_scoped
@@ -29,6 +30,7 @@ module ActiveRecord
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)
@@ -66,7 +68,7 @@ module ActiveRecord
end
conn.insert(
- im.to_sql,
+ im,
'SQL',
primary_key,
primary_key_value,
@@ -92,6 +94,48 @@ module ActiveRecord
scoping { @klass.create!(*args, &block) }
end
+ # 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
+
def respond_to?(method, include_private = false)
arel.respond_to?(method, include_private) ||
Array.method_defined?(method) ||
@@ -102,24 +146,30 @@ module ActiveRecord
def to_a
return @records if loaded?
- @records = if @readonly_value.nil? && !@klass.locking_enabled?
- eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
- else
- IdentityMap.without do
- eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
+ 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
- end
- preload = @preload_values
- preload += @includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
+ 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
- @records.each { |record| record.readonly! } if readonly
+ # @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
+ end
@loaded = true
@records
@@ -208,19 +258,21 @@ module ActiveRecord
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
- limit = nil
- order = []
- # Apply limit and order only if they're both present
- if @limit_value.present? == @order_values.present?
- limit = arel.limit
- order = arel.orders
- end
+ stmt = Arel::UpdateManager.new(arel.engine)
- stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
- stmt.take limit if limit
- stmt.order(*order)
+ stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
+ stmt.table(table)
stmt.key = table[primary_key]
- @klass.connection.update stmt.to_sql, '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
end
@@ -242,8 +294,7 @@ module ActiveRecord
# Person.update(people.keys, people.values)
def update(id, attributes)
if id.is_a?(Array)
- idx = -1
- id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
+ id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])}
else
object = find(id)
object.update_attributes(attributes)
@@ -287,7 +338,7 @@ module ActiveRecord
end
# 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
+ # 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.
#
# This essentially finds the object (or multiple objects) with the given id, creates a new object
@@ -316,7 +367,7 @@ module ActiveRecord
# 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
+ # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
# the number of rows affected.
#
# ==== Parameters
@@ -338,8 +389,7 @@ module ActiveRecord
where(conditions).delete_all
else
statement = arel.compile_delete
- affected = @klass.connection.delete(
- statement.to_sql, 'SQL', bind_values)
+ affected = @klass.connection.delete(statement, 'SQL', bind_values)
reset
affected
@@ -385,7 +435,7 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= arel.to_sql
+ @to_sql ||= klass.connection.to_sql(arel)
end
def where_values_hash
@@ -397,11 +447,21 @@ module ActiveRecord
end
def scope_for_create
- @scope_for_create ||= where_values_hash.merge(@create_with_value || {})
+ @scope_for_create ||= where_values_hash.merge(create_with_value)
end
def eager_loading?
- @should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?))
+ @should_eager_load ||=
+ @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.
+ # Note that this is a naive implementation because we could have strings and symbols which
+ # 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
end
def ==(other)
@@ -418,9 +478,10 @@ module ActiveRecord
end
def with_default_scope #:nodoc:
- if default_scoped?
- default_scope = @klass.send(:build_default_scope)
- default_scope ? default_scope.merge(self) : self
+ 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