I've made minor (or major) rework in how inheritance works.
Basicaly it only loads the inheritance on the class that actually will
use it, also it moves the inheritance support to a different module,
makes simple to create additional inheritances support.
I'm planning to release a class table inheritance support soon.

I've tried to submit to trac, but it seems down.

Would you guys give a test and please let me now if it breaks something.

Thanks ;)

--
Rodrigo Kochenburger
<divoxx at gmail dot com>
Linkedin professional profile: http://www.linkedin.com/in/rodrigok
Index: test/connections/native_postgresql/connection.rb
===================================================================
--- test/connections/native_postgresql/connection.rb    (revision 4751)
+++ test/connections/native_postgresql/connection.rb    (working copy)
@@ -7,13 +7,13 @@
 ActiveRecord::Base.configurations = {
   'arunit' => {
     :adapter  => 'postgresql',
-    :username => 'postgres',
+    :username => 'rodrigo',
     :database => 'activerecord_unittest',
     :min_messages => 'warning'
   },
   'arunit2' => {
     :adapter  => 'postgresql',
-    :username => 'postgres',
+    :username => 'rodrigo',
     :database => 'activerecord_unittest2',
     :min_messages => 'warning'
   }
Index: test/base_test.rb
===================================================================
--- test/base_test.rb   (revision 4751)
+++ test/base_test.rb   (working copy)
@@ -1135,7 +1135,7 @@
 
   def test_set_inheritance_column_with_block
     k = Class.new( ActiveRecord::Base )
-    k.set_inheritance_column { original_inheritance_column + "_id" }
+    k.set_inheritance_column { |original| original + "_id" }
     assert_equal "type_id", k.inheritance_column
   end
 
Index: test/inheritance_test.rb
===================================================================
--- test/inheritance_test.rb    (revision 4751)
+++ test/inheritance_test.rb    (working copy)
@@ -47,7 +47,7 @@
     firm = Firm.new
     firm.name = "Next Angle"
     firm.save
-    
+
     next_angle = Company.find(firm.id)
     assert next_angle.kind_of?(Firm), "Next Angle should be a firm"
   end
@@ -139,6 +139,6 @@
         c.save
       end
     
-      def Company.inheritance_column() "ruby_type" end
+      Company.set_inheritance_column("ruby_type")
     end
 end
Index: lib/active_record/associations.rb
===================================================================
--- lib/active_record/associations.rb   (revision 4751)
+++ lib/active_record/associations.rb   (working copy)
@@ -1508,7 +1508,7 @@
               join << %(AND %s.%s = %s ) % [
                 aliased_table_name, 
                 
reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
 
-                klass.quote(klass.name.demodulize)] unless 
klass.descends_from_active_record?
+                klass.quote(klass.name.demodulize)] if 
!klass.descends_from_active_record? and 
reflection.active_record.respond_to?(:inheritance_column)
               join << "AND 
#{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if 
reflection.options[:conditions]
               join
             end
Index: lib/active_record/base.rb
===================================================================
--- lib/active_record/base.rb   (revision 4751)
+++ lib/active_record/base.rb   (working copy)
@@ -207,25 +207,6 @@
   #   user = User.create(:preferences => %w( one two three ))
   #   User.find(user.id).preferences    # raises SerializationTypeMismatch
   #
