aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/base.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/base.rb')
-rw-r--r--activerecord/lib/active_record/base.rb281
1 files changed, 166 insertions, 115 deletions
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index d07f9365b3..78159d13d4 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -177,6 +177,10 @@ module ActiveRecord #:nodoc:
# And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
# <tt>Person.find_all_by_last_name(last_name)</tt>.
#
+ # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
+ # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
+ # like <tt>Person.find_by_last_name!</tt>.
+ #
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
#
# Person.where(:user_name => user_name, :password => password).first
@@ -393,8 +397,8 @@ module ActiveRecord #:nodoc:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
- cattr_accessor :pluralize_table_names, :instance_writer => false
- @@pluralize_table_names = true
+ class_attribute :pluralize_table_names, :instance_writer => false
+ self.pluralize_table_names = true
##
# :singleton-method:
@@ -438,6 +442,7 @@ module ActiveRecord #:nodoc:
class << self # Class methods
delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
@@ -504,8 +509,7 @@ module ActiveRecord #:nodoc:
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, options, &block) }
else
- object = new(attributes, options)
- yield(object) if block_given?
+ object = new(attributes, options, &block)
object.save
object
end
@@ -577,15 +581,25 @@ module ActiveRecord #:nodoc:
#
# ==== Examples
#
- # class Invoice < ActiveRecord::Base; end;
+ # class Invoice < ActiveRecord::Base
+ # end
+ #
# file class table_name
# invoice.rb Invoice invoices
#
- # class Invoice < ActiveRecord::Base; class Lineitem < ActiveRecord::Base; end; end;
+ # class Invoice < ActiveRecord::Base
+ # class Lineitem < ActiveRecord::Base
+ # end
+ # end
+ #
# file class table_name
# invoice.rb Invoice::Lineitem invoice_lineitems
#
- # module Invoice; class Lineitem < ActiveRecord::Base; end; end;
+ # module Invoice
+ # class Lineitem < ActiveRecord::Base
+ # end
+ # end
+ #
# file class table_name
# invoice/lineitem.rb Invoice::Lineitem lineitems
#
@@ -611,6 +625,8 @@ module ActiveRecord #:nodoc:
# Computes the table name, (re)sets it internally, and returns it.
def reset_table_name #:nodoc:
+ return if abstract_class?
+
self.table_name = compute_table_name
end
@@ -645,8 +661,8 @@ module ActiveRecord #:nodoc:
def set_table_name(value = nil, &block)
@quoted_table_name = nil
define_attr_method :table_name, value, &block
+ @arel_table = nil
- @arel_table = Arel::Table.new(table_name, arel_engine)
@relation = Relation.new(self, arel_table)
end
alias :table_name= :set_table_name
@@ -691,6 +707,10 @@ module ActiveRecord #:nodoc:
# Returns an array of column objects for the table associated with this class.
def columns
+ if defined?(@primary_key)
+ connection_pool.primary_keys[table_name] ||= primary_key
+ end
+
connection_pool.columns[table_name]
end
@@ -699,6 +719,12 @@ module ActiveRecord #:nodoc:
connection_pool.columns_hash[table_name]
end
+ # Returns a hash where the keys are column names and the values are
+ # default values when instantiating the AR object for this table.
+ def column_defaults
+ connection_pool.column_defaults[table_name]
+ end
+
# Returns an array of column names as strings.
def column_names
@column_names ||= columns.map { |column| column.name }
@@ -767,6 +793,17 @@ module ActiveRecord #:nodoc:
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
+ # Returns an array of column names as strings if it's not
+ # an abstract class and table exists.
+ # Otherwise it returns an empty array.
+ def attribute_names
+ @attribute_names ||= if !abstract_class? && table_exists?
+ column_names
+ else
+ []
+ end
+ end
+
# Set the lookup ancestors for ActiveModel.
def lookup_ancestors #:nodoc:
klass = self
@@ -867,7 +904,7 @@ module ActiveRecord #:nodoc:
end
def arel_table
- Arel::Table.new(table_name, arel_engine)
+ @arel_table ||= Arel::Table.new(table_name, arel_engine)
end
def arel_engine
@@ -895,7 +932,7 @@ module ActiveRecord #:nodoc:
# not use the default_scope:
#
# Post.unscoped {
- # limit(10) # Fires "SELECT * FROM posts LIMIT 10"
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
#
# It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
@@ -911,17 +948,6 @@ module ActiveRecord #:nodoc:
self.current_scope = nil
end
- # Specifies how the record is loaded by +Marshal+.
- #
- # +_load+ sets an instance variable for each key in the hash it takes as input.
- # Override this method if you require more complex marshalling.
- def _load(data)
- record = allocate
- record.init_with(Marshal.load(data))
- record
- end
-
-
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
@@ -1025,20 +1051,15 @@ module ActiveRecord #:nodoc:
# Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
# is first invoked, so that future attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments, &block)
- if match = DynamicFinderMatch.match(method_id)
+ if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
- if match.finder?
- options = arguments.extract_options!
- relation = options.any? ? scoped(options) : scoped
- relation.send :find_by_attributes, match, attribute_names, *arguments
- elsif match.instantiator?
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
+ if arguments.size < attribute_names.size
+ method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
+ backtrace = [method_trace] + caller
+ raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
end
- elsif match = DynamicScopeMatch.match(method_id)
- attribute_names = match.attribute_names
- super unless all_attributes_exists?(attribute_names)
- if match.scope?
+ if match.respond_to?(:scope?) && match.scope?
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
@@ -1047,6 +1068,12 @@ module ActiveRecord #:nodoc:
end # end
METHOD
send(method_id, *arguments)
+ elsif match.finder?
+ options = arguments.extract_options!
+ relation = options.any? ? scoped(options) : scoped
+ relation.send :find_by_attributes, match, attribute_names, *arguments, &block
+ elsif match.instantiator?
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
end
else
super
@@ -1183,11 +1210,11 @@ MSG
end
def current_scope #:nodoc:
- Thread.current[:"#{self}_current_scope"]
+ Thread.current["#{self}_current_scope"]
end
def current_scope=(scope) #:nodoc:
- Thread.current[:"#{self}_current_scope"] = scope
+ Thread.current["#{self}_current_scope"] = scope
end
# Use this macro in your model to set a default scope for all operations on
@@ -1242,22 +1269,43 @@ MSG
def build_default_scope #:nodoc:
if method(:default_scope).owner != Base.singleton_class
- # Use relation.scoping to ensure we ignore whatever the current value of
- # self.current_scope may be.
- relation.scoping { default_scope }
+ evaluate_default_scope { default_scope }
elsif default_scopes.any?
- default_scopes.inject(relation) do |default_scope, scope|
- if scope.is_a?(Hash)
- default_scope.apply_finder_options(scope)
- elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(scope.call)
- else
- default_scope.merge(scope)
+ evaluate_default_scope do
+ default_scopes.inject(relation) do |default_scope, scope|
+ if scope.is_a?(Hash)
+ default_scope.apply_finder_options(scope)
+ elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
+ default_scope.merge(scope.call)
+ else
+ default_scope.merge(scope)
+ end
end
end
end
end
+ def ignore_default_scope? #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"]
+ end
+
+ def ignore_default_scope=(ignore) #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"] = ignore
+ end
+
+ # The ignore_default_scope flag is used to prevent an infinite recursion situation where
+ # a default scope references a scope which has a default scope which references a scope...
+ def evaluate_default_scope
+ return if ignore_default_scope?
+
+ begin
+ self.ignore_default_scope = true
+ yield
+ ensure
+ self.ignore_default_scope = false
+ end
+ end
+
# Returns the class type of the record using the current module as a prefix. So descendants of
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
def compute_type(type_name)
@@ -1278,7 +1326,6 @@ MSG
rescue NameError => e
# We don't want to swallow NoMethodError < NameError errors
raise e unless e.instance_of?(NameError)
- rescue ArgumentError
end
end
@@ -1289,7 +1336,7 @@ MSG
# Returns the class descending directly from ActiveRecord::Base or an
# abstract class, if any, in the inheritance hierarchy.
def class_of_active_record_descendant(klass)
- if klass.superclass == Base || klass.superclass.abstract_class?
+ if klass == Base || klass.superclass == Base || klass.superclass.abstract_class?
klass
elsif klass.superclass.nil?
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
@@ -1378,9 +1425,8 @@ MSG
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name).alias(default_table_name)
- viz = Arel::Visitors.for(arel_engine)
PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
- viz.accept b
+ connection.visitor.accept b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -1502,6 +1548,7 @@ MSG
@marked_for_destruction = false
@previously_changed = {}
@changed_attributes = {}
+ @relation = nil
ensure_proper_type
set_serialized_attributes
@@ -1510,9 +1557,8 @@ MSG
assign_attributes(attributes, options) if attributes
- result = yield self if block_given?
+ yield self if block_given?
run_callbacks :initialize
- result
end
# Populate +coder+ with attributes about this record that should be
@@ -1543,6 +1589,7 @@ MSG
# post.title # => 'hello world'
def init_with(coder)
@attributes = coder['attributes']
+ @relation = nil
set_serialized_attributes
@@ -1557,16 +1604,6 @@ MSG
self
end
- # Specifies how the record is dumped by +Marshal+.
- #
- # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this
- # method if you require more complex marshalling.
- def _dump(level)
- dump = {}
- encode_with(dump)
- Marshal.dump(dump)
- end
-
# Returns a String, which Action Pack uses for constructing an URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
@@ -1606,7 +1643,8 @@ MSG
when new_record?
"#{self.class.model_name.cache_key}/new"
when timestamp = self[:updated_at]
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp.to_s(:number)}"
+ timestamp = timestamp.utc.to_s(:number)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
else
"#{self.class.model_name.cache_key}/#{id}"
end
@@ -1632,9 +1670,6 @@ MSG
# If any attributes are protected by either +attr_protected+ or
# +attr_accessible+ then only settable attributes will be assigned.
#
- # The +guard_protected_attributes+ argument is now deprecated, use
- # the +assign_attributes+ method if you want to bypass mass-assignment security.
- #
# class User < ActiveRecord::Base
# attr_protected :is_admin
# end
@@ -1643,21 +1678,10 @@ MSG
# user.attributes = { :username => 'Phusion', :is_admin => true }
# user.username # => "Phusion"
# user.is_admin? # => false
- def attributes=(new_attributes, guard_protected_attributes = nil)
- unless guard_protected_attributes.nil?
- message = "the use of 'guard_protected_attributes' will be removed from the next major release of rails, " +
- "if you want to bypass mass-assignment security then look into using assign_attributes"
- ActiveSupport::Deprecation.warn(message)
- end
-
+ def attributes=(new_attributes)
return unless new_attributes.is_a?(Hash)
- guard_protected_attributes ||= true
- if guard_protected_attributes
- assign_attributes(new_attributes)
- else
- assign_attributes(new_attributes, :without_protection => true)
- end
+ assign_attributes(new_attributes)
end
# Allows you to set all the attributes for a particular mass-assignment
@@ -1691,12 +1715,11 @@ MSG
return unless new_attributes
attributes = new_attributes.stringify_keys
- role = options[:as] || :default
-
multi_parameter_attributes = []
+ @mass_assignment_options = options
unless options[:without_protection]
- attributes = sanitize_for_mass_assignment(attributes, role)
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
end
attributes.each do |k, v|
@@ -1709,6 +1732,7 @@ MSG
end
end
+ @mass_assignment_options = nil
assign_multiparameter_attributes(multi_parameter_attributes)
end
@@ -1718,7 +1742,7 @@ MSG
end
# Returns an <tt>#inspect</tt>-like string for the value of the
- # attribute +attr_name+. String attributes are elided after 50
+ # attribute +attr_name+. String attributes are truncated upto 50
# characters, and Date and Time attributes are returned in the
# <tt>:db</tt> format. Other attributes return the value of
# <tt>#inspect</tt> without modification.
@@ -1763,16 +1787,12 @@ MSG
# Note also that destroying a record preserves its ID in the model instance, so deleted
# models are still comparable.
def ==(comparison_object)
- comparison_object.equal?(self) ||
+ super ||
comparison_object.instance_of?(self.class) &&
id.present? &&
comparison_object.id == id
end
-
- # Delegates to ==
- def eql?(comparison_object)
- self == comparison_object
- end
+ alias :eql? :==
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
@@ -1790,6 +1810,15 @@ MSG
@attributes.frozen?
end
+ # Allows sort on objects
+ def <=>(other_object)
+ if other_object.is_a?(self.class)
+ self.to_key <=> other_object.to_key
+ else
+ nil
+ end
+ end
+
# Backport dup from 1.9 so that initialize_dup() gets called
unless Object.respond_to?(:initialize_dup)
def dup # :nodoc:
@@ -1825,7 +1854,7 @@ MSG
ensure_proper_type
populate_with_current_scope_attributes
- clear_timestamp_attributes
+ super
end
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
@@ -1841,12 +1870,16 @@ MSG
# Returns the contents of the record as a nicely formatted string.
def inspect
- attributes_as_nice_string = self.class.column_names.collect { |name|
- if has_attribute?(name)
- "#{name}: #{attribute_for_inspect(name)}"
- end
- }.compact.join(", ")
- "#<#{self.class} #{attributes_as_nice_string}>"
+ inspection = if @attributes
+ self.class.column_names.collect { |name|
+ if has_attribute?(name)
+ "#{name}: #{attribute_for_inspect(name)}"
+ end
+ }.compact.join(", ")
+ else
+ "not initialized"
+ end
+ "#<#{self.class} #{inspection}>"
end
protected
@@ -1864,12 +1897,33 @@ MSG
value
end
+ def mass_assignment_options
+ @mass_assignment_options ||= {}
+ end
+
+ def mass_assignment_role
+ mass_assignment_options[:as] || :default
+ end
+
private
+ # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
+ # of the array, and then rescues from the possible NoMethodError. If those elements are
+ # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
+ # which significantly impacts upon performance.
+ #
+ # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
+ #
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
+ def to_ary # :nodoc:
+ nil
+ end
+
def set_serialized_attributes
- (@attributes.keys & self.class.serialized_attributes.keys).each do |key|
- coder = self.class.serialized_attributes[key]
- @attributes[key] = coder.load @attributes[key]
+ sattrs = self.class.serialized_attributes
+
+ sattrs.each do |key, coder|
+ @attributes[key] = coder.load @attributes[key] if @attributes.key?(key)
end
end
@@ -1879,8 +1933,9 @@ MSG
# do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
# No such attribute would be set for objects of the Message class in that example.
def ensure_proper_type
- unless self.class.descends_from_active_record?
- write_attribute(self.class.inheritance_column, self.class.sti_name)
+ klass = self.class
+ if klass.finder_needs_type_condition?
+ write_attribute(klass.inheritance_column, klass.sti_name)
end
end
@@ -1976,18 +2031,21 @@ MSG
# If Date bits were not provided, error
raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
+ # If Date bits were provided but blank, then return nil
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
+
set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
- # If Date bits were provided but blank, then default to 1
# If Time bits are not there, then default to 0
- [1,1,1,0,0,0].each_with_index{|v,i| set_values[i] = set_values[i].blank? ? v : set_values[i]}
+ (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
instantiate_time_object(name, set_values)
end
def read_date_parameter_value(name, values_hash_from_param)
- set_values = (1..3).collect{|position| values_hash_from_param[position].blank? ? 1 : values_hash_from_param[position]}
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
+ set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
begin
Date.new(*set_values)
- rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
end
@@ -2008,7 +2066,7 @@ MSG
def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }
- for pair in pairs
+ pairs.each do |pair|
multiparameter_name, value = pair
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
@@ -2054,16 +2112,10 @@ MSG
end
def populate_with_current_scope_attributes
- self.class.scoped.scope_for_create.each do |att,value|
- respond_to?("#{att}=") && send("#{att}=", value)
- end
- end
+ return unless self.class.scope_attributes?
- # Clear attributes and changed_attributes
- def clear_timestamp_attributes
- all_timestamp_attributes_in_model.each do |attribute_name|
- self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
+ self.class.scope_attributes.each do |att,value|
+ send("#{att}=", value) if respond_to?("#{att}=")
end
end
end
@@ -2110,6 +2162,5 @@ MSG
end
end
-# TODO: Remove this and make it work with LAZY flag
-require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/abstract/connection_specification'
ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)