Skip to content
This repository

Added customizable STI type serialization. #7541

Closed
wants to merge 1 commit into from

6 participants

Pivotal Labs Common Effort Role Account Matt Jones Steve Klabnik Andrew White Jon Leighton Francesco Rodríguez
Pivotal Labs Common Effort Role Account

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
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
Matt Jones

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.

Pivotal Labs Common Effort Role Account

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.

Pivotal Labs Common Effort Role Account

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

Pivotal Labs Common Effort Role Account

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

Steve Klabnik
Collaborator

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

Andrew White
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.

Matt Jones

@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.

Jon Leighton

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?

Francesco Rodríguez

@jonleighton any decision on this?

Steve Klabnik
Collaborator

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

Steve Klabnik steveklabnik closed this October 26, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 2 authors.

Sep 06, 2012
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
This page is out of date. Refresh to see the latest.
4  activerecord/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,9 @@
1 1
 ## Rails 4.0.0 (unreleased) ##
2 2
 
  3
+*   Allow customization of STI type serialization/deserialization for legacy databases.
  4
+
  5
+    *Ian Lesperance & Matthew Kane Parker*
  6
+
3 7
 *   Fix bug when call `store_accessor` multiple times.
4 8
     Fixes #7532.
5 9
 
59  activerecord/lib/active_record/inheritance.rb
@@ -85,7 +85,11 @@ def abstract_class?
85 85
       end
86 86
 
87 87
       def sti_name
88  
-        store_full_sti_class ? name : name.demodulize
  88
+        if inheritance_serializer.present?
  89
+          inheritance_serializer.call(self)
  90
+        else
  91
+          store_full_sti_class ? name : name.demodulize
  92
+        end
89 93
       end
90 94
 
91 95
       # Finder methods must instantiate through this method to work with the
@@ -105,6 +109,57 @@ def active_record_super #:nodoc:
105 109
         superclass < Model ? superclass : Model
106 110
       end
107 111
 
  112
+      # Allows you to customize the type value stored when using STI. (By default, Rails
  113
+      # will store the class name.)
  114
+      #
  115
+      #     class MyModel < ActiveRecord::Base
  116
+      #       self.inheritance_serializer = ->(klass) do
  117
+      #         if klass == Child1
  118
+      #           1
  119
+      #         elsif klass == Child2
  120
+      #           2
  121
+      #         end
  122
+      #       end
  123
+      #     end
  124
+      #
  125
+      #     class Child1 < MyModel; end
  126
+      #     class Child2 < MyModel; end
  127
+      #
  128
+      # If using this, you will probably also need to implement an inheritance_deserializer.
  129
+      def inheritance_serializer=(serializer)
  130
+        @inheritance_serializer = serializer
  131
+      end
  132
+
  133
+      def inheritance_serializer
  134
+        (@inheritance_serializer ||= nil) || active_record_super.inheritance_serializer
  135
+      end
  136
+
  137
+      # Allows you to customize the class lookup when instantiating an STI record. (By
  138
+      # default, Rails assumes the stored type is a class name.)
  139
+      #
  140
+      #     class MyModel < ActiveRecord::Base
  141
+      #       self.inheritance_deserializer = ->(type_before_cast) do
  142
+      #         case type_before_cast.to_i
  143
+      #         when 1
  144
+      #           Child1
  145
+      #         when 2
  146
+      #           Child2
  147
+      #         end
  148
+      #       end
  149
+      #     end
  150
+      #
  151
+      #     class Child1 < MyModel; end
  152
+      #     class Child2 < MyModel; end
  153
+      #
  154
+      # If using this, you will probably also need to implement an inheritance_serializer.
  155
+      def inheritance_deserializer=(deserializer)
  156
+        @inheritance_deserializer = deserializer
  157
+      end
  158
+
  159
+      def inheritance_deserializer
  160
+        (@inheritance_deserializer ||= nil) || active_record_super.inheritance_deserializer
  161
+      end
  162
+
108 163
       protected
109 164
 
110 165
       # 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)
139 194
       def find_sti_class(type_name)
140 195
         if type_name.blank? || !columns_hash.include?(inheritance_column)
141 196
           self
  197
+        elsif inheritance_deserializer.present?
  198
+          inheritance_deserializer.call(type_name)
142 199
         else
143 200
           begin
144 201
             if store_full_sti_class
6  activerecord/lib/active_record/model.rb
@@ -107,6 +107,12 @@ def abstract_class?
107 107
       def inheritance_column
108 108
         'type'
109 109
       end
  110
+
  111
+      def inheritance_serializer
  112
+      end
  113
+
  114
+      def inheritance_deserializer
  115
+      end
110 116
     end
111 117
 
112 118
     class DeprecationProxy < BasicObject #:nodoc:
13  activerecord/test/cases/inheritance_test.rb
... ...
@@ -1,5 +1,6 @@
1 1
 require "cases/helper"
2 2
 require 'models/company'
  3
+require 'models/dog'
3 4
 require 'models/person'
4 5
 require 'models/post'
5 6
 require 'models/project'
@@ -269,8 +270,18 @@ def test_inheritance_without_mapping
269 270
     assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
270 271
     assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
271 272
   end
272  
-end
273 273
 
  274
+  def test_inheritance_with_custom_type_serialization
  275
+    labrador = Labrador.create
  276
+    assert_kind_of Labrador, Dog.find(labrador.id)
  277
+
  278
+    retriever = Retriever.create
  279
+    assert_kind_of Retriever, Dog.find(retriever.id)
  280
+
  281
+    dog = Dog.create
  282
+    assert_kind_of Dog, Dog.find(dog.id)
  283
+  end
  284
+end
274 285
 
275 286
 class InheritanceComputeTypeTest < ActiveRecord::TestCase
276 287
   fixtures :companies
15  activerecord/test/models/dog.rb
... ...
@@ -1,4 +1,19 @@
1 1
 class Dog < ActiveRecord::Base
  2
+  INHERITANCE_TYPE_MAP = {
  3
+    2 => 'Labrador',
  4
+    3 => 'Retriever'
  5
+  }
  6
+
  7
+  self.inheritance_column = 'dog_type'
  8
+  self.inheritance_serializer = ->(klass) { INHERITANCE_TYPE_MAP.invert[klass.name] }
  9
+  self.inheritance_deserializer = ->(type_before_cast) { INHERITANCE_TYPE_MAP[type_before_cast.to_i].constantize }
  10
+
2 11
   belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count
3 12
   belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count
4 13
 end
  14
+
  15
+class Labrador < Dog
  16
+end
  17
+
  18
+class Retriever < Dog
  19
+end
1  activerecord/test/schema/schema.rb
@@ -235,6 +235,7 @@ def create_table(*args, &block)
235 235
   create_table :dogs, :force => true do |t|
236 236
     t.integer :trainer_id
237 237
     t.integer :breeder_id
  238
+    t.integer :dog_type
238 239
   end
239 240
 
240 241
   create_table :edges, :force => true, :id => false do |t|
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.