-  # == Single table inheritance
-  #
-  # Active Record allows inheritance by storing the name of the class in a 
column that by default is called "type" (can be changed
-  # by overwriting <tt>Base.inheritance_column</tt>). This means that an 
inheritance looking like this:
-  #
-  #   class Company < ActiveRecord::Base; end
-  #   class Firm < Company; end
-  #   class Client < Company; end
-  #   class PriorityClient < Client; end
-  #
-  # When you do Firm.create(:name => "37signals"), this record will be saved 
in the companies table with type = "Firm". You can then
-  # fetch this row again using Company.find(:first, "name = '37signals'") and 
it will return a Firm object.
-  #
-  # If you don't have a type column defined in your table, single-table 
inheritance won't be triggered. In that case, it'll work just
-  # like normal subclasses with no special magic for differentiating between 
them or reloading the right type with find.
-  #
-  # Note, all the attributes for all the cases are kept in the same table. 
Read more:
-  # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
-  #
   # == Connection to multiple databases in different models
   #
   # Connections are usually created through 
ActiveRecord::Base.establish_connection and retrieved by 
ActiveRecord::Base.connection.
@@ -625,11 +606,6 @@
         key
       end
 
-      # Defines the column name for use with single table inheritance -- can 
be overridden in subclasses.
-      def inheritance_column
-        "type"
-      end
-
       # Lazy-set the sequence name to the connection's default.  This method
       # is only ever called once since set_sequence_name overrides it.
       def sequence_name #:nodoc:
@@ -669,22 +645,6 @@
       end
       alias :primary_key= :set_primary_key
 
-      # Sets the name of the inheritance column to use to the given value,
-      # or (if the value # is nil or false) to the value returned by the
-      # given block.
-      #
-      # Example:
-      #
-      #   class Project < ActiveRecord::Base
-      #     set_inheritance_column do
-      #       original_inheritance_column + "_id"
-      #     end
-      #   end
-      def set_inheritance_column(value = nil, &block)
-        define_attr_method :inheritance_column, value, &block
-      end
-      alias :inheritance_column= :set_inheritance_column
-
       # Sets the name of the sequence to use when generating ids to the given
       # value, or (if the value is nil or false) to the value returned by the
       # given block. This is required for Oracle and is useful for any
@@ -750,9 +710,8 @@
       end
 
       # Returns an array of column objects where the primary id, all columns 
ending in "_id" or "_count",
-      # and columns used for single table inheritance have been removed.
       def content_columns
-        @content_columns ||= columns.reject { |c| c.primary || c.name =~ 
/(_id|_count)$/ || c.name == inheritance_column }
+        @content_columns ||= columns.reject { |c| c.primary || c.name =~ 
/(_id|_count)$/ }
       end
 
       # Returns a hash of all the methods added to query each of the columns 
in the table with the name of the method as the key
@@ -791,10 +750,6 @@
         attribute_key_name.humanize
       end
 
-      def descends_from_active_record? # :nodoc:
-        superclass == Base || !columns_hash.include?(inheritance_column)
-      end
-
       def quote(value, column = nil) #:nodoc:
         connection.quote(value,column)
       end
@@ -1016,26 +971,10 @@
         # 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)
-          object = 
-            if subclass_name = record[inheritance_column]
-              if subclass_name.empty?
-                allocate
-              else
-                require_association_class(subclass_name)
-                begin
-                  compute_type(subclass_name).allocate
-                rescue NameError
-                  raise SubclassNotFound,
-                    "The single-table inheritance mechanism failed to locate 
the subclass: '#{record[inheritance_column]}'. " +
-                    "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 #{self.to_s}.inheritance_column to use 
another column for that information."
-                end
-              end
-            else
-              allocate
-            end
+          build_attributes(allocate, record)
+        end
 
+        def build_attributes(object, record)
           object.instance_variable_set("@attributes", record)
           object
         end
@@ -1114,24 +1053,19 @@
         # Adds a sanitized version of +conditions+ to the +sql+ string. Note 
that the passed-in +sql+ string is changed.
         # The optional scope argument is for the current :find scope.
         def add_conditions!(sql, conditions, scope = :auto)
+          segments = generate_conditions_segments!(sql, conditions, scope)
+          segments.compact!
+          sql << "WHERE (#{segments.join(") AND (")}) " unless segments.empty?
+        end
+
+        def generate_conditions_segments!(sql, conditions, scope)
           scope = scope(:find) if :auto == scope
           segments = []
           segments << sanitize_sql(scope[:conditions]) if scope && 
scope[:conditions]
           segments << sanitize_sql(conditions) unless conditions.nil?
-          segments << type_condition unless descends_from_active_record?       
 
-          segments.compact!
-          sql << "WHERE (#{segments.join(") AND (")}) " unless segments.empty?
+          segments
         end
 
-        def type_condition
-          quoted_inheritance_column = 
connection.quote_column_name(inheritance_column)
-          type_condition = 
subclasses.inject("#{table_name}.#{quoted_inheritance_column} = 
'#{name.demodulize}' ") do |condition, subclass|
-            condition << "OR #{table_name}.#{quoted_inheritance_column} = 
'#{subclass.name.demodulize}' "
-          end
-
-          " (#{type_condition}) "
-        end
-
         # Guesses the table name, but does not decorate it with prefix and 
suffix information.
         def undecorated_table_name(class_name = base_class.name)
           table_name = Inflector.underscore(Inflector.demodulize(class_name))
@@ -1444,7 +1378,6 @@
       def initialize(attributes = nil)
         @attributes = attributes_from_column_definition
         @new_record = true
-        ensure_proper_type
         self.attributes = attributes unless attributes.nil?
         yield self if block_given?
       end
@@ -1757,17 +1690,6 @@
         id
       end
 
-      # Sets the attribute used for single table inheritance to this class 
name if this is not the ActiveRecord descendent.
-      # Considering the hierarchy Reply < Message < ActiveRecord, this makes 
it possible to do Reply.new without having to
-      # set Reply[Reply.inheritance_column] = "Reply" 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, 
Inflector.demodulize(self.class.name))
-        end
-      end
-
-
       # Allows access to the object attributes, which are held in the 
@attributes hash, as were
       # they first-class methods. So a Person class with a name attribute can 
use Person#name and
       # Person#name= and never directly use the attributes hash -- except for 
multiple assigns with
@@ -1939,7 +1861,7 @@
 
       # The primary key and inheritance column can never be set by 
mass-assignment for security reasons.
       def attributes_protected_by_default
-        default = [ self.class.primary_key, self.class.inheritance_column ]
+        default = [ self.class.primary_key ]
         default << 'id' unless self.class.primary_key.eql? 'id'
         default
       end
Index: lib/active_record/inheritances/single_table.rb
===================================================================
--- lib/active_record/inheritances/single_table.rb      (revision 0)
+++ lib/active_record/inheritances/single_table.rb      (revision 0)
@@ -0,0 +1,116 @@
+module ActiveRecord
+  module Inheritances
+    # == Single table inheritance
+    #
+    # Active Record allows inheritance by storing the name of the class in a 
column that by default is called "type" (can be changed
+    # by overwriting <tt>Base.inheritance_column</tt>). This means that an 
inheritance looking like this:
+    #
+    #   class Company < ActiveRecord::Base; end
+    #   class Firm < Company; end
+    #   class Client < Company; end
+    #   class PriorityClient < Client; end
+    #
+    # When you do Firm.create(:name => "37signals"), this record will be saved 
in the companies table with type = "Firm". You can then
+    # fetch this row again using Company.find(:first, "name = '37signals'") 
and it will return a Firm object.
+    #
+    # If you don't have a type column defined in your table, single-table 
inheritance won't be triggered. In that case, it'll work just
+    # like normal subclasses with no special magic for differentiating between 
them or reloading the right type with find.
+    #
+    # Note, all the attributes for all the cases are kept in the same table. 
Read more:
+    # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+    #
+    module SingleTable
+
+      def self.included(base)
+        base.reset_column_information
+        base.extend(ClassMethods)
+        base.class_eval do
+          class << self
+            [:instantiate, :content_columns, 
:generate_conditions_segments!].each { |m| alias_method_chain m, :sti }
+          end
+          [:initialize, :attributes_protected_by_default].each { |m| 
alias_method_chain m, :sti }
+        end
+      end
+
+      # This method determines wheter to load or not the inheritance module.
+      def self.load?(base)
+        base.column_names.include?(base.inheritance_column(self))
+      end
+
+      # Defines the default inheritance column to be used
+      def self.default_column
+        "type"
+      end
+
+      module ClassMethods
+        
+        def sti_column
+          inheritance_column(SingleTable)
+        end
+
+        def instantiate_with_sti(record)
+          subclass_name = record[sti_column]
+          if subclass_name.blank?
+            instantiate_without_sti(record)
+          else
+            instantiate_subclass(subclass_name, record)
+          end
+        end
+
+        def instantiate_subclass(subclass, record)
+          require_association_class(subclass)
+          begin
+            build_attributes(compute_type(subclass).allocate, record)
+          rescue NameError
+            raise SubclassNotFound,
+              "The single-table inheritance mechanism failed to locate the 
subclass: '#{record[sti_column]}'. " +
+              "This error is raised because the column '#{sti_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 #{self.to_s}.inheritance_column to use another 
column for that information."
+          end
+        end
+
+        def content_columns_with_sti #:nodoc
+          @content_columns ||= content_columns_without_sti.reject { |c| c.name 
== sti_column }
+        end
+
+        def generate_conditions_segments_with_sti!(*args)
+          segments = generate_conditions_segments_without_sti!(*args)
+          segments << type_condition unless superclass == Base
+          segments
+        end
+
+        def type_condition #:nodoc:
+          quoted_inheritance_column = connection.quote_column_name(sti_column)
+          subclasses.inject("#{table_name}.#{quoted_inheritance_column} = 
'#{name.demodulize}' ") do |condition, subclass|
+            condition << "OR #{table_name}.#{quoted_inheritance_column} = 
'#{subclass.name.demodulize}' "
+          end
+        end
+
+      end
+
+      def initialize_with_sti(*args, &block) #:nodoc:
+        result = initialize_without_sti(*args, &block)
+        ensure_proper_type
+        result
+      end
+
+      # Sets the attribute used for single table inheritance to this class 
name if this is not the ActiveRecord descendent.
+      # Considering the hierarchy Reply < Message < ActiveRecord, this makes 
it possible to do Reply.new without having to
+      # set Reply[Reply.inheritance_column] = "Reply" yourself. No such 
attribute would be set for objects of the
+      # Message class in that example.
+      def ensure_proper_type
+        unless self.class.superclass == Base
+          write_attribute(self.class.sti_column, 
Inflector.demodulize(self.class.name))
+        end
+      end
+
+      def attributes_protected_by_default_with_sti #:nodoc:
+        default = attributes_protected_by_default_without_sti
+        default << self.class.sti_column
+        default
+      end  
+      
+    end
+  end
+end
Index: lib/active_record/inheritances/class_table.rb
===================================================================
--- lib/active_record/inheritances/class_table.rb       (revision 0)
+++ lib/active_record/inheritances/class_table.rb       (revision 0)
@@ -0,0 +1,16 @@
+module ActiveRecord
+  module Inheritances
+    module ClassTable
+      def self.included(base)
+        base.extend(ClassMethods)
+      end
+
+      def self.load?(base)
+        false
+      end
+    
+      module ClassMethods
+      end
+    end
+  end
+end
Index: lib/active_record/inheritances.rb
===================================================================
--- lib/active_record/inheritances.rb   (revision 0)
+++ lib/active_record/inheritances.rb   (revision 0)
@@ -0,0 +1,104 @@
+require 'active_record/inheritances/single_table'
+
+module ActiveRecord
+  module Inheritances #:nodoc:
+    def self.included(base)
+      base.extend(ClassMethods)
+      base.class_eval do 
+        class << self
+          alias_method_chain :inherited, :inheritance
+        end
+        register_inheritances!
+      end
+    end
+
+    class UnexisistingInheritance < StandardError
+      def initialize(inheritance)
+        super("#{inheritance.name} is not a valid inheritance.")
+      end
+    end
+
+    module ClassMethods
+
+      # Extends Base.inherited to automatically load inheritances.
+      def inherited_with_inheritance(child) #:nodoc:
+        inherited_without_inheritance(child)
+        load_inheritances! if table_exists? rescue nil 
+      end
+
+      # Register all available inheritances.
+      def register_inheritances! #:nodoc:
+        ::ActiveRecord::Inheritances.constants.each do |const_name|
+          const = ::ActiveRecord::Inheritances.const_get(const_name)
+          next unless const.respond_to?(:load?)
+          write_inheritable_array(:inheritances, Array(const))
+        end
+      end
+
+      def inheritances
+        read_inheritable_attribute(:inheritances) || []
+      end
+
+      def propagate_inheritable_attribute(attribute)
+        subclasses.each do |subclass|
+          yield(subclass) unless send(attribute) == subclass.send(attribute)
+        end
+      end
+
+      # Loaded inheritances
+      def inheritance_loaded?(inheritance) #:nodoc:
+        loaded_inheritances.include?(inheritance)
+      end
+
+      def loaded_inheritances
+        read_inheritable_attribute(:loaded_inheritances) || []
+      end
+    
+      def load_inheritances! #:nodoc:
+        inheritances.each do |inheritance|
+          if not inheritance_loaded?(inheritance) and inheritance.load?(self)
+            include inheritance
+            write_inheritable_array(:loaded_inheritances, Array(inheritance))
+            propagate_inheritable_attribute(:loaded_inheritances) do 
|subclass| 
+              subclass.write_inheritable_array(:loaded_inheritances, 
Array(inheritance))
+            end
+          end
+        end
+      end
+
+      # Inheritance columns
+      def inheritance_columns
+        read_inheritable_attribute(:inheritance_columns) || {}
+      end
+      
+      def inheritance_column(inheritance=SingleTable)
+        inheritance_columns[inheritance] || inheritance.default_column
+      end
+
+      def set_inheritance_column_for(column=nil, inheritance=SingleTable)
+        column = yield inheritance_column(inheritance) if block_given?
+        raise ArgumentError if column.nil?
+        raise UnexisistingInheritance.new(inheritance) unless 
inheritances.include?(inheritance)
+        write_inheritable_hash(:inheritance_columns, {inheritance => column})
+        propagate_inheritable_attribute(:inheritance_columns) do |subclass| 
+          subclass.set_inheritance_column_for(column, inheritance)
+        end
+      end
+
+      def set_inheritance_columns(opts={}, &block)
+        if opts.is_a?(Hash) && !opts.empty?
+          opts.each { |inheritance, column| set_inheritance_column_for(column, 
inheritance, &block) }
+        else
+          set_inheritance_column_for(opts, &block)
+        end
+      end
+      alias_method :inheritance_column=, :set_inheritance_columns
+      alias_method :set_inheritance_column, :set_inheritance_columns
+
+
+      def descends_from_active_record?
+        superclass == Base || !inheritance_loaded?(SingleTable)
+      end
+    end
+  end
+end
Index: lib/active_record.rb
===================================================================
--- lib/active_record.rb        (revision 4751)
+++ lib/active_record.rb        (working copy)
@@ -52,6 +52,7 @@
 require 'active_record/schema'
 require 'active_record/calculations'
 require 'active_record/xml_serialization'
+require 'active_record/inheritances'
 require 'active_record/attribute_methods'
 
 ActiveRecord::Base.class_eval do
@@ -70,6 +71,7 @@
   include ActiveRecord::Acts::NestedSet
   include ActiveRecord::Calculations
   include ActiveRecord::XmlSerialization
+  include ActiveRecord::Inheritances
   include ActiveRecord::AttributeMethods
 end
 
_______________________________________________
Rails-core mailing list
Rails-core@lists.rubyonrails.org
http://lists.rubyonrails.org/mailman/listinfo/rails-core

Reply via email to