Вопрос

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

Другие советы

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.

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 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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top