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
[email protected]
http://lists.rubyonrails.org/mailman/listinfo/rails-core