diff options
Diffstat (limited to 'activerecord/lib/active_record/base.rb')
-rw-r--r-- | activerecord/lib/active_record/base.rb | 273 |
1 files changed, 157 insertions, 116 deletions
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cd16b8d3ca..360e494af1 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -115,8 +115,8 @@ module ActiveRecord #:nodoc: # When joining tables, nested hashes or keys written in the form 'table_name.column_name' # can be used to qualify the table name of a particular condition. For instance: # - # Student.joins(:schools).where(:schools => { :type => 'public' }) - # Student.joins(:schools).where('schools.type' => 'public' ) + # Student.joins(:schools).where(:schools => { :category => 'public' }) + # Student.joins(:schools).where('schools.category' => 'public' ) # # == Overwriting default accessors # @@ -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 } @@ -878,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 @@ -922,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. @@ -1036,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)] @@ -1058,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 @@ -1194,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 @@ -1253,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) @@ -1289,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 @@ -1300,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" @@ -1389,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 @@ -1513,6 +1548,7 @@ MSG @marked_for_destruction = false @previously_changed = {} @changed_attributes = {} + @relation = nil ensure_proper_type set_serialized_attributes @@ -1521,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 @@ -1554,6 +1589,7 @@ MSG # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] + @relation = nil set_serialized_attributes @@ -1568,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. @@ -1617,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 @@ -1643,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 @@ -1654,20 +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) - if guard_protected_attributes == false - assign_attributes(new_attributes, :without_protection => true) - else - assign_attributes(new_attributes) - end + assign_attributes(new_attributes) end # Allows you to set all the attributes for a particular mass-assignment @@ -1701,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| @@ -1719,6 +1732,7 @@ MSG end end + @mass_assignment_options = nil assign_multiparameter_attributes(multi_parameter_attributes) end @@ -1728,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. @@ -1773,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) ] @@ -1800,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: @@ -1835,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 @@ -1851,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 @@ -1874,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 @@ -1889,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 @@ -1986,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 @@ -2018,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) @@ -2064,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 @@ -2103,7 +2145,7 @@ MSG # AutosaveAssociation needs to be included before Transactions, because we want # #save_with_autosave_associations to be wrapped inside a transaction. include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization + include Aggregations, Transactions, Reflection, Serialization, Store NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) @@ -2120,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) |