diff options
Diffstat (limited to 'activerecord/lib/active_record/inheritance.rb')
-rw-r--r-- | activerecord/lib/active_record/inheritance.rb | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb new file mode 100644 index 0000000000..2c766411a0 --- /dev/null +++ b/activerecord/lib/active_record/inheritance.rb @@ -0,0 +1,184 @@ +require 'active_support/concern' + +module ActiveRecord + module Inheritance + extend ActiveSupport::Concern + + included do + # Determine whether to store the full constant name including namespace when using STI + config_attribute :store_full_sti_class + self.store_full_sti_class = true + end + + module ClassMethods + # True if this isn't a concrete subclass needing a STI type condition. + def descends_from_active_record? + sup = active_record_super + + if sup.abstract_class? + sup.descends_from_active_record? + elsif self == Base + false + else + [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column) + end + end + + def finder_needs_type_condition? #:nodoc: + # This is like this because benchmarking justifies the strange :false stuff + :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) + end + + def symbolized_base_class + @symbolized_base_class ||= base_class.to_s.to_sym + end + + def symbolized_sti_name + @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class + end + + # Returns the base AR subclass that this class descends from. If A + # extends AR::Base, A.base_class will return A. If B descends from A + # through some arbitrarily deep hierarchy, B.base_class will return A. + # + # If B < A and C < B and if A is an abstract_class then both B.base_class + # and C.base_class would return B as the answer since A is an abstract_class. + def base_class + class_of_active_record_descendant(self) + end + + # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). + attr_accessor :abstract_class + + # Returns whether this class is an abstract class or not. + def abstract_class? + defined?(@abstract_class) && @abstract_class == true + end + + def sti_name + store_full_sti_class ? name : name.demodulize + 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, column_types = {}) + sti_class = find_sti_class(record[inheritance_column]) + record_id = sti_class.primary_key && record[sti_class.primary_key] + + if ActiveRecord::IdentityMap.enabled? && record_id + if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? + record_id = record_id.to_i + end + if instance = IdentityMap.get(sti_class, record_id) + instance.reinit_with('attributes' => record) + else + instance = sti_class.allocate.init_with('attributes' => record) + IdentityMap.add(instance) + end + else + column_types = sti_class.decorate_columns(column_types) + instance = sti_class.allocate.init_with('attributes' => record, + 'column_types' => column_types) + end + + instance + end + + # For internal use. + # + # If this class includes ActiveRecord::Model then it won't have a + # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model). + def active_record_super #:nodoc: + superclass < Model ? superclass : Model + end + + protected + + # 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) + unless klass < Model + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + end + + sup = klass.active_record_super + if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class? + klass + else + class_of_active_record_descendant(sup) + 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) + if type_name.match(/^::/) + # If the type is prefixed with a scope operator then we assume that + # the type_name is an absolute reference. + ActiveSupport::Dependencies.constantize(type_name) + else + # Build a list of candidates to search for + candidates = [] + name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } + candidates << type_name + + candidates.each do |candidate| + begin + constant = ActiveSupport::Dependencies.constantize(candidate) + return constant if candidate == constant.to_s + rescue NameError => e + # We don't want to swallow NoMethodError < NameError errors + raise e unless e.instance_of?(NameError) + end + end + + raise NameError, "uninitialized constant #{candidates.first}" + end + end + + private + + def find_sti_class(type_name) + if type_name.blank? || !columns_hash.include?(inheritance_column) + self + else + begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + + "or overwrite #{name}.inheritance_column to use another column for that information." + end + end + end + + def type_condition(table = arel_table) + sti_column = table[inheritance_column.to_sym] + sti_names = ([self] + descendants).map { |model| model.sti_name } + + sti_column.in(sti_names) + end + end + + private + + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # 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 + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) + end + end + end +end |