{"id":171,"date":"2010-03-18T05:29:40","date_gmt":"2010-03-18T11:29:40","guid":{"rendered":"http:\/\/www.pervasivecode.com\/blog\/?p=171"},"modified":"2018-11-26T13:32:31","modified_gmt":"2018-11-26T21:32:31","slug":"rails-migration-antipatterns-and-how-to-fix-them","status":"publish","type":"post","link":"http:\/\/www.pervasivecode.com\/blog\/2010\/03\/18\/rails-migration-antipatterns-and-how-to-fix-them\/","title":{"rendered":"Rails Migration Antipatterns and How To Fix Them"},"content":{"rendered":"<p>Migrations are one of the best features of Rails. Although some folks prefer pure SQL rather than Rails migration DSL, I don&#8217;t know of anyone who dislikes the idea of a versioned schema that can evolve in a controlled and repeatable fashion.<\/p>\n<p>But because the concept of database migrations is such a powerful one, it&#8217;s tempting to jam any old change that affects the database into a new migration and run <code>rake db:migrate<\/code> to make it happen. I&#8217;ve been guilty of a bit of this in the past, and I&#8217;ve joined some projects that did other ugly things in migrations. In the process I&#8217;ve learned the hard way that there are some things you <b>must never do<\/b> in a migration or they will come back to haunt you later. Here they are.<br \/>\n<!--more--><\/p>\n<h3>Antipattern: Require the Database to Exist Already<\/h3>\n<p>In other words, the antipattern is for the first migration to depend on some tables and maybe even some data already being in the database.<\/p>\n<p>I know that the original Rails blog video shows DHH using a MySQL admin tool to create the blog database interactively, but really you should be using migrations to create the schema programmatically from scratch.<\/p>\n<p>If you&#8217;re already working on a project that didn&#8217;t do that, you can run <code>rake db:schema:dump<\/code> and look at db\/schema.rb; it contains code that you can insert into a new migration to create the same schema in your development environment. If you&#8217;re using DB features that the design philosophy of ActiveRecord doesn&#8217;t agree with, such as triggers, and the schema.rb dump doesn&#8217;t include them (or if you just think the migration DSL is ugly and you like SQL DDL better), you can do a mysqldump \/ pg_dump \/ whateverdump and wrap a migration around the loading of that SQL file.<\/p>\n<p>If you have a hybrid (you have to start with an old db dump and then migrate it so it becomes current), that&#8217;s gross, and you have a couple of options which are both pretty ugly. But they will work, and when you&#8217;re done the ugliness will be gone.<\/p>\n<p>You could fight your way back to the oldest schema version by debugging the <code>self.down<\/code> methods and running <code>rake db:rollback<\/code> repeatedly until you can create a <code>00001_starting_db_schema.rb<\/code> migration, or you could just blow away all the migrations and use the highest schema version for a new migration that contains the output of a current <code>rake db:schema:dump<\/code>. It depends on how many copies of the database are out there with old schemas that would need to be brought up to date. Clearing out db\/migrate and replacing it all with a single migration is cleaner, but if your production database is 5 migrations out of date you obviously can&#8217;t do that. But you could collapse it down to the one big-bang migration (as the oldest), plus the 5 pending schema changes. If you do it right, you can just deploy the new code and run <code>rake db:migrate<\/code> and everything will be fine. If not, well, you were testing it on a backup of the production database, right? :)<\/p>\n<h3>Antipattern: Only Work Correctly With the Production Data<\/h3>\n<p>What&#8217;s wrong with developers just making dumps of the production database and loading them locally?<\/p>\n<p>First of all, it means that all schema changes have to start at the production database and work backwards to developers&#8217; sandboxed development environments. Hopefully this strikes you as a very stupid workflow.<\/p>\n<p>Secondly, maybe your users don&#8217;t all want to get a message that says &#8220;test message foo bar sdfasdfasd bloopity bloop&#8221; when you&#8217;re testing your new alert system. Should you really be putting their data (passwords, contact info, etc.) at the mercy of your crummy new code?<\/p>\n<p>You should be able to immediately generate an empty, clean database for development. <code>rake db:drop; rake db:create; rake db:migrate<\/code> should do this; <code>rake db:reset<\/code> should have the same result but should be faster since it doesn&#8217;t bother with each migration in sequence.<\/p>\n<p>You should also be able to immediately generate any essential base data such as the initial admin user. The <a href=\"http:\/\/github.com\/mbleigh\/seed-fu\">SeedFu<\/a> plugin does a good job here.<\/p>\n<p>If you need some additional fake data to fiddle around with in your development environment, the <a href=\"http:\/\/github.com\/ryanb\/populator\">Populator<\/a> gem is handy for mass-inserting a bunch of faux data, especially in conjunction with <a href=\"http:\/\/faker.rubyforge.org\/\">Faker<\/a>.<\/p>\n<p>Note that the migrations should neither depend on nor contain actual data. They should just change the data model.<\/p>\n<h3>Antipattern: Clean Up That Only Works on Production Data<\/h3>\n<p>This is really a subset of the previous item but it&#8217;s worth considering as a special case.<\/p>\n<p>If you want to fix some data that got slightly corrupted by some bad code that has been replaced, migrations aren&#8217;t a terrible way to accomplish that.<\/p>\n<p>It&#8217;s not really what migrations are for, and a one-off rake task can do it just as well, but if you really want to, you can get away with it under one condition: you have to make your cleanup migration code succeed even if the database is empty (such as when a developer has just run <code>rake db:reset; rake db:migrate<\/code>).<\/p>\n<h3>Antipattern: Load Data<\/h3>\n<p>The <a href=\"http:\/\/github.com\/ryanb\/populator\">populator<\/a> gem is good for initial, mandatory data. The <a href=\"http:\/\/github.com\/notahat\/machinist\">machinist<\/a> gem is good for synthetic test data. Delete db\/fixtures and everything in it. Fixtures are evil.<\/p>\n<p>Wrap a rake task around the &#8220;get my development database ready&#8221; concept. This task should start with the &#8220;get my empty production database ready&#8221; task (or some subset of that which is appropriate for developer use).<\/p>\n<p>If you need to load arbitrary data now and then, write an importer. Do this as a rake task, or a web UI to a bulk data importer feature. Better yet, make a web UI in your admin area which is just a wrapper around the rake task that bulk imports data. Then delegate the bulk importing to your customers so your admins can do real admin work. But don&#8217;t load data in a migration.<\/p>\n<h3>Antipattern: Use Rails Models in the Migration<\/h3>\n<p>Models evolve, but old migrations don&#8217;t change (nor should they). So when you wrote a migration that used a model, it used the old version of the model code. Then a year later the model has evolved, and the new validations on first_name and last_name fail because it used to be full_name, and that old migration that hasn&#8217;t changed has stopped working. It depended on something that did change, incompatibly.<\/p>\n<p>For rockstar points, in your continuous integration environment you should run <code>rake db:drop; rake db:create; rake db:migrate<\/code> to make sure that this can never happen.<\/p>\n<p>But if it has already happened, rip out the model code and replace it with Rails DSL code, with execute statements containing raw SQL code, or (if you feel like a Ruby rockstar) declare new, stripped down model classes inside your migration class that will act as stand-ins for the limited needs of the migration. See <a href=\"https:\/\/web.archive.org\/web\/20100316223057\/http:\/\/toolmantim.com\/thoughts\/migrating_with_models\">Migrating with Models<\/a> for more on how to do this last trick.<\/p>\n<h3>Conclusion<\/h3>\n<p>You should always be able to do this in every Rails environment that your application has: <code>rake db:drop; rake db:create; rake db:migrate; rake db:reset<\/code><\/p>\n<p>At this point you should then be able to run <code>rake db:test:prepare<\/code> and then <code>rake spec<\/code> or <code>rake test<\/code> or whatever and have it work.<\/p>\n<p>If any part of that process fails, you are missing out on the benefits of using Rails migrations.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Migrations are one of the best features of Rails. Although some folks prefer pure SQL rather than Rails migration DSL, I don&#8217;t know of anyone who dislikes the idea of a versioned schema that can evolve in a controlled and repeatable fashion. But because the concept of database migrations is such a powerful one, it&#8217;s &hellip; <a href=\"http:\/\/www.pervasivecode.com\/blog\/2010\/03\/18\/rails-migration-antipatterns-and-how-to-fix-them\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Rails Migration Antipatterns and How To Fix Them&#8221;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17,26,20,1],"tags":[],"class_list":["post-171","post","type-post","status-publish","format-standard","hentry","category-databases","category-ruby","category-ruby-on-rails","category-uncategorized"],"_links":{"self":[{"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/posts\/171"}],"collection":[{"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/comments?post=171"}],"version-history":[{"count":19,"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/posts\/171\/revisions"}],"predecessor-version":[{"id":397,"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/posts\/171\/revisions\/397"}],"wp:attachment":[{"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/media?parent=171"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/categories?post=171"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.pervasivecode.com\/blog\/wp-json\/wp\/v2\/tags?post=171"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}