19

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.

arun
  • 193
  • 1
  • 1
  • 4
  • Have you tried anything so far? – xlembouras Apr 25 '14 at 12:47
  • 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! – arun Apr 26 '14 at 07:12

6 Answers6

24

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

VAIRIX
  • 691
  • 5
  • 7
  • 3
    shouldn't the `find_sti_class` override go to the base class? – artm Sep 22 '14 at 15:31
  • 1
    That's where I normally put it. – James Pearson Apr 18 '15 at 19:10
  • 3
    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 Sep 30 '15 at 22:48
  • @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` – Perennialista Jul 11 '19 at 17:27
4

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

Brian Underwood
  • 10,746
  • 1
  • 22
  • 34
3

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.

inopinatus
  • 3,597
  • 1
  • 26
  • 38
1

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

Community
  • 1
  • 1
vvohra87
  • 5,594
  • 4
  • 22
  • 34
  • 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. – arun Apr 26 '14 at 07:18
1

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.

artm
  • 3,559
  • 1
  • 26
  • 36
0

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
Alexandr
  • 1,704
  • 1
  • 14
  • 20