Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Tools
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker
Ben Smith

Moving db tables between Rails engines

Ben Smith
Friday, June 7, 2013

If you’re using Rails engines to break up your app and you’re putting your migrations in the engines, then you’re already doing great! Here’s an additional pro-tip when it comes to having migrations within your engines: only allow each migration to touch one db table.

Why would we do this? When it comes to refactoring, there are times when you want to move a database table from one engine to another. Better yet, pull a whole table out into it’s own new engine! Or maybe you have a big Rails app, and you’re wanting to pull chunks of functionality into engines.

Doing any of these refactorings is tricky if your migrations touch multiple database tables. If you’re in this boat, then your best bet is to drop the table in one migration, and do a new migration to create the table again in your new engine. But if your migrations for that table ONLY touch the table your trying to move, then it’s as easy as…

1) Move ALL migration files for the db table from engine X to engine Y (this includes the migrations that created the table, added columns to it, renamed it, etc)
2) Run rake app:db:drop app:db:create && rake app:db:migrate app:db:test:prepare from within engine X and engine Y

…and you’re done. You’ve moved your database table from engine X to engine Y!

The crazy thing is, when you run rake db:migrate from your wrapping Rails app (the one that requires all your engines), it won’t run any new migrations. To the wrapping Rails app (ie your production server), there are no new migrations. It’s pulling the same migrations from a different place, so it doesn’t notice a difference.

This makes refactoring and moving tables around 100x easier, and I loooove me some easy refactorings!

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Ben Smith

leave your migrations in your Rails engines

Ben Smith
Wednesday, May 8, 2013

If you are using Rails engines to break up a single app into modular pieces, migrations (as they are currently implemented in Rails 3.2.13) become clumsy.

There are three options for migrations within an engine (spoiler: #3 is the best):

1) You can use the your_engine_name:install:migrations rake task, which copies the migrations out of the engine and into the wrapping Rails app where they can be run normally. This works fine if your migrations in your engine never change, but if you’re actively developing your engine you need to run this rake task each time you add a migration.

2) You can put all your migrations in your wrapping Rails app. This works if you’re using your engines as a way to break up your app, but it doesn’t feel right. If your models, views, and controllers all live within the engine (and depend on migrations), shouldn’t your migrations live within the engine as well? If your migrations live in the wrapper Rails app, you actually create a weird upward dependency where the engine is actually dependent on the wrapper app. This is bad.

3) You can monkey patch Rails so all of your engine’s migrations automatically get run in the wrapper Rails app. Everything just works, and migrations live where they should: in the engine. If you’re breaking up your large Rails app into engines, this is the way to go. Here’s how you do it….

Within your Rails engine, there should be a file called engine.rb here’s an example of it for an engine I called EngineWithMigrations:


module EngineWithMigrations
  class Engine < ::Rails::Engine
    isolate_namespace EngineWithMigrations
  end
end

All you need to do is tell Rails to add your engine’s migration directory to its list of places it looks for migrations. Like so:


module EngineWithMigrations
  class Engine < ::Rails::Engine
    isolate_namespace EngineWithMigrations
    
    initializer :append_migrations do |app|
      unless app.root.to_s.match root.to_s
        app.config.paths["db/migrate"] += config.paths["db/migrate"].expanded
      end
    end
  end
end

app.config is the config of your wrapper Rails app, config is the config of your engine. The above line adds the engine's migration directory to the wrapper Rails app's migration directory list. The unless wrapping it is to keep your migrations from running twice in your testing dummy app (which already runs migrations fine). Now when you run rake db:migrate from your wrapper app, your engine's migrations just work!

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Pivotal Labs

Mongoid Migrations using the Mongo Driver

Pivotal Labs
Saturday, January 29, 2011

In my last post, I modified a Mongoid::Document during a migration in order to access fields that where no longer defined in the class. This time I am using the mongo ruby driver directly to migrate data.

Since I am using Mongoid in this project, I will be using it to access the dependent mongo driver. This will prevent me from having to provide mongo with a connection string in my migration. I am also using mongoid_rails_migrations to roll out the change.

