Keep that SQL in Ruby or doing it right with migrations
Migrations are those little ruby files that are added to the db/migrate/ folder every time you create a model. If you haven’t looked in there, go ahead and check it out, they are there. They don’t have much in them, but they are there just waiting to be used. There are many seasoned developers out there who can sling t-sql like champions, I count myself as one of them, for us, it’s very difficult to consider breaking the n-tier abstraction principals of OO (especially when we know what we’re doing) and simply “trust” Ruby and Rails. If you can give into your fears for a little bit, you’ll see that there are certainly great rewards to using migrations.
Ruby: the one to rule them all. I want you to take a leap of faith and know the following. If you’re using Ruby on Rails, you’ve committed to the Rails way and should follow its conventions, explicitly. Dealing with your database is no different. After all, your database is a part of your Rails application, isn’t it? Just because you had to manually trudge through a mountain of t-sql before Rails doesn’t mean you still need to. Unless you’re addicted to the DB GUI crack… then you’re on your own to find rehab. I can’t help you. You trust Rails to make your calls to the database, to validate the data before it goes it, to deliver it back to you quickly when it comes out, right? Well, why not trust it to create your schema as well? This way if you need to deploy your entire application from Unix/MySQL to Windows/MSSQL you can and you won’t even break a sweat. You’re not locking yourself into DB specific syntax. You’re using Ruby to talk to any Rails supported DB. It’s what the big boys call being “database agnostic”.
Enough Jibber-Jabber! Let’s sling that code! You’ve read this far, which means you want to see some migrations in action, so let’s get to it! This time, I’m placing the code RIGHT HERE in the middle of the article so you can follow along! Don’t worry, it’s at the end if you want to type(Copy and Paste) your way through the examples. Let’s start at the beginning…with a new project. This project is going to be the “start” of what would be (what might be…) a bigger project. The focus in on migrations and not what the app will actually do. :) Be sure to create your databases as well. I’m using Sqlite3… so that step is done for me
rails miapp (if using sqlite3) rails miapp -d sqlite3
Now, let’s create a few models that we are going to use in our app. I’m going to create models that I have created for every app I’ve ever written (I’ve got a generator for them now, but that’s a different article.) so they may look familiar to you
ruby script/generate model user ruby script/generate model role
you can probably guess where this is going. But instead of one role for each user, lets edit our models to allow a has_and_belongs_to_many relationship between the models. This means that my user can have many roles.
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
So far, nothing out of the ordinary. At this point, some of you would go to your mysql, sqlite3, etc command line and start typing out a sql statement to create the 3 tables needed to make this happen. Or, you’re like many developers you launch a GUI app and create the tables there, adding new relationships and keys… letting the GUI build the SQL for you. For you, this should be a smaller leap of faith than the people constructing them by hand. If you will trust a GUI with your sql statements why not rails? After all, you trust them with all the other sql queries…what’s a few more?
So, we know that 2 tables have already been created but if we’re going to allow multiples roles per user we are going to need another table to relate the users and roles tables. To get this migration we type the following into the terminal (please note the habtm alphabetical convention)
ruby script/generate migration roles_users
Ok, the meat and potatoes. let’s open those migrations and add some simple properties to the migration files. Open the first file which if you’re following along would be db/migrate/001_create_users.rb
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
# t.column :name, :string
end
end
def self.down
drop_table :users
end
end
Look familiar? It should, it’s RUBY! so, without changing languages, we’re going to set up our database tables following the habtm(has_and_belongs_to_many) convention. Here are the migration files as I have created them,
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.column :name, :string
t.column :password, :string
t.column :email, :string
end
User.create( :name=>"admin", :password=>"admin", email=>"admin@admin.com")
end
def self.down
drop_table :users
end
end
class CreateRoles < ActiveRecord::Migration
def self.up
create_table :roles do |t|
t.column :name, :string
end
%w{admin user}.map{|role| Role.create(:name=>role)}
end
def self.down
drop_table :roles
end
end
class RolesUsers < ActiveRecord::Migration
def self.up
create_table :roles_users, :id=> false do |t|
t.column :role_id, :integer
t.column :user_id, :integer
end
User.find_by_name("admin").roles << Role.find_by_name("admin")
end
def self.down
drop_table :roles_users
end
end
OK. There’s a lot in there but the things I want you to see right off the bat, is that these are Ruby classes. This is the same language that you’ve been writing your model and controllers in. Migration run as the application so you have full access the models that you’ve created.
If you notice, after each of the create_table methods is called I have established some default values using Ruby. That way, when I start up my application I’ll have to data to play with. When all else fails, you can also insert a Role by opening the script/console and doing it there. Could I have done this in t-sql? Of course, but why do you need to? I haven’t created things like indexes and key relationships because Rails keeps that logic in the business layer instead of the data layer. It is certainly a hot topic for debate (esp amongst DBAs!) but that debate is better served in a different forum.
So, now, all you have to do to get this running is open that terminal window and run…
rake db:migrate
You’re done. You’ve just created a schema for your models and even ran code that proves that they all work! If you don’t believe me you can scaffold a new controller set for users and roles and CRUD away! Or you can go to the console and look for the user and roles. Or Add and remove roles from the user. It’s alot of fun. I did this in sqlite3. But now if I wanted to run it in mysql, I would just reconfigure my database.yml file to point to mysql and run rake db:migrate and BAM, I’m ready to go!
I haven’t gone over nearly all of the options when it comes to migrations but between the sample code and the API you can see all of the additional things that can be done to keep your feature complete when creating your database schema for your application.
Oh hell, just for fun let’s assume that we’re not done yet. Someone came by 10 months after we’ve released to Production and wants to add a few more columns to the user model. They are optional columns but wants users to fill them in if they want. They are blog url and hometown. So, instead of cracking open the database or running a query to modify the tables through sql we’re going to do a migration. Let’s go…
ruby script/generate migration add_blog_url_and_hometown_to_users
How do you like that long name? It’s crappy but you get the point. Let’s add those columns to the user table
class AddBlogUrlAndHometownToUsers < ActiveRecord::Migration
def self.up
add_column :users, :blog_url, :string
add_column :users, :hometown, :integer
end
def self.down
remote_column :users, :blog_url
remove_column :usres, :hometown
end
end
then run the rake…
rake db:migrate
done. OH NO!!!! I fracked it up again! hometown is not supposed to be an integer! It’s supposed to be a string! I guess I’m going to have to go into the database after all, right? Wrong. Just roll back to the previous version make the change and migrate up again.
rake db:migrate VERSION=3
class AddBlogUrlAndHometownToUsers < ActiveRecord::Migration
def self.up
add_column :users, :blog_url, :string
add_column :users, :hometown, :string
end
def self.down
remote_column :users, :blog_url
remove_column :usres, :hometown
end
end
rake db:migrate
So…how did I know to roll back to VERSION=3? see those numbers in front of the migration file names? Those are your versions. If you remove one, you may want to refactor them to keep them up to date, but that’s completely up to you.
So, that’s migrations in a nutshell. Migrations are a great way to use Ruby to create your database schema in a quick and easy fashion. I didn’t go over tests or deploying to production or any of those things (dumping, importing schema etc) those are for another day. For now, just know that migrations are a clean DRY way to manage your schema in Rails and if you’re not taking advantage of them you might want to take another look!
Download the project files here.
articleStats
Here are some silly little facts about this Keep that SQL in Ruby or doing it right with migrations...
article Links
These are the links that appear in this article. They probably don't make sense out of context... but just in case. :)
<span class="caps"><span class="caps"><span class="caps">API</span></span></span> docs here<span class="caps"><span class="caps"><span class="caps">RIGHT HERE</span></span></span>
Download the project files here.
One thing you may want to think about is instead of creating data in your migrations, like when you created new Roles and a User, use data that you have in your fixtures.
Since you're using migrations and you want to do things the rails way, then you should also be doing unit testing. Unit testing uses your test database, and that database will not run migrations. It will use your finished schema. This means that the data that you have just added by using the migrations will not transfer to your test database. So in order to use your unit tests with data, you need to have fixtures. These fixtures could contain test users, like your admin user, as well as the roles that you have assigned in your migrations.
To me it goes against all that is DRY to have data in your migrations and data in fixtures when it's the same thing. So I have written a blog entry on "how to load your fixtures from your migrations":http://shifteleven.com/articles/2006/09/28/loading-fixtures-in-a-migration
And as an aside, I know the purpose of going back a version with your migrations was to show how that functionality works; however, I think it's bad form to do so as a practice. Like I said, it works for this case, but if this code were committed to a repository and then other users ran that migration before you went in to make a change, then there could be unnecessary bug tracking going on. Instead, it's better to create a new migration which fixes something that was in the previous migration(s).