diff options
Diffstat (limited to 'activerecord/lib/active_record/core.rb')
-rw-r--r-- | activerecord/lib/active_record/core.rb | 345 |
1 files changed, 265 insertions, 80 deletions
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 94c6684700..c2d5582f02 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,6 +1,7 @@ +require 'thread' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/duplicable' -require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord module Core @@ -16,7 +17,6 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## - # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -42,9 +42,16 @@ module ActiveRecord # 'database' => 'db/production.sqlite3' # } # } - mattr_accessor :configurations, instance_writer: false + def self.configurations=(config) + @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + end self.configurations = {} + # Returns fully resolved configurations hash + def self.configurations + @@configurations + end + ## # :singleton-method: # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling @@ -69,32 +76,132 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true - class_attribute :connection_handler, instance_writer: false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new + ## + # :singleton-method: + # Specify whether schema dump should happen at the end of the + # db:migrate rake task. This is true by default, which is useful for the + # development environment. This should ideally be false in the production + # environment where dumping schema is rarely needed. + mattr_accessor :dump_schema_after_migration, instance_writer: false + self.dump_schema_after_migration = true + + mattr_accessor :maintain_test_schema, instance_accessor: false + + def self.disable_implicit_join_references=(value) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Implicit join references were removed with Rails 4.1. + Make sure to remove this configuration because it does nothing. + MSG + end + + class_attribute :default_connection_handler, instance_writer: false + class_attribute :find_by_statement_cache + + def self.connection_handler + ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler + end + + def self.connection_handler=(handler) + ActiveRecord::RuntimeRegistry.connection_handler = handler + end + + self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new end module ClassMethods - def inherited(child_class) #:nodoc: - child_class.initialize_generated_modules + def allocate + define_attribute_methods super end - def initialize_generated_modules - @attribute_methods_mutex = Mutex.new + def initialize_find_by_cache + self.find_by_statement_cache = {}.extend(Mutex_m) + end - # force attribute methods to be higher in inheritance hierarchy than other generated methods - generated_attribute_methods.const_set(:AttrNames, Module.new { - def self.const_missing(name) - const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze) - end - }) + def inherited(child_class) + child_class.initialize_find_by_cache + super + end + + def find(*ids) + # We don't have cache keys for this stuff yet + return super unless ids.length == 1 + # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5 + return super if ids.first.kind_of?(Symbol) + return super if block_given? || + primary_key.nil? || + default_scopes.any? || + columns_hash.include?(inheritance_column) || + ids.first.kind_of?(Array) + + id = ids.first + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG + end + key = primary_key + + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + where(key => params.bind).limit(1) + } + } + record = s.execute([id], self, connection).first + unless record + raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + end + record + rescue RangeError + raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" + end + + def find_by(*args) + return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? + return super if default_scopes.any? + + hash = args.first + + return super if hash.values.any? { |v| + v.nil? || Array === v || Hash === v + } + + # We can't cache Post.find_by(author: david) ...yet + return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } + + key = hash.keys + + klass = self + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + wheres = key.each_with_object({}) { |param,o| + o[param] = params.bind + } + klass.where(wheres).limit(1) + } + } + begin + s.execute(hash.values, self, connection).first + rescue TypeError => e + raise ActiveRecord::StatementInvalid.new(e.message, e) + rescue RangeError + nil + end + end + + def find_by!(*args) + find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + end - generated_feature_methods + def initialize_generated_modules + generated_association_methods end - def generated_feature_methods - @generated_feature_methods ||= begin - mod = const_set(:GeneratedFeatureMethods, Module.new) + def generated_association_methods + @generated_association_methods ||= begin + mod = const_set(:GeneratedAssociationMethods, Module.new) include mod mod end @@ -106,6 +213,8 @@ module ActiveRecord super elsif abstract_class? "#{super}(abstract)" + elsif !connected? + "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' "#{super}(#{attr_list})" @@ -122,27 +231,26 @@ module ActiveRecord # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. # # class Post < ActiveRecord::Base - # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) + # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end - def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) + def arel_table # :nodoc: + @arel_table ||= Arel::Table.new(table_name) end # Returns the Arel engine. - def arel_engine - @arel_engine ||= begin + def arel_engine # :nodoc: + @arel_engine ||= if Base == self || connection_handler.retrieve_connection_pool(self) self else superclass.arel_engine end - end end private def relation #:nodoc: - relation = Relation.new(self, arel_table) + relation = Relation.create(self, arel_table) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -160,21 +268,19 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil) - defaults = self.class.column_defaults.dup - defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } - - @attributes = self.class.initialize_attributes(defaults) - @columns_hash = self.class.column_types.dup + def initialize(attributes = nil, options = {}) + @attributes = self.class._default_attributes.dup init_internals - ensure_proper_type - populate_with_current_scope_attributes + initialize_internals_callback - assign_attributes(attributes) if attributes + self.class.define_attribute_methods + # +options+ argument is only needed to make protected_attributes gem easier to hook. + # Remove it when we drop support to this gem. + init_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize unless _initialize_callbacks.empty? + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ must contain @@ -188,15 +294,16 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) - @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) + @attributes = coder['attributes'] init_internals - @new_record = false + @new_record = coder['new_record'] - run_callbacks :find - run_callbacks :initialize + self.class.define_attribute_methods + + _run_find_callbacks + _run_initialize_callbacks self end @@ -229,27 +336,17 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes, :serialized => false) - - @attributes = cloned_attributes - @attributes[self.class.primary_key] = nil + @attributes = @attributes.dup + @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) unless _initialize_callbacks.empty? - - @changed_attributes = {} - self.class.column_defaults.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) - end + _run_initialize_callbacks @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} @new_record = true + @destroyed = false - ensure_proper_type - populate_with_current_scope_attributes super end @@ -266,7 +363,10 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes + # FIXME: Remove this when we better serialize attributes + coder['raw_attributes'] = attributes_before_type_cast + coder['attributes'] = @attributes + coder['new_record'] = new_record? end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -281,7 +381,7 @@ module ActiveRecord def ==(comparison_object) super || comparison_object.instance_of?(self.class) && - id.present? && + !id.nil? && comparison_object.id == id end alias :eql? :== @@ -289,12 +389,18 @@ module ActiveRecord # 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) ] def hash - id.hash + if id + id.hash + else + super + end end - # Freeze the attributes hash such that associations are still accessible, even on destroyed records. + # Clone and freeze the attributes hash such that associations are still + # accessible, even on destroyed records, but cloned models will not be + # frozen. def freeze - @attributes.freeze + @attributes = @attributes.clone.freeze self end @@ -307,6 +413,8 @@ module ActiveRecord def <=>(other_object) if other_object.is_a?(self.class) self.to_key <=> other_object.to_key + else + super end end @@ -321,16 +429,15 @@ module ActiveRecord @readonly = true end - # Returns the connection currently associated with the class. This can - # also be used to "borrow" the connection to do database work that isn't - # easily done without going straight to SQL. - def connection - self.class.connection + def connection_handler + self.class.connection_handler end # Returns the contents of the record as a nicely formatted string. def inspect - inspection = if @attributes + # We check defined?(@attributes) not to issue warnings if the object is + # allocated but not initialized. + inspection = if defined?(@attributes) && @attributes self.class.column_names.collect { |name| if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" @@ -342,13 +449,79 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # when pp is required. + def pretty_print(pp) + pp.object_address_group(self) do + if defined?(@attributes) && @attributes + column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) + pp.breakable ' ' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end + end + else + pp.breakable ' ' + pp.text 'not initialized' + end + end + end + # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) - Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access + Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access + end + + def set_transaction_state(state) # :nodoc: + @transaction_state = state + end + + def has_transactional_callbacks? # :nodoc: + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? end private + # Updates the attributes on this particular ActiveRecord object so that + # if it is associated with a transaction, then the state of the AR object + # will be updated to reflect the current state of the transaction + # + # The @transaction_state variable stores the states of the associated + # transaction. This relies on the fact that a transaction can only be in + # one rollback or commit (otherwise a list of states would be required) + # Each AR object inside of a transaction carries that transaction's + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the ActiveRecord object + # as appropriate. + # + # Since ActiveRecord objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the ActiveRecord object reflects the state of the object. + def sync_with_transaction_state + update_attributes_from_transaction_state(@transaction_state, 0) + end + + def update_attributes_from_transaction_state(transaction_state, depth) + if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? + unless @reflects_state[depth] + restore_transaction_record_state if transaction_state.rolledback? + clear_transaction_record_state + @reflects_state[depth] = true + end + + if transaction_state.parent && !@reflects_state[depth+1] + update_attributes_from_transaction_state(transaction_state.parent, depth+1) + end + end + end + # 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, @@ -362,20 +535,32 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - @attributes[pk] = nil unless @attributes.key?(pk) - - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true - @txn = nil + @aggregation_cache = {} + @association_cache = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @destroyed_by_association = nil + @new_record = true + @txn = nil @_start_transaction_state = {} + @transaction_state = nil + @reflects_state = [false] + end + + def initialize_internals_callback + end + + # This method is needed to make protected_attributes gem easier to hook. + # Remove it when we drop support to this gem. + def init_attributes(attributes, options) + assign_attributes(attributes) + end + + def thaw + if frozen? + @attributes = @attributes.dup + end end end end |