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
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
When coding this migration, we test it and it works out well. Then we decide to move on and remove the
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