Netskin Logo

Pure Models in Migrations

#rails
by Tom Rothe on 05.01.2023

Relying on models in database migrations can be dangerous. Models evolve over time, but migrations typically stay the same. Let’s consider a simple blog example. We have the following migration:

class CreateArticleAndComments < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string :title
    end

    create_table :comments do |t|
      t.references :article
      t.string :message
    end
  end
end

This migration was probably written quite early in the project. The respective models Article and Comment were created. Later in the project we decide to generalize comments, so they can be attached to any model and not only articles:

class MoveCommentsToCommentable < ActiveRecord::Migration
  def change
    create_table :commentable do |t|
      t.references :subject, polymorphic: true
      t.string :message
    end

    # move all article comments
    Comment.find_each do |comment|
      Commentable.create!(subject: comment.article, message: comment.message)
    end

    # remove old table
    drop_table :comments
  end
end

This code is not ideal for multiple reasons. First, iterating through all comments and creating a new record in Commentable is not particularly efficient. We could easily solve this with a little SQL or simply renaming the table.

But even if we decide to ignore this performance issue, we have a much more severe problem: the use of Comment. When coding this migration, we test it and it works out well. Then we decide to move on and remove the app/models/comment.rb file. This makes sense, because the Comment model is not needed anymore. A few days later we deploy on production and 💥. The migration relies on the model, but the model is already 6 feet under.

How can we solve this? We introduce a pure model:

class MoveCommentsToCommentable < ActiveRecord::Migration
  class PureComment < ActiveRecord::Base
    self.table_name = 'comments'
    belongs_to :article
  end

  def change
    # ...
    PureComment.find_each do |comment|
      Commentable.create!(subject: comment.article, message: comment.message)
    end
    # ...
  end
end

No matter what happens with the Comment model, this migration will not break. We can now happily deploy on production.

The example above is simplified and – let’s be honest – a little contrived. Yet, the technique of pure models gets more interesting if the model to be changed/removed is more complex. Here some examples:

class SomeMigration < ActiveRecord::Migration
  class PureModel < ActiveRecord::Base
    # ensure removal of dependent records (if not configured in db)
    has_many :children, dependent: :destroy

    # use the migration to clean up uploaded files
    mount_uploader :avatar, AvatarUploader

    # retain access to Ruby-serialized data in the migration
    # even if the model/attribute is already gone
    serialize :data
  end

Happy coding!

❮ Office 365 data in Ruby on Rails app using Microsoft Graph REST API
Simple maintenance page ❯
Netskin Logo