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!