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