Initial Design

class User
  include Mongoid::Document
  field :first_name
  field :last_name
end

New Design

class User
  include Mongoid::Document
  field :name
end

Migration

class MergeUsersFirstAndLastName < Mongoid::Migration
  def self.up
    #get the mongo database instance from the Mongoid::Document
    mongo_db = User.db

    #query the collection for the fields needed for the migration
    user_hashes = mongo_db.collection("users").find({}, :fields => ["first_name", "last_name"])

    user_hashes.each do |user_hash|
      new_name = "#{user_hash['first_name']} #{user_hash['last_name']}"

      #update the new field
      mongo_db.collection("users").update({"_id" => user_hash["_id"]}, {"$set" => {"name" => new_name}})

      #remove old fields from collection
      mongo_db.collection("users").update({"_id" => user_hash["_id"]}, {"$unset" => { "last_name" => 1, "first_name" => 1}})
    end
  end
end

Resources

MongoDB Ruby Driver Tutorial

Querying with MongoDB Ruby Driver

Updating with MongoDB Ruby Driver

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Pivotal Labs

Embedding Mongoid documents and data migrations

Pivotal Labs
Thursday, January 6, 2011

When first starting out with mongodb, it’s easy to make the wrong decision on whether to embed a document or not. Even if you made the correct decision at that moment, changing requirements may force you into a migration. So how do you migrate existing data when transitioning from a standalone document to an embedded document? This is what I came up with.

Initial Data Structure

class User
  include Mongoid::Document
  field :name
  references_many :sales
end

class Sale
  include Mongoid::Document
  field :price, :type => Integer
  referenced_in :user
end

Now with Sale embedded in User

class User
  include Mongoid::Document
  field :name
  embeds_many :sales
end

class Sale
  include Mongoid::Document
  field :price, :type => Integer
  embedded_in :user, :inverse_of => :sales
end

Migrating Sales Data

class EmbedSalesInUsers < Mongoid::Migration
  def self.up

    # pull your existing data into memory
    # consider batching for large data sets
    # Note that you must call query methods on the object you are migrating
    # for this method to work (i.e. you can not pull via User#sales)

    sales_attributes = while_stand_alone_doc(Sale) do
      Sale.all.map(&:attributes)
    end

    # now when you save your data, your fields will be embedded

    sales_attributes.each do |attributes|
      user = User.find(attributes[:user_id])
      user.sales << Sale.new(:price => attributes[:price])
    end

    # remove all the documents from the original collection

    while_stand_alone_doc(Sale) do
      Sale.destroy_all
    end
  end

  def self.while_stand_alone_doc(klass)
    # by changing the Mongoid::Document.embedded you can temporarily
    # modify which collection Mongoid looks to for your model's data store

    begin
      klass.embedded = false

      yield
    ensure
      klass.embedded = true
    end
  end

end

