Search code examples
ruby-on-railspostgresqlpolymorphismrails-migrations

Rails 5 with PostgreSQL change table relation from polymorphic to standard has_one/belongs_to association


Rails 5 with PostgreSQL change table relation from polymorphic to standard has_one/belongs_to association.

I have a table, Car, that has an unnecessary polymorphic association with table Key. As a result of polymorphic being true, car.key works and key.car fails. I am trying to change that relationship back to has_one/belongs_to as the relationship is immutable and has no need for polymorphism.

I suppose I could just drop the table and redefine it, but then there is the matter of restoring the data and all the relationships.

I've built a migration that runs nicely and, for my thought, should resolve the problem. The migration is run and then polymorphism is removed from the Car model. Rails seems to be happy with this. PostgreSQL is not so much. It still considers the relationship to be polymorphic and complains that the key_type field is missing. See following for code and simplified examples.

There must be some trigger, procedure, or constraint external to the schema that is causing this issue. But, I cannot yet find it.

Here, polymorphism is an unnecessary complication. How can this be resolved, either through this effort or through other recommendations you may have?

The migration:

class ResolveCarKeyPolymorphism < ActiveRecord::Migration[5.2]
  def self.up
    add_column :cars, :will_be_key_id, :integer
    self.copy_key_id_to_will_be_key_id
    remove_reference :cars, :key, polymorphic: true
    rename_column :cars, :will_be_key_id, :key_id
  end
  def self.down
    rename_column :cars, :key_id, :will_be_key_id
    add_reference :cars, :keys, polymorphic: true
    self.copy_will_be_key_id_to_key_id
    remove_column :cars, :will_be_key_id
  end
  def copy_key_id_to_will_be_key_id
    Car.all.each do |car|
      car.will_be_key_id = car.key_id
      car.save!
      puts "Car:#{car.id};Key:#{car.key_id}"
    end
  end
  def copy_will_be_key_id_to_key_id
    Car.all.each do |car|
      car.key_id = car.will_be_key_id
      car.key_type = "Key"
      car.save!
      puts "Car:#{car.id};Key:#{car.key_id}"
    end
  end
end

Example code:

class Key < ApplicationRecord
    has_one :car, dependent: :destroy
end

class Car < ApplicationRecord
    belongs_to :key, dependent: :destroy  #, polymorphic: true (commented out after the migration)
end

car = Car.find_by(stock_number: "PRT38880")
=> #<Car id: 56251, stock_number: "PRT38880", key_id: 25629>

car.key
=> #<Key id: 25629>

key = Key.find(25629)
=> #<Key id: 25629>

key.car
=> PG::UndefinedColumn: ERROR:  column cars.key_type does not exist

Additional example: (I was surprised this worked, but only after migrate and rollback.)

car = Car.find_by(stock_number: "PRT38880")
=> #<Car id: 56251, stock_number: "PRT38880", key_id: 25629>

key = car.key
=> #<Key id: 25629>

key.car
=> #<Car id: 56251, stock_number: "PRT38880", key_id: 25629>

Alternate migration replacing entire table with same result (Updated):

class ResolveCarKeyPolymorphism < ActiveRecord::Migration[5.2]
  def self.up
    create_table "cars_news", id: :serial, force: :cascade do |t|
      t.string "stock_number", limit: 255, default: "", null: false
      ... additional fields
      t.integer "key_id"
    end
    add_foreign_key :cars, :keys
    self.copy_cars_to_cars_news
    drop_table :cars
    rename_table :cars_news, :cars
    change_table :cars do |t|
      t.index ["company_id"], name: "index_cars_on_company_id"
      t.index ["stock_number"], name: "index_cars_on_stock_number"
      t.index ["key_id"], name: "index_cars_on_key_id"
    end
  end

  def self.down
    remove_foreign_key :cars, :keys if foreign_key_exists?(:cars, :keys)
    remove_index :cars, :key_id if index_exists?(:keys, :key_id)
    add_column :cars, :key_type, :string unless column_exists?(:cars, :key_type)
    add_index :cars, ["key_type", "key_id"], name: "index_cars_on_key_type_and_key_id"
    Car.update_all(key_type: "Key")
  end

  def copy_cars_to_cars_news
    Car.all.each do |car|
      cars_new = CarsNew.new
      car.attributes.each do |key, value|
        cars_new[key] = value unless key == "key_type"
      end
      cars_new.save!
      puts "Car:#{cars_new[:stock_number]}:#{cars_new[:id]} created with key_id:#{cars_new[:key_id]};"
    end
  end
end

Solution

  • The problem was not related to polymorphism but instead was STI. Because STI was in play, it required and used the type field to identify the record. The Key class, the parent, should have been defined with:

    self.abstract_class = true
    

    And, the children should have defined individual tables without the type field.