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.rb245
1 files changed, 157 insertions, 88 deletions
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 06a388cd21..47dc2d4a3a 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -7,7 +7,7 @@ require 'active_support/time'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
-require 'active_support/core_ext/class/inheritable_attributes'
+require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/indifferent_access'
@@ -204,7 +204,7 @@ module ActiveRecord #:nodoc:
#
# # No 'Winter' tag exists
# winter = Tag.find_or_initialize_by_name("Winter")
- # winter.new_record? # true
+ # winter.persisted? # false
#
# To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
# a list of parameters.
@@ -412,7 +412,7 @@ module ActiveRecord #:nodoc:
self.store_full_sti_class = true
# Stores the default scope for the class
- class_inheritable_accessor :default_scoping, :instance_writer => false
+ class_attribute :default_scoping, :instance_writer => false
self.default_scoping = []
# Returns a hash of all the attributes that have been specified for serialization as
@@ -420,6 +420,9 @@ module ActiveRecord #:nodoc:
class_attribute :serialized_attributes
self.serialized_attributes = {}
+ class_attribute :_attr_readonly, :instance_writer => false
+ self._attr_readonly = []
+
class << self # Class methods
delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
@@ -504,12 +507,12 @@ module ActiveRecord #:nodoc:
# Attributes listed as readonly will be used to create a new record but update operations will
# ignore these fields.
def attr_readonly(*attributes)
- write_inheritable_attribute(:attr_readonly, Set.new(attributes.map { |a| a.to_s }) + (readonly_attributes || []))
+ self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
end
# Returns an array of all the attributes that have been specified as readonly.
def readonly_attributes
- read_inheritable_attribute(:attr_readonly) || []
+ self._attr_readonly
end
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
@@ -724,10 +727,6 @@ module ActiveRecord #:nodoc:
@arel_engine = @relation = @arel_table = nil
end
- def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
- descendants.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
- end
-
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
@@ -736,15 +735,12 @@ module ActiveRecord #:nodoc:
def lookup_ancestors #:nodoc:
klass = self
classes = [klass]
+ return classes if klass == ActiveRecord::Base
+
while klass != klass.base_class
classes << klass = klass.superclass
end
classes
- rescue
- # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
- # Apparently the method base_class causes some trouble.
- # It now works for sure.
- [self]
end
# Set the i18n scope to overwrite ActiveModel.
@@ -833,7 +829,7 @@ module ActiveRecord #:nodoc:
def arel_engine
@arel_engine ||= begin
if self == ActiveRecord::Base
- Arel::Table.engine
+ ActiveRecord::Base
else
connection_handler.connection_pools[name] ? self : superclass.arel_engine
end
@@ -856,8 +852,8 @@ module ActiveRecord #:nodoc:
# limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
#
- # It is recommended to use block form of unscoped because chaining unscoped with <tt>named_scope</tt>
- # does not work. Assuming that <tt>published</tt> is a <tt>named_scope</tt> following two statements are same.
+ # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
+ # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same.
#
# Post.unscoped.published
# Post.published
@@ -874,20 +870,36 @@ module ActiveRecord #:nodoc:
reset_scoped_methods
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.
+ def instantiate(record) # :nodoc:
+ model = find_sti_class(record[inheritance_column]).allocate
+ model.init_with('attributes' => record)
+ model
+ end
+
private
def relation #:nodoc:
@relation ||= Relation.new(self, arel_table)
- finder_needs_type_condition? ? @relation.where(type_condition) : @relation
- 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.
- def instantiate(record)
- model = find_sti_class(record[inheritance_column]).allocate
- model.init_with('attributes' => record)
- model
+ if finder_needs_type_condition?
+ @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
+ else
+ @relation
+ end
end
def find_sti_class(type_name)
@@ -917,11 +929,10 @@ module ActiveRecord #:nodoc:
end
def type_condition
- sti_column = arel_table[inheritance_column]
- condition = sti_column.eq(sti_name)
- descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) }
+ sti_column = arel_table[inheritance_column.to_sym]
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
- condition
+ sti_column.in(sti_names)
end
# Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -1129,7 +1140,8 @@ MSG
# Article.create.published # => true
def default_scope(options = {})
reset_scoped_methods
- self.default_scoping << construct_finder_arel(options, default_scoping.pop)
+ default_scoping = self.default_scoping.dup
+ self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop)
end
def current_scoped_methods #:nodoc:
@@ -1286,7 +1298,7 @@ MSG
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
- if values.first.is_a?(Hash) and statement =~ /:\w+/
+ if values.first.is_a?(Hash) && statement =~ /:\w+/
replace_named_bind_variables(statement, values.first)
elsif statement.include?('?')
replace_bind_variables(statement, values)
@@ -1367,6 +1379,8 @@ MSG
# hence you can't have attributes that aren't part of the table columns.
def initialize(attributes = nil)
@attributes = attributes_from_column_definition
+ @association_cache = {}
+ @aggregation_cache = {}
@attributes_cache = {}
@new_record = true
@readonly = false
@@ -1385,28 +1399,20 @@ MSG
result
end
- # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone
- # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is
- # application specific and is therefore left to the application to implement according to its need.
- def initialize_copy(other)
- _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- cloned_attributes.delete(self.class.primary_key)
-
- @attributes = cloned_attributes
-
- @changed_attributes = {}
- attributes_from_column_definition.each do |attr, orig_value|
- @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
- end
-
- clear_aggregation_cache
- clear_association_cache
- @attributes_cache = {}
- @new_record = true
- ensure_proper_type
-
- populate_with_current_scope_attributes
+ # Populate +coder+ with attributes about this record that should be
+ # serialized. The structure of +coder+ defined in this method is
+ # guaranteed to match the structure of +coder+ passed to the +init_with+
+ # method.
+ #
+ # Example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ # coder = {}
+ # Post.new.encode_with(coder)
+ # coder # => { 'id' => nil, ... }
+ def encode_with(coder)
+ coder['attributes'] = attributes
end
# Initialize an empty model object from +coder+. +coder+ must contain
@@ -1422,11 +1428,24 @@ MSG
def init_with(coder)
@attributes = coder['attributes']
@attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
- @new_record = @readonly = @destroyed = @marked_for_destruction = false
+ @association_cache = {}
+ @aggregation_cache = {}
+ @readonly = @destroyed = @marked_for_destruction = false
+ @new_record = false
_run_find_callbacks
_run_initialize_callbacks
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.
@@ -1481,22 +1500,9 @@ MSG
@attributes.has_key?(attr_name.to_s)
end
- # Returns an array of names for the attributes available on this object sorted alphabetically.
+ # Returns an array of names for the attributes available on this object.
def attribute_names
- @attributes.keys.sort
- end
-
- # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- # (Alias for the protected read_attribute method).
- def [](attr_name)
- read_attribute(attr_name)
- end
-
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
- def []=(attr_name, value)
- write_attribute(attr_name, value)
+ @attributes.keys
end
# Allows you to set all the attributes at once by passing in a hash with keys
@@ -1529,8 +1535,10 @@ MSG
attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
+ elsif respond_to?("#{k}=")
+ send("#{k}=", v)
else
- respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
end
end
@@ -1539,9 +1547,7 @@ MSG
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
- attrs = {}
- attribute_names.each { |name| attrs[name] = read_attribute(name) }
- attrs
+ Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -1572,8 +1578,7 @@ MSG
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
def attribute_present?(attribute)
- value = read_attribute(attribute)
- !value.blank?
+ !_read_attribute(attribute).blank?
end
# Returns the column object for the named attribute.
@@ -1581,16 +1586,25 @@ MSG
self.class.columns_hash[name.to_s]
end
- # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
+ #
+ # Note that new records are different from any other record by definition, unless the
+ # other record is the receiver itself. Besides, if you fetch existing records with
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
+ #
+ # 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) ||
- (comparison_object.instance_of?(self.class) &&
- comparison_object.id == id && !comparison_object.new_record?)
+ comparison_object.instance_of?(self.class) &&
+ id.present? &&
+ comparison_object.id == id
end
# Delegates to ==
def eql?(comparison_object)
- self == (comparison_object)
+ self == comparison_object
end
# Delegates to id in order to allow two records of the same type and id to work with something like:
@@ -1609,11 +1623,42 @@ MSG
@attributes.frozen?
end
- # Returns duplicated record with unfreezed attributes.
- def dup
- obj = super
- obj.instance_variable_set('@attributes', @attributes.dup)
- obj
+ # Backport dup from 1.9 so that initialize_dup() gets called
+ unless Object.respond_to?(:initialize_dup)
+ def dup # :nodoc:
+ copy = super
+ copy.initialize_dup(self)
+ copy
+ end
+ end
+
+ # Duped objects have no id assigned and are treated as new records. Note
+ # that this is a "shallow" copy as it copies the object's attributes
+ # only, not its associations. The extent of a "deep" copy is application
+ # specific and is therefore left to the application to implement according
+ # to its need.
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
+ def initialize_dup(other)
+ cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
+ cloned_attributes.delete(self.class.primary_key)
+
+ @attributes = cloned_attributes
+
+ _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
+
+ @changed_attributes = {}
+ attributes_from_column_definition.each do |attr, orig_value|
+ @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
+ end
+
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @new_record = true
+
+ ensure_proper_type
+ populate_with_current_scope_attributes
+ clear_timestamp_attributes
end
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
@@ -1630,7 +1675,7 @@ 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) || new_record?
+ if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
}.compact.join(", ")
@@ -1682,7 +1727,7 @@ MSG
if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name))
value = read_attribute(name)
- if value && self.class.serialized_attributes.key?(name)
+ if !value.nil? && self.class.serialized_attributes.key?(name)
value = YAML.dump value
end
attrs[self.class.arel_table[name]] = value
@@ -1817,7 +1862,19 @@ MSG
def populate_with_current_scope_attributes
if scope = self.class.send(:current_scoped_methods)
create_with = scope.scope_for_create
- create_with.each { |att,value| self.respond_to?(:"#{att}=") && self.send("#{att}=", value) } if create_with
+ create_with.each { |att,value|
+ respond_to?("#{att}=") && send("#{att}=", value)
+ }
+ end
+ end
+
+ # Clear attributes and changed_attributes
+ def clear_timestamp_attributes
+ %w(created_at created_on updated_at updated_on).each do |attribute_name|
+ if has_attribute?(attribute_name)
+ self[attribute_name] = nil
+ changed_attributes.delete(attribute_name)
+ end
end
end
end
@@ -1841,6 +1898,7 @@ MSG
include ActiveModel::MassAssignmentSecurity
include Callbacks, ActiveModel::Observing, Timestamp
include Associations, AssociationPreload, NamedScope
+ include ActiveModel::SecurePassword
# AutosaveAssociation needs to be included before Transactions, because we want
# #save_with_autosave_associations to be wrapped inside a transaction.
@@ -1848,6 +1906,17 @@ MSG
include Aggregations, Transactions, Reflection, Serialization
NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner)
+
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ alias [] read_attribute
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ alias []= write_attribute
+
+ public :[], :[]=
end
end