There are a couple things to note here.

  • The embedded flag in Mongoid::Document is not documented so it could easily change. This was working as of 2.0.0.beta.20
  • When you create the new embedded document, make sure you pass only the attributes you care about. Passing all attributes will add things that you no longer need like user_id in this case. (For clarity, attributes you assign will be persisted, though you will only have setters and getters for the fields you explicitly define in your document.
  • I am using mongoid_rails_migrations in this example
  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Collapsing Migrations

Alex Chaffee
Wednesday, December 12, 2007

(6:30 pm: updated to use mysqldump)
(12/14/07: updated to remove db:reset since the Rails 2.0 version now does something different.)
(12/15/07: updated to not set ENV['RAILS_ENV'] since that gets passed down to child processes)

There was an old hacker who lived in a shoe; she had so many migrations she didn’t know what to do. Every time her build ran clean, she spent a whole minute staring at the screen.

Fortunately, she read this blog post and now her db:setup task is so fast she’s started building multiple test environments so she can run tests in parallel!

  • Figure out what migration to collapse to. This number should be less than or equal to the oldest deployed version of your app. E.g. if most of your deployments are on version 348 but there’s one client running a branch that’s only up to version 298, then pick 298 (or 297 if you’re afraid of off-by-one errors). For this example we will use 100.

  • Install lib/tasks/db.rake and lib/db_tasks.rb (source below)

  • Clear the development database by running

    rake db:clear

  • Dump the development structure by running

    rake db:dump

  • Delete all the migrations up to and including your target version. Here’s a sneaky awk script that deletes everything up to and including 100. (Go ahead and run it, it won’t bite, and you can always revert.)

    ls db/migrate/ | awk ‘{split($0, a, “_”); if(a[1]<=100) print $0}’ | xargs svn rm

  • Create a new migration called “100_collapsed_migrations.rb” using the following template.

100_collapsed_migrations.rb:

class CollapsedMigrations < ActiveRecord::Migration
  def self.up
    sql = <<-SQL
  # development_structure.sql goes here
    SQL

    execute("SET FOREIGN_KEY_CHECKS=0")
    sql.split(";").each do |statement|
      execute(statement)
    end
  ensure
    execute("SET FOREIGN_KEY_CHECKS=1")
  end

  def self.down
    raise IrreversibleMigration
  end
end
  • Open up db/development_dump.sql and copy its entire contents into your clipboard, then paste it above the “SQL” line in your new migration 100.

  • Search for the statement that creates the schema_info table and remove it.

Mine looks like this:

CREATE TABLE `schema_info` (
  `version` int(11) default NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  • Set up your databases and run your tests.

    rake db:setup test

  • Congratulations! Your migrations are now blazingly fast, just like back in the (scaff)old days. You can run “rake db:setup” any time you get a svn update that looks like it may have done something funky to your schema, rather than shying away from that minute-long migration and just hoping your tests still pass.

Why do we need to use db:dump rather than db:schema:dump? Well, unfortunately, db:schema:dump doesn’t dump everything. It misses CONSTRAINT statements and also seems to get the charset wrong (although that may have been a function of how I constructed the db in my test). And db:structure:dump misses any data that may have been added by your migrations.

Here’s my current db.rake. Unfortunately, it only works with MySQL, but if you want to make it support your favorite DB (or even your least favorite) then please go right ahead.

Oh, and that part about multiple test environments and parallellized tests? Stay tuned… :-)

db.rake:

require "db_tasks"

namespace :db do
  def tasks
    (@db_tasks ||= DbTasks.new(self))
  end

  desc "Drop and recreate database"
  task :clear => :environment do
    tasks.clear
  end

  desc "Clear and migrate dev and test databases, and load fixtures into development db"
  task :setup => :environment do
    tasks.setup
  end

  desc "Dump the current environment's database schema and data to, e.g., db/development_dump.sql (optional param: FILE=foo.sql)"
  task :dump => :environment do
    if ENV['FILE']
      tasks.dump ENV['FILE']
    else
      tasks.dump
    end
  end

  desc "Load an sql file (by default db/development_dump.sql). (Optional param: FILE=foo.sql)"
  task :load => :environment do
    if ENV['FILE']
      tasks.load ENV['FILE']
    else
      tasks.load
    end
  end
end

db_tasks.rb:

# This creates a duplicate of the database config for a db config as defined in database.yml.
# For example, if the "test" database is named "myapp_test",
# for clone number 0, the new environment is named "test0", and the database is "myapp_test0".
# All other settings are preserved (esp. username and password).
module ActiveRecord
  class Base
    def self.clone_config(original_config, worker_number)
      original = configurations[original_config.to_s]
      raise "Could not find conguration '#{original_config}' to clone" if original.nil?
      worker_config = original.dup
      worker_config["database"] += worker_number.to_s
      configurations["#{original_config}#{worker_number}"] = worker_config
    end
  end
end

class DbTasks
  def initialize(rake)
    @rake = rake
  end

  def init
    connect_to('development')
    clear_database
    migrate_database
    dump
    test_environments.each do |test_db|
      if test_db =~ /([0-9]+)$/
        clone_test_config($1.to_i)
      end
      connect_to(test_db)
      clear_database
      load
    end
  end

  # db:clear -> drop and create db for RAILS_ENV
  def clear
    clear_database
  end

  # db:setup -> drop, create, and migrate dbs for test and development environments, and import fixtures into development
  def setup
    init
    connect_to 'development'
    load_fixtures
  end

  def dump(file = "#{RAILS_ROOT}/db/#{environment}_dump.sql")
    puts "Dumping #{database} into #{file}"
    system "mysqldump #{database} -u#{username} #{password_parameter} --default-character-set=utf8 > #{file}"
  end

  def load(sql_file = "#{RAILS_ROOT}/db/development_dump.sql")
    puts "Loading #{sql_file} into #{database}"
    query('SET foreign_key_checks = 0')
    sql_file = File.expand_path(sql_file)
    IO.readlines(sql_file).join.split(";").each do |statement|
      query(statement.strip) unless statement.strip == ""
    end
    query('SET foreign_key_checks = 1')
  end

  protected

  def clone_test_config(worker_num)
    ActiveRecord::Base.clone_config("test", worker_num)
  end

  def connect_to(environment)
    ActiveRecord::Base.establish_connection(environment)
    @environment = environment
    Object.const_set(:RAILS_ENV, environment)
    # Note: don't set ENV['RAILS_ENV'] since that gets passed down to invoked tasks (including 'rake test')
  end

  def environment
    (@environment ||= RAILS_ENV)
  end

  def test_environments
    environments = ['test']
    if Object.const_defined?(:TEST_WORKERS)
      TEST_WORKERS.times do |worker_num|
        environments << "test#{worker_num}"
      end
    end
    environments
  end

  def load_fixtures
    puts "Loading fixtures into #{environment}"
    Rake::Task["db:fixtures:load"].invoke
  end

  def clear_database
    puts "Clearing #{environment} database"
    sql = "drop database if exists #{database}; create database #{database} character set utf8;"
    cmd = %Q|mysql -u#{username} #{password_parameter} -e "#{sql}"|
    # puts "executing #{cmd.inspect}"
    system(cmd)
  end

  def migrate_database
    puts "Migrating #{environment} database"
    ActiveRecord::Migration.verbose = false
    Rake::Task["db:migrate"].invoke
  end

  def config(env = environment)
    ActiveRecord::Base.configurations[env]
  end

  def query(sql)
    ActiveRecord::Base.connection.execute(sql)
  end

  def database
    config["database"]
  end

  def username
    config["username"]
  end

  def password
    config["password"]
  end

  def password_parameter
    if password.nil? || password.empty?
      ""
    else
      "-p#{password}"
    end
  end

  def execute(cmd)
    puts "t#{cmd}"
    unless system(cmd)
      puts "tFailed with status #{$?.exitstatus}"
    end
  end

  def system(cmd)
    @rake.send(:system, cmd)
  end
end
  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Topics

  • agile (783)
  • rails (117)
  • testing (90)
  • ruby (86)
  • ruby on rails (71)
  • jobs (62)
  • javascript (59)
  • techtalk (44)
  • ironblogger (42)
  • rspec (39)
  • bloggerdome (34)
  • productivity (34)
  • activerecord (30)
  • rubymine (30)
  • git (29)
  • gogaruco (29)
  • nyc (27)
  • design (24)
  • mobile (23)
  • pivotal tracker (22)
  • process (21)
  • cucumber (21)
  • jasmine (19)
  • ios (18)
  • tracker ecosystem (17)
  • webos (17)
  • objective-c (17)
  • fun (16)
  • android (16)
  • palm (16)
  • ci (16)
  • "soft" ware (16)
  • bdd (15)
  • tdd (15)
  • cedar (15)
  • rails3 (14)
  • performance (14)
  • css (14)
  • gem (13)
  • mouse-free development (12)
  • selenium (12)
  • goruco (12)
  • bundler (12)
  • api (12)
  • keyboard (11)
  • meetup (11)
  • railsconf (11)
  • nyc-standup (11)
  • capybara (10)
  • mac (10)
Subscribe to migrations Feed
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Tools
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >