Rename an ActiveRecord model

Choosing a good name for a class or a variable isn’t easy at all and sometimes there is a requirement to rename existing tables and models to match the updated product audience.

So how we should do this migration then:

1. Ask why

The first step is to ask why should we rename the model, with a productive mindset and a good set of questions you will be surprised that in most cases a renaming is not required at all!

2. Change the model name

It seems that this case requires to rename the ActiveRecord model, so let’s use the following example:

# app/models/author.rb

class Author < ApplicationRecord
  has_many :books
end
# app/models/book.rb

class Book < ApplicationRecord
  belongs_to :author
end
# db/schema.rb

ActiveRecord::Schema.define(version: 2018_06_09_111650) do

  create_table "authors", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "books", force: :cascade do |t|
    t.string "title"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "author_id"
    t.index ["author_id"], name: "index_books_on_author_id"
  end

end

As we can see we have an Author model who has many Books and our requirement is to rename Author model to Writer.

We start by renaming the Author model file and mapping the new model to the existing authors table. Also, we need to specify the foreign key for the association to be able to access associated books.

# app/models/author.rb -> app/models/writer.rb

class Writer < ApplicationRecord

  self.table_name = "authors"

  has_many :books, foreign_key: 'author_id'
end

Let’s update the Book model by specifying the existent foreign key. This step is important to maintain the belongs_to association.

class Book < ApplicationRecord
  belongs_to :writer, foreign_key: 'author_id'
end

I guess that you are asking why should we bother with this step instead of renaming the models and tables at the same time.

To answer this good question, we should consider how applications look like in production. Usually, models may have different associations and used in different parts of the application like controllers and services. That’s why we do this process step by step to be sure that our application is still working as expected before changing the tables and columns in the database.

3. Run the test suite

At this point, we must run our test suite and be sure all specs are passing before moving to the last step. This step is very important to be sure that we didn’t break an existent feature during the migration.

4. Rename DB tables and columns

Now we are ready for the last step, so let’s write some migrations. We start by renaming the authors’ table to writers.

class RenameAuthorsToWriters < ActiveRecord::Migration[5.2]
  def change
    rename_table :authors, :writers
  end
end

Then we need to rename the foreign key from author_id to writer_id.

class RenameFkInBooks &lt; ActiveRecord::Migration[5.2]
  def change
    rename_column :books, :author_id, :writer_id
  end
end

And finally, we clean up our models.

# app/models/writer.rb

class Writer < ApplicationRecord
  has_many :books
end
# app/models/book.rb

class Book < ApplicationRecord
  belongs_to :writer
end

Here is the updated schema, we can check that renaming the column took care of renaming the index. But we should keep in mind this only valid for Rails version >= 4.0 as stated in this changelog:

In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed.

source

ActiveRecord::Schema.define(version: 2018_06_09_115643) do

  create_table "books", force: :cascade do |t|
    t.string "title"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "writer_id"
    t.index ["writer_id"], name: "index_books_on_writer_id"
  end

  create_table "writers", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

That’s it, have fun and keep coding 🙂