We had a new developer join our project recently, and he needed his computer to be setup so he could run the application.
“Here is the svn repository, when you check it out, run these rake tasks.”
Unfortunately, is never that easy. This project setup revealed something about Active Record and migrations that I didn’t know about.
When I create a migration, I will often do data manipulation on the database, or pre-populate some fields with data needed for a lookup table. Lets look at a sample migration from a trivia game:
1 class CreateQuestions < ActiveRecord::Migration 2 3 def self.up 4 5 create_table :questions do |t| 6 t.column :text, :string 7 t.column :answer, :string 8 t.timestamps 9 end 10 11 Question.populate 12 end 13 14 def self.down 15 drop_table :questions 16 end 17 end
I want to add some sample questions, so that even if you don’t have your own questions, you will still be able to play the game. I added the method to the populate model, because I use it elsewhere in the code, and I try to keep it DRY. The populate method on the question model looks like this:
1 class Question < ActiveRecord::Base 2 belongs_to :game 3 has_many :answers 4 5 def self.populate 6 Questions.create(:name => "What is your favorite color?", :answer => "I don't know") 7 Questions.create(:name => "Who was the first President", :answer => "George Washington") 8 Questions.create(:name => "Who was born Samuel Clemens?", :answer => "Mark Twain") 9 end 10 11 end
So, later on, I decided to add a degree of difficulty to the questions, so the players can get more points for answering harder questions. Here is what the migration looked like:
1 class CreateQuestions < ActiveRecord::Migration 2 3 def self.up 4 add_column :questions, :rank, :integer 5 Question.destroy_all #In case there are any old ones 6 Question.populate 7 end 8 9 def self.down 10 remove_column :questions, :rank 11 end 12 end
Of which I had to change the populate method on the question class to:
1 class Question < ActiveRecord::Base 2 belongs_to :game 3 has_many :answers 4 5 def self.populate 6 Questions.create(:name => "What is your favorite color?", :answer => "I don't know", :rank => 1) 7 Questions.create(:name => "Who was the first President", :answer => "George Washington", :rank => 3) 8 Questions.create(:name => "Who was born Samuel Clemens?", :answer => "Mark Twain", :rank => 8) 9 end 10 11 end
Then I ran my migrations, and continued development. Then when developer number 2 came across and checked out the project and ran the migrations, he got the error:
1 undefined method rank = for class Question
The problem is the old migration is dependent on the new model. All models in rails are just a mirror of the database, so the new model has a forward definition of the data. The code in the model knows about the rank field, but the schema of the database hasn’t caught up to create that portion of the mirror yet. This creates a little bit of a catch 22.
The rails wiki about migrations tells you to redefine the class to stop name conflicts. This would require me to make my migrations model agnostic, inserting straight to the database.
As a spoiled brat when it comes to databases and rails, I refuse to let go of my Active Record sugary syntax. Another solution I thought of is to just make the last change to question do the populate, and remove it from the previous versions. This will become a maintenance nightmare.
I came to the realization that I want to make a distinction between form and content when it comes to migrations. Form in this case is schema form, the changes to the database which reflect the data which the Active Records can potential hold.
Content is the specific data which is in the database. This distinction allows for me to use the power of my model classes in my data migrations, which is the place it is useful. It maintains backwards compatibility, because before I go touching the data, I have to make sure my schema is right.
What does this look like in Rails? I am not sure yet. Possibly saving the data migrations in each migration as a block and executing those at the end, only when you have the schema is correct. I am going to try it out!