0

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
10
  • Have you reloaded your console before typing the examples ? Commented Mar 13, 2021 at 1:20
  • @Maxence Yes. And, reviewed the database to ensure that the key_id column was correct and that the key_type column was in fact gone. I had asked myself the same question and reran the test after restarts to ensure that the process was complete. Commented Mar 13, 2021 at 14:49
  • Indeed strange. Have you had a look to the schema in db folder and checked if it consistent with your changes ? (I doubt it would differ but this old column must still live somewhere in the app) Commented Mar 13, 2021 at 14:56
  • @Maxence Yes, I have. I pulled the Car table from it and am considering more of a major change where I take the old table that includes key_type and copy the entire thing to a new table without key_type and doing renames, much like the migration specified here just a more global scope. However, I am not yet convinced that I won't end up with the same issue. I agree that some reference to key_type must exist, but it must exist in PG because that is who is issuing the error, I guess. Commented Mar 13, 2021 at 15:42
  • @Maxence What is odd to me is that "remove_reference :cars, :key, polymorphic: true" should really clean up this mess by removing key_type references and yet it does not. Commented Mar 13, 2021 at 15:44

3 Answers 3

1
+50

Regarding Querying key.car

It should be possible to have a bidirectional relationship using a polymorphic association. I believe, in your case, the syntax would be as follows:

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

class Car < ApplicationRecord
    belongs_to :key, polymorphic: true
end

Note the as: :key. In this case, the :key refers to the name you chose for the polymorphic column aka key_id and key_type it could still have referred to the Key model and be called something else such as source_id and source_type which is why Rails requires that you specify it.

Results from running steps above: Results from code above

Regarding Removing the Polymorphic Relation

The database itself does not know if your relation is polymorphic or not. Regular relations work fine without the foreign_key which is used to ensure data integrity.

The reason why you may be getting the error PG::UndefinedColumn: ERROR: column cars.key_type does not exist is that Rails is attempting to query using the key_type column which no longer exists after the migration to remove it.

I suspect that you may not have removed the polymorphic: true from the Car model immediately after running the migration or that Rails had already loaded the model considering it to be polymorphic and it did not get reloaded correctly. The following steps should work to switch the migration from polymorphic to non-polymorphic:

  1. Starting with the server in its original state with a polymorphic relation run the following migration. Note that this step is optional but recommended:
class ResolveCarKeyPolymorphism < ActiveRecord::Migration[5.2]
  def change
    remove_index :cars, column: [:key_type, :key_id]
    remove_column :cars, :key_type, :string
    add_index :cars, :key_id
  end
end
  1. Now change your model relationships to the following:
class Key < ApplicationRecord
    has_one :car, dependent: :destroy
end

class Car < ApplicationRecord
    belongs_to :key
end
  1. Restart the server or console session.

Let me know if this works! I didn't get a chance to run the code so there may be some syntax errors.

Results from running steps above: Results from running steps above

Sign up to request clarification or add additional context in comments.

9 Comments

Just to assure you, I run each test both with and without polymorphic. And, i restart the server on each change. That takes some time, but it takes longer if I get it wrong. 2nd comment follows.
Sorry, too late to be working on this. Will try again in the morning with a clear head.
Update for 2 options. Double checked it. I Restored the database from its backup from before I started working on this, so it's clean. I updated both Key and Car as you recommended. I replaced my migration with yours and ran rails db:migrate. Started the server. If I provide "key = car.key" that works. In that one case, "key.car" works. If I do "key = Key.find(car.key_id)" without polymorphism, that works. But, then key.car still fails with the same error of "cars.key_type does not exist" Which it would not. If I do "key = Key.find(car.key_id)" or "car.key" w/ polymorphism, both return nil.
@Richard_G Let me try reproducing on my end.
Okay, your first example, with polymorphism, works in all test cases, given a car, car.key, Key.find(car.key_id).car both work. I would still like to remove polymorphism and may take a shot at your second solution later.
|
0

That's strange, can you try modifying the new key_id column to be a foreign key referencing the Key table?

Although it seems to me the issue is ActiveRecord rather than PostgreSQL as ActiveRecord is generating a query that asumes a polymorphic relation. That being said, after removing the polymorphic relation from the model if you use spring I suggest stopping it with spring stop before opening a new console. I have found some strange issues like this one that are fixed after stopping spring.

1 Comment

Well, I really don't want the key_type column is the issue. I actually restart the server when I change the model. Let me look at specifying key_id as foreign. Thanks.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.