Rails STI: How to change mapping between class name & value of the 'type' column

rails sti change type
single table inheritance rails guide
rails abstract class
rails sti example
rails sti abstract base class
rails sti validate type
rails sti namespace
store_full_sti_class

Because of company rules I can't use our domain class names; I am going to use an analogy instead. I have a table called projects which has a column called 'type' with possible values as 'indoor' & 'outdoor'. Records having indoor and outdoor have clear functionality separation and would fit pretty neatly as a STI implementation. Unfortunately I can't change the type-names and can't add classes inside the global namespace. Is there a way to specify a different value for 'type'?

Note: I am not trying to use a different column name instead of 'type' for STI. I am looking to have a different value for type, other than my class name.

You can try something like this:

class Proyect < ActiveRecord::Base
end

Then the Indoor class but with Other name

class Other < Proyect
  class << self
    def find_sti_class(type_name)
      type_name = self.name
      super
    end

    def sti_name
      "Indoor"
    end
  end
end

The same apply for Outdoor class. You can check sti_name in http://apidock.com/rails/ActiveRecord/Base/find_sti_class/class

Use custom type name with Ruby on Rails STI mechanism., Rails STI: How to change mapping between class name , Due to company rules, I can not use the names of our domain class; I'm going to use an analogy  Stop Rails from using the type column to infer the model class to instantiate, allowing the base class (Step) to be instantiated. Use the type and id fields to fake a belongs_to association on Step, making the row appear to be two objects. Stop content types inheriting from the base class and set their table_name by hand.

In Rails 5, the Attributes API allows us to change the serialisation of any column, including the type column of STI models, thus:

# lib/my_sti_type.rb
class MyStiType < ActiveRecord::Type::String
  def cast_value(value)
    case value
    when 'indoor'; 'App::CarpetProject'
    when 'outdoor'; 'App::LawnProject'
    else super
    end
  end

  def serialize(value)
    case value
    when 'App::CarpetProject'; 'indoor'
    when 'App::LawnProject'; 'outdoor'
    else super
    end
  end

  def changed_in_place?(original_value_for_database, value)
    original_value_for_database != serialize(value)
  end
end

# config/initializers/types.rb
require 'my_sti_type'
ActiveRecord::Type.register(:my_sti_type, MyStiType)

# app/models/project.rb
class Project < ActiveRecord::Base
  attribute :type, :my_sti_type
end

Substitute your own class names and string matching/manipulation as required. See this commit for an example.

The same mechanism also works for the attributename_type column of a polymorphic belongs_to association.

The Ruby on Rails 3 Tutorial and Reference Collection, Because of company rules I can't use our domain class names; I am going to use an analogy instead. I have a table called projects which has a  ActiveRecord::Reflection::AbstractReflection#class_name class_name (table_name = table_name) public Turns the table_name back into a class name following the reverse rules of table_name .

