[Rails-core] Inheritance rework
Rodrigo Kochenburger
divoxx at gmail.com
Fri Aug 11 22:06:19 GMT 2006
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
-------------- next part --------------
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
More information about the Rails-core
mailing list