Skip to content

Instantly share code, notes, and snippets.

@HelioCampos
Created September 28, 2017 14:00
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save HelioCampos/72be2d301a9d42ee71fb40bedc019c51 to your computer and use it in GitHub Desktop.
Save HelioCampos/72be2d301a9d42ee71fb40bedc019c51 to your computer and use it in GitHub Desktop.
Database migration best practices for Rails

Database migration best practices for Rails

Never ever change data on schema changes! [1]

Use rake tasks to change the data! [1]

This decouples a deployment from completed migrations. Give us control of the data manipulation proccess by encapsulatin it in on place. need to remember to:

  1. Run it in one of the ways bellow: a. Add this rake task the deployment script or; b. Run it mannually after the deployment.
  2. Need to clean up after the end of the data manipulation;
  3. Need to remove the rake task after the deployment is implemented; Example of rake task:
# lib/tasks/temporary/users.rake
namespace :users do
  desc "Update confirmed users to receive newsletter"
  task set_newsletter: :environment do
    users = User.confirmed
    puts "Going to update #{users.count} users"

    ActiveRecord::Base.transaction do
      users.each do |user|
        user.mark_newsletter_received!
        print "."
      end
    end

    puts " All done now!"
  end
end

Read-Only Models [2]

If you don't need to save the model, then make it read-only with:

class Role < ActiveRecord::Base
  def readonly?
    true
  end
end

This way the model can be modified without concerns.

Removing columns [2]

If you need to remove a column, first deploy an update so the ActiveRecord cache ignore it like this:

class User
  def self.columns
    super.reject { |c| c.name == "notes" }
  end
end

Then you may safelly remove the column in the next deploy.

Renaming columns [2]

Do it i three steps:

  1. Add the new column (first_name) and read/write from/to both columns (first_name failling back to fname). Don't change any queries yet!
def first_name
  super || attributes["fname"]
end
  1. Populate the new column with data from the old column and update the queries.
  2. Delete the old column.

Adding NOT NULL constraint [2]

  1. Make sure you are writing to the columnn that will receive the constraint
before_save :assign_defaults

def assign_defaults
  self.admin ||= false
end
  1. Update all existing records that have the column as null.
  2. Add the constraint.

Creating indexes [2]

  1. Rails don't create indexes concurrently and, if the table receives lots os writes, you need it. the only way is to use raw SQL.
class IndexUsersEmails < ActiveRecord::Migration
  def ddl_transaction(&block)
    block.call # do not start a transaction
  end

  def self.up
    execute "CREATE INDEX CONCURRENTLY index_users_on_email ON users(email)"
  end
end

Rails will dump the index to a Ruby schema normally.

Cheat sheet [2]

Adding columns

– Safe for readonly models – Safe when there are no constraints on the column

Removing columns

– Safe for readonly models – Tell AR to ignore the column first

Renaming columns

– Not safe – First add a new column, then remove the old one – When the column is used on SQL queries you’ll need to split this in three steps

Creating tables

– Safe

Removing tables

– Safe

Creating indexes

– Safe only for readonly models – Otherwise make sure you create indexes concurrently

Removing indexes

– Safe

Bibliografia

  1. https://robots.thoughtbot.com/data-migrations-in-rails
  2. https://blog.codeship.com/rails-migrations-zero-downtime/
  3. http://engineering.harrys.com/2014/06/09/seed-migrations.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment