Search code examples
ruby-on-railsruby-on-rails-4activerecordrakerake-task

Invalid single-table inheritance type with rake task and custom STI names


I made a big work refactoring tonnes of code, and also made big changes to db schema. And now I am trying to write a rake task to migrate records from old tables to a new one.

I am having such classes:

#app/models/restream/service.rb
class Restream::Service < ActiveRecord::Base
def self.types
    %w(custom multiple_destinations_service one_destination_service) +
    Restream::Custom.types + Restream::MultipleDestinationsService.types
  end

  def self.find_sti_class(type_name) #allows to find classes by short names
    type_name = "Restream::#{type_name.camelcase}".constantize
    super
  end
end

#app/models/restream/custom.rb
class Restream::Custom < Restream::Service
  def self.sti_name
    "custom"
  end

  def self.types
    %w(periscope odnoklassniki vkontakte)
  end
end

#app/models/restream/periscope.rb
class Restream::Periscope < Restream::Custom
  def self.sti_name
    "periscope"
  end
end

Everything works just fine. Until I'm trying to add records manually. In my previous version I had such a structure:

class Restream::Custom < ActiveRecord::Base
  def self.types; %w(custom periscope vkontakte); end
end

class Restream::Periscope < Restream::Custom
  def self.sti_name; 'periscope'; end
end

And now I'm trying simply get all records from old restream_custom table and just copy type. Roughly:

Restream::Custom.create(type: old_restream_custom.type)

And that fails saying:

ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: periscope is not a subclass of Restream::Custom

It's obviously not! But anyway I already have a bunch of records with type: 'periscope', so that I know it's a valid value. What's the reason for this, and how can I fix this behaviour?

======

I can see two ways:

1) Set type to Restream::Periscope, not just a periscope. But that creates records, that can't be found by Restream::Periscope.find_each or Restream::Custom.find_each or smth like that, 'cause it will search for records with periscope in its type column, not a Restream::Periscope.

2) Select from restream_custom table only records with each types of custom, periscope, etc. and create Restream::Periscope for periscopes, not Restream::Custom and trying to provide a correct type here. But I found it kind of unpretty, not-DRY and unnecessary, and wonder if I can do smth more beautiful with it.


Solution

  • If it is not too big of a refactor, I would go with your first option 1) to set type to Restream::Periscope and not periscope primarily just because it is the Rails convention.

    If option 1) is implemented, you said your other concern about this which is that Restream::Periscope.find_each will no longer return records of the other "types" and will be automatically filtered accordingly depending on the subclass... does makes sense because your .find_each is querying upon Restream::Periscope, and thus it is intuitive that I will be expecting that all of the returned records will be of type "Restream::Periscope".

    Now if you'd like to query for "all types", then you may just query on the "parent" class which is the Restream::Service, in which you can now do the following:

    Restream::Service.find_each(type: old_restream_custom.type)
    # or
    Restream::Service.create(type: old_restream_custom.type)
    

    This is my suggestion, unless of course it is really a big task refactoring all of your code. Hope this somehow helps.