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