Rails STI: How to change mapping between class name & value of the 'type' column
single table inheritance rails guide
rails abstract class
rails sti example
rails sti abstract base class
rails sti validate type
rails sti namespace
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
# 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
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.
- 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_classoverride 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
sti_namein 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 Proyectwhile at the same time
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.