Permalink
Browse files

Added customizable STI type serialization.

Rails will store the name of a child class in the database
to use for re-instantiating the record as the correct class later.
By specifying an `inheritance_serializer` and
`inheritance_deserializer`, you can customize the stored identifier.

    class Foo < ActiveRecord::Base
      self.inheritance_serializer = ->(klass) do
        # Map the class to the appropriate type identifier.
        # Defaults to `klass.name`.
      end

      self.inheritance_deserializer = ->(type_before_cast) do
        # Map the type identifier back into the appropriate class.
        # Defaults (approximately) to `type_before_cast.constantize`.
      end
    end

This is primarily useful for working with legacy data models.
  • Loading branch information...
Ian Lesperance & Matt Parker authored and elliterate committed Sep 5, 2012
1 parent 2ed325a commit d3bfcba3d3759416b44b8aa1762cdaf1faf3f3db
@@ -1,5 +1,9 @@
## Rails 4.0.0 (unreleased) ##
+* Allow customization of STI type serialization/deserialization for legacy databases.
+
+ *Ian Lesperance & Matthew Kane Parker*
+
* Fix bug when call `store_accessor` multiple times.
Fixes #7532.
@@ -85,7 +85,11 @@ def abstract_class?
end
def sti_name
- store_full_sti_class ? name : name.demodulize
+ if inheritance_serializer.present?
+ inheritance_serializer.call(self)
+ else
+ store_full_sti_class ? name : name.demodulize
+ end
end
# Finder methods must instantiate through this method to work with the
@@ -105,6 +109,57 @@ def active_record_super #:nodoc:
superclass < Model ? superclass : Model
end
+ # Allows you to customize the type value stored when using STI. (By default, Rails
+ # will store the class name.)
+ #
+ # class MyModel < ActiveRecord::Base
+ # self.inheritance_serializer = ->(klass) do
+ # if klass == Child1
+ # 1
+ # elsif klass == Child2
+ # 2
+ # end
+ # end
+ # end
+ #
+ # class Child1 < MyModel; end
+ # class Child2 < MyModel; end
+ #
+ # If using this, you will probably also need to implement an inheritance_deserializer.
+ def inheritance_serializer=(serializer)
+ @inheritance_serializer = serializer
+ end
+
+ def inheritance_serializer
+ (@inheritance_serializer ||= nil) || active_record_super.inheritance_serializer
+ end
+
+ # Allows you to customize the class lookup when instantiating an STI record. (By
+ # default, Rails assumes the stored type is a class name.)
+ #
+ # class MyModel < ActiveRecord::Base
+ # self.inheritance_deserializer = ->(type_before_cast) do
+ # case type_before_cast.to_i
+ # when 1
+ # Child1
+ # when 2
+ # Child2
+ # end
+ # end
+ # end
+ #
+ # class Child1 < MyModel; end
+ # class Child2 < MyModel; end
+ #
+ # If using this, you will probably also need to implement an inheritance_serializer.
+ def inheritance_deserializer=(deserializer)
+ @inheritance_deserializer = deserializer
+ end
+
+ def inheritance_deserializer
+ (@inheritance_deserializer ||= nil) || active_record_super.inheritance_deserializer
+ end
+
protected
# Returns the class type of the record using the current module as a prefix. So descendants of
@@ -139,6 +194,8 @@ def compute_type(type_name)
def find_sti_class(type_name)
if type_name.blank? || !columns_hash.include?(inheritance_column)
self
+ elsif inheritance_deserializer.present?
+ inheritance_deserializer.call(type_name)
else
begin
if store_full_sti_class
@@ -107,6 +107,12 @@ def abstract_class?
def inheritance_column
'type'
end
+
+ def inheritance_serializer
+ end
+
+ def inheritance_deserializer
+ end
end
class DeprecationProxy < BasicObject #:nodoc:
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/company'
+require 'models/dog'
require 'models/person'
require 'models/post'
require 'models/project'
@@ -269,8 +270,18 @@ def test_inheritance_without_mapping
assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
end
-end
+ def test_inheritance_with_custom_type_serialization
+ labrador = Labrador.create
+ assert_kind_of Labrador, Dog.find(labrador.id)
+
+ retriever = Retriever.create
+ assert_kind_of Retriever, Dog.find(retriever.id)
+
+ dog = Dog.create
+ assert_kind_of Dog, Dog.find(dog.id)
+ end
+end
class InheritanceComputeTypeTest < ActiveRecord::TestCase
fixtures :companies
@@ -1,4 +1,19 @@
class Dog < ActiveRecord::Base
+ INHERITANCE_TYPE_MAP = {
+ 2 => 'Labrador',
+ 3 => 'Retriever'
+ }
+
+ self.inheritance_column = 'dog_type'
+ self.inheritance_serializer = ->(klass) { INHERITANCE_TYPE_MAP.invert[klass.name] }
+ self.inheritance_deserializer = ->(type_before_cast) { INHERITANCE_TYPE_MAP[type_before_cast.to_i].constantize }
+
belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count
belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count
end
+
+class Labrador < Dog
+end
+
+class Retriever < Dog
+end
@@ -235,6 +235,7 @@ def create_table(*args, &block)
create_table :dogs, :force => true do |t|
t.integer :trainer_id
t.integer :breeder_id
+ t.integer :dog_type
end
create_table :edges, :force => true, :id => false do |t|

0 comments on commit d3bfcba

Please sign in to comment.