There's plenty of documentation out there describing how to create a polymorphic association in Rails and Active Record. But not much about updating an existing association.
Say you have the following model:
class Post < ApplicationRecord
belongs_to :user
endWhich was generated with the following migration:
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.belongs_to :user, null: false, foreign_key: true
t.string :title, null: false
t.text :body, null: false
t.timestamps
end
end
endYou're have a User who can create a Post. But one day, we decide we want to create a GuestUser model. This would allow somebody to author and manage their posts without creating a full account.
Sure, we could add a flag to your Users table to do this, but you might have some other fields or context making this untenable. Instead, we'd like two separate classes and a polymorphic belongs_to :user association on Post. How can we modify the user relationship?
Looking at the Rails docs, to create a polymorphic association we need a migration like this:
class CreatePictures < ActiveRecord::Migration[7.1]
def change
create_table :pictures do |t|
t.string :name
t.bigint :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
endYou could also shortcut the :imageable_id, :imageable_type, and the associated index with t.references :imageable, polymorphic: true. This means we need to modify our Posts table by:
- Adding a
:user_typecolumn ofstringtype. - Add a new index using the
:user_idand:user_typecolumns. - Remove our old index, which just used
user_id.
In between steps 2 and 3 we're going to have to manually add a user_type value to all existing Posts. We could do this...
class MakePostUserPolymorphic < ActiveRecord::Migration[7.0]
def up
add_column :posts, :user_type, :string
add_index :posts, [:user_id, :user_type]
Post.all.each do |post|
post.update(user_type: "User")
end
remove_index :posts, :user_id
end
def down
remove_index :posts, [:user_id, :user_type]
remove_column :posts, :user_type
add_index :posts, :user_id
end
endRun the migration, then update our Post model so it reads:
class Post < ApplicationRecord
belongs_to :user, polymorphic: true
endEverything will work! BUT if you push this to production it will fail. Why?
The problem is that the model code will get updated before the migration is run. When the migration is run after the polymorphic: true flag is in place, ActiveRecord will run all sorts of validation, change updates, and hooks. Specifically, it will try to look at how you're updating the associated record and compare it to the old value...for which there is no model type string. And you'll get this error:
NoMethodError: undefined method `<' for nil:NilClass (NoMethodError)
if foreign_key_was && model_was < ActiveRecord::BaseTrying to use .save(validate: false) will not save you.
Instead, just did down to raw SQL. Like so:
class MakePostUserPolymorphic < ActiveRecord::Migration[7.0]
def up
add_column :posts, :user_type, :string
add_index :posts, [:user_id, :user_type]
ActiveRecord::Base.connection.execute("UPDATE posts SET user_type = 'User'")
remove_index :posts, :user_id
end
def down
remove_index :posts, [:user_id, :user_type]
remove_column :posts, :user_type
add_index :posts, :user_id
end
endWorks like a charm. (And it's faster!)