Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added customizable STI type serialization. #7541

Closed
wants to merge 1 commit into from

6 participants

@pivotalcommon

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.

This is primarily useful for working with legacy data models.

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

  self.inheritance_deserializer = ->(type_before_cast) do
    # Map the type identifier back into the appropriate class.
    # Defaults (approximately) to `type_before_cast.constantize`.
    case type_before_cast.to_i
    when 1
      Child1
    when 2
      Child2
    end
  end
end

class Child1 < Foo; end
class Child2 < Foo; end
Ian Lesperance & Matt Parker 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.
d3bfcba
@al2o3cr

Not sure what the best API would be, but some way to ask the class what name actually got written would be handy. For instance, you may need that string for SQL conditions, etc.

@pivotalcommon

Hi @al2o3cr - that actually exists already, it's called "sti_name", and it's a public method (MyModel.sti_name). It's been part of Rails for a while, just never really been documented to my knowledge.

@pivotalcommon

Also, the value will still be stored in record[Model.inheritance_column], so you can continue to access it that way.

@pivotalcommon

any word on whether or not this pull request might get accepted?

@steveklabnik
Collaborator

It needs rebased, for one. I don't use STI, so I can't comment on that...

@pixeltrix
Owner

I have to say I'm :-1: on this - I'm pretty sure that there's some association code that assumes that the stored value is a class name (though it may have been refactored away at some point by @jonleighton) and without some more tests around that area I wouldn't be confident of it working correctly.

I feel your legacy pain but I think in this case it isn't worth the maintenance cost of including it.

@al2o3cr

@pixeltrix - most of the code that needs to use an STI type column uses sti_name, as that's technically required for things to work correctly for both settings of store_full_sti_class. There are two places I could find (on a rough search) that still make assumptions:

At a minimum, the two points noted above should be fixed, so that people who want to override the existing behavior from outside (by, for instance, patching sti_name and find_sti_class) won't have problems.

@jonleighton
Collaborator

I am very -1 on this API. I think we should just make sti_name a publicly documented method and allow users to reimplement it to their needs as necessary. And fix the places where sti_name is not called but should be. Does that address the problems this PR is trying to solve?

@frodsan

@jonleighton any decision on this?

@steveklabnik
Collaborator

We have two core people with a :-1:, and no answer in a month from @pivotalcommon. So I'm closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 6, 2012
  1. @elliterate

    Added customizable STI type serialization.

    Ian Lesperance & Matt Parker authored elliterate committed
    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.
This page is out of date. Refresh to see the latest.
View
4 activerecord/CHANGELOG.md
@@ -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.
View
59 activerecord/lib/active_record/inheritance.rb
@@ -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
View
6 activerecord/lib/active_record/model.rb
@@ -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:
View
13 activerecord/test/cases/inheritance_test.rb
@@ -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
View
15 activerecord/test/models/dog.rb
@@ -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
View
1  activerecord/test/schema/schema.rb
@@ -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|
Something went wrong with that request. Please try again.