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
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).
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.
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
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.
In Rails 5.1.4 the solution of @VAIRIX not worked for me, because I had inheritance_column of integer type and Rails find_sti_class have extra cast at first line:
type_name = base_class.type_for_attribute(inheritance_column).cast(type_name)
I faced with cast issue. Even if I prepared type_name to proper class name. The cast required integer and changing any string to 0. That is why I extended #compute_type instead and set store_full_sti_class to false:
class Parent < ApplicationRecord
self.table_name = 'PolyTable'
self.inheritance_column = 'RecordType'
self.store_full_sti_class = false
class << self
def compute_type(type_name)
type_name = case type_name.to_s
when "4"
"ChildOfType4"
...
else
type_name
end
super
end
end
end
来源:https://stackoverflow.com/questions/23293177/rails-sti-how-to-change-mapping-between-class-name-value-of-the-type-column