Refactor: renaming a model/table name
Refactoring a model name and the database table name requires good unit tests and integration tests with coverage to the model that we are about to refactor. Using a gem like https://github.com/simplecov-ruby/simplecov helps with this. I found it useful even in small apps.
After you make sure that your tests are good enough, start taking notes on which files you have references for that model and macros that you may have in place for it. For example, the model Message may have methods generated by the hasmany macro, like messages and messageids, and some others for other associations. See more here https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Use those notes as a to-do list to read and start making the necessary changes.
Database migrations
You could use self.table_name to avoid this, but that defeats the purpose of this kind of refactoring.
Let's first change the table name. In some scenarios just renametable :oldtablename, :newtable_name will do the trick. But if you have indices, foreign keys constraints and/or join tables, you need to take care of that in this migration too
For example, this migration will create the new refactored table, then recreate the index, set the foreign key, and then insert the old table data into the new table. Finally we drop the old table.
The down direction of this migration does the same but in the opposite way.
class RenameMessagesToDirectMessages < ActiveRecord::Migration[8.1]
def up
return unless table_exists?(:messages)
create_table :direct_messages do |t|
...(whatever columns you defined here)
end
add_index :direct_messages, :contact_id, name: "index_direct_messages_on_contact_id"
add_foreign_key :direct_messages, :contacts
execute <<~SQL
INSERT INTO direct_messages (id, contact_id, content, created_at, direction, sent_at, updated_at)
SELECT id, contact_id, content, created_at, direction, sent_at, updated_at FROM messages;
SQL
drop_table :messages
end
def down
return unless table_exists?(:direct_messages)
create_table :messages do |t|
...(whatever columns you defined here)
end
add_index :messages, :contact_id, name: "index_messages_on_contact_id"
add_foreign_key :messages, :contacts
execute <<~SQL
INSERT INTO messages (id, contact_id, content, created_at, direction, sent_at, updated_at)
SELECT id, contact_id, content, created_at, direction, sent_at, updated_at FROM direct_messages;
SQL
drop_table :direct_messages
end
end
If you dont need full control of each step, you can also have a migration like this one:
class RenameShopsToBusinesses < ActiveRecord::Migration[5.0]
def change
remove_index :messages, :contact_id
rename_table :messages, :direct_messages
add_index :direct_messages, :contact_id
end
end
Troubleshooting
If you manually insert data instead of renaming the database table, you'll likely run into the same issue I did: the primary key sequence doesn't advance after the INSERT in the migration. Because of this, when I tried to create a new direct_message, the system attempted to use id=1 instead of continuing the sequence. To fix this, I wrote the following migration:
class FixDirectMessagesPrimaryKeySequence < ActiveRecord::Migration[8.1]
def up
execute <<~SQL
SELECT setval(
pg_get_serial_sequence('direct_messages','id'),
COALESCE((SELECT MAX(id) FROM direct_messages), 0),
true
);
SQL
end
def down
# There is no reverse for this operation, the sequence value will advance naturally
end
end
pg_get_serial_sequence
will get the sequencename for the directmessages.id
COALESCE(...)
will get the latest id.
setval(seq_name, latest_recorded_value, true)
updates the sequence. It uses the sequence and the latest ID to update the sequence, continuing from the latest ID that we have.
Running the test suite was good enough, I did a full regression test too ofc but everything was already covered