In looking at the source code there seems to be a store_full_sti_class option (I don't know when it was introduced):

config.active_record.store_full_sti_class = false

That gets rid of the namespacing modules for me in Rails 4.2.7

TIL: You can use an ActiveRecord's Enum with Single Table , Let's say you want to modify default type column Rails uses to identify class -> subclass inheritance type_name = self.name super end def sti_name '​Administrator' end end Often one of those parts is a timestamp or a counter, some value that will change when the data that ActiveSupport::TimeZone.all.​map(&:name). Browse other questions tagged ruby-on-rails ruby-on-rails-3 has-many single-table-inheritance sti or ask your own question. The Overflow Blog Podcast 241: New tools for new times

This is possible but a little convoluted. Essentially when you save a record of an inherited class, the method moves up to the parent class with an added 'type' value.

The standard definition:

class Vehicle < ActiveRecord::Base
  ..
  def type=(sType)
  end
  ..
end

class Truck < Vehicle
  # calling save here will trigger a save on a vehicle object with type=Truck
end

Changing this is precarious at best in my opinion. You'll likely run into other issues.

I recently discovered AR method becomes which should allow you to morph children objects into parent objects or as the documentation suggests parents into children, you might have some luck with that.

vehicle = Vehicle.new
vehicle = vehicle.becomes(Truck) # convert to instance from Truck to Vehicle
vehicle.type = "Truck"
vehicle.save!

Not used this myself, but simply, you should be able to change the type column value before saving rather easily. This will likely cause a few problems with the inbuilt Active Record query interface and associations.

Also, you can do something like:

class Truck
 ..
 def self.model_name
  "MyNewClassName"
 end
 ..
end

But with this approach beware that the rest of rails, including routes and controllers will refer to the model as "MyNewClassName" and not "Truck".

PS: If you can't add classes inside your Global Namespace then why not add them inside another namespace? The parent and child can belong to different namespaces or can both belong to a unique namespace (not the global).

The Rails 4 Way, To continue explaining single-table inheritance, let's turn back to our example of a to write a pair of class and instance methods on the Timesheet class: class Timesheet not paid? billable_weeks.map(&:total_hours).sum else 0 end end That latest change is a clear The word “type” is a very common column name and. 4 Change text colour with bootstrap-vue navbar item-dropdown Aug 17 '19 3 Ruby on Rails, including a module with arguments Jun 16 '18 3 Rails STI: How to change mapping between class name & value of the 'type' column Jun 19 '18

If all that is required is stripping module names from the type field, the following helper module, based on VAIRIX's answer, can be used:

# lib/util/namespaceless_sti.rb

module NamespacelessSti
  def find_sti_class(type_name)
    type_name = self.name
    super
  end

  def sti_name
    self.name.sub(/^.*:/,"")
  end
end

This module has to be extended into every STI base class, e.g. Project from OP's description (imagining models live in Acme namespace/module)

# app/models/acme/project.rb

require "util/namespaceless_sti"

module Acme
  class Project < ActiveRecord::Base
    extend NamespacelessSti
  end
end

# app/models/acme/indoor.rb

module Acme
  class Indoor < Project
    # nothing special needs to be done here
  end
end

This makes sure Acme::Indoor records in projects table will have "Indoor" in their type field.

Active Record Basics, TIL: You can use an ActiveRecord's Enum with Single Table Inheritance to make the Among the worst is that type column with string class names. Changing class names is difficult because you need to update every row Use the Hash declaration (like in my example) instead to ensure the mapping doesn't change. If you need to change the mapping of a field, create a new index with the correct mapping and reindex your data into that index. Renaming a field would invalidate data already indexed under the old field name. Instead, add an alias field to create an alternate field name. View the mapping of an index edit

Active Record Associations, (STI). A lot of applications start out with a User model of some sort. Over time, as Admin and Guest classes are introduced as subclasses of User. a type column that will hold the name of the class to be instantiated for a given row. not paid? billable_weeks.map(&:total_hours).sum 0 1 2 3 4 else 5 That latest change is a  STI in Rails presumes that there is one “super-class” that takes on the name of the schema table and inherits from ActiveRecord::Base. In my example, this is the “Sport” class. Here is where I keep any variables or methods that relate to sports generally - e.g., the API call requests, for example.

Refactoring our Rails app out of single-table inheritance, What Object Relational Mapping and Active Record are and how they are When using class names composed of two or more words, the model class keyword used to designate a table using Single Table Inheritance (STI). def change. No documentation. This method has no description. You can help the Ruby on Rails community by adding new notes.

Single Table Inheritance (STI) class CreateBooks < ActiveRecord::Migration[​6.0] def change create_table :authors do |t| t.string :name t.timestamps end The name of the other model is pluralized when declaring a has_many association. class Post belongs_to :postable, polymorphic: trueend class Person has_many :posts, as :postableend class Group has_many :posts, as :postableend. The Rails convention for naming a polymorphic association uses “-able” with the class name (:postable for the Post class). This makes it clear in your relationships which class is polymorphic.

Comments
  • Have you tried anything so far?
  • No I din't try anything. I couldn't get any good google result which would give me a hint of the direction which I can take. The solution provided by @VAIRIX works perfectly though!
  • shouldn't the find_sti_class override go to the base class?
  • That's where I normally put it.
  • Take note that is you use this approach, objects found through the base class will be instantiated as base class, not as subclass. For example, Proyect.new(type: 'Other') will return an object of type Proyect with its attribute :type set to 'Other'. Whereas default behavior would be to return an object already cast as type Other.
  • @LikeMaBell you are right, you basically cannot create records with type 'Indoor' by instantiating the base class. I have overridden the two methods find_sti_class and sti_name in the base class and the child classes but I cannot create a record with: Proyect.new(type: 'Indoor') because I get: ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Indoor is not a subclass of Proyect while at the same time Proyect.new(type: 'Other').save! returns ActiveRecord::RecordInvalid: Validation failed: Type Other is not a valid type
  • I have my code inside a namespace. By default the type that gets set in the db becomes 'namespace::Classname'. What I meant when I said I can't add my class to a global namespace was that just because I 'have' to retain the type value as 'Indoor' I don't want to move my class to the top namespace.