DRY Migrations (aka reverting or undoing earlier migrations)

I have a case where I made some database changes in migration 001 and now I want to undo them in migration 005. This is different that actually rolling back to version 001 (i.e. rake db:migrate version=1)...I actually want to undo or revert what I did in 001. The obvious thing to do would be to simply have 005 do the opposite of 001, but that isn't very DRY.

Using a simple example, let's say my migrations were:


  • 001_create_books.rb
  • 002_create_libraries.rb
  • 003_create_librarians.rb

and now my library is going electronic, so I do this:

  • 004_create_e_books.rb
  • 005_destroy_books.rb

In 005 I want to do the exact opposite of 001, so I did this:

class DestroyBooks < ActiveRecord::Migration
def self.up
CreateBooks::migrate(:down)
end

def self.down
CreateBooks::migrate(:up)
end
end

Custom Configurations and App Specific Settings


One not immediately obvious Ruby on Rails configuration issue I came across is how to structure application-specific config parameters, and in particular, how to make them configurable/overridable across different environments (dev, test, production). This has been solved repeatedly in different ways by the Rails community and there's info scattered around but assimilating it took a while, here's what I've settled on for now.


I couldn't have said it better myself. I've now spent 4 hours researching the best way to do this...this is a basic feature of a web app, you'd think there would be a conventional way to create ..whatever you want to call them: custom configurations, application specific settings, application configs...in Rails.

At present, I've found four solutions...the one suggested by the above blogger, and these three others:

  • http://www.taknado.com/2007/7/25/custom-configuration-info-in-rails
  • http://jarmark.org/projects/app-config/
  • Use after_initialize method on the Rails::Configuration class. (Note: this is only semi-well documented...it's documented, but not included at http://api.rubyonrails.org. I found it here: http://edgedocs.planetargon.org/classes/Rails/Configuration.html#M002860)


In addition, I found these blog posts useful:


  • http://toolmantim.com/article/2006/12/27/environments_and_the_rails_initialisation_process
  • http://glu.ttono.us/articles/2006/05/22/guide-environments-in-rails-1-1


I haven't decided how I'm going to do this yet, but I hope you find this helpful.

UPDATE: I was exchanging e-mail from Jeff at softiesonrails.com who suggested this solution:
What I usually do is add it to the bottom of environment.rb. If I have RAILS_ENV-dependent data (like development mode vs. production mode), then just put the relevant Ruby code at the bottom of environments/development.rb, for example:

    module MyConstants
ALLOWED_NAMES = ['jeff', 'cookie monster']
IP_ALLOWED = '1.2.3.4'
end

and then you can access them anywhere in your Rails code as MyConstants::ALLOWED_NAMES, etc.

If you prefer to move it to a file, you'd create a file named my_constants.rb in the /lib folder, and then explicitly require it from environment.rb:

    require 'my_constants'

Reverting Migrations (self.drop) Issue

I created an ran a migration today. Here is how my database looked to begin:


mysql> show tables;
+--------------------------------------+
| Tables_in_books_development |
+--------------------------------------+
| journals |
| schema_info |
+--------------------------------------+
2 rows in set (0.00 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
| 19 |
+---------+
1 row in set (0.00 sec)

Then, I ran this migration:

class CreateThemes < ActiveRecord::Migration
def self.up
create_table :themes do |t|
t.column :name, :string
t.column :stylesheet_path, :string
t.column :thumbnail_path, :string
t.column :journals_count, :integer, :default => 0
end
#add_column :journals, :theme_id, :integer
end

def self.down
drop_table :themes
remove_column :journals, :theme_id
end
end

Notice how I have the add_column method commented out. Running Rake yields this:

C:\rails_apps\books>rake db:migrate
(in C:/rails_apps/books)
== CreateThemes: migrating ====================================================
-- create_table(:themes)
-> 0.1200s
== CreateThemes: migrated (0.1200s) ===========================================

As you can see, the column is not added to the journals table. Here's what my database looks like now:

mysql> show tables;
+--------------------------------------+
| Tables_in_books_development |
+--------------------------------------+
| journals |
| schema_info |
| themes |
+--------------------------------------+
3 rows in set (0.00 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
| 20 |
+---------+
1 row in set (0.00 sec)

Well, I really need that column added to journals, so I better revert that migration via Rake, uncomment the add_column method, and re-Rake. Here's what happens:

C:\rails_apps\books>rake db:migrate version=19
(in C:/rails_apps/books)
== CreateThemes: reverting ====================================================
-- drop_table(:themes)
-> 0.0500s
-- remove_column(:journals, :theme_id)
rake aborted!
Mysql::Error: Can't DROP 'theme_id'; check that column/key exists: ALTER TABLE journals DROP `theme_id`

(See full trace by running task with --trace)

Oops! The self.down method is crapping out because there is no column in journals called theme_id. (Of course, we knew that.) Let's checkout the database now:

mysql> show tables;
+--------------------------------------+
| Tables_in_books_development |
+--------------------------------------+
| journals |
| schema_info |
+--------------------------------------+
2 rows in set (0.01 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
| 20 |
+---------+
1 row in set (0.00 sec)

Do you see that schema_info still says version 20. I presume that the last thing a migration does is update the schema_info.version with the idea being that we don't modify the version until we sure that the migration has succeeded. The problem with that is that migrations aren't transactional--that is, they can complete or fail partially, yet the schema_info is updated as though it were a transaction. I don't know how Rails fixes this, but it's something worth knowing about.

Anyhow, at this point my database is in limbo...neither in version 19 nor 20. When I run
rake db:migrate
nothing happens because as far as Rake can tell, the DB is up-to-date. But, I can't run
rake db:migrate version=19
either because Rake will error out because there is no theme table to drop.

Okay, so the short-term solution to this problem is to clean-up the database manually. In my case, everything was actually reverted in the self.drop, but Rake doesn't know that. In your case you may have more manual clean-up to do. So, all I needed to do was update schema_info.version to 19.

Netscape Navigator 1.0

I'm not getting much work done today...my desk and office are a mess, so I'm working at cleaning up.

While doing so, I found a bit of history in my desk, and you can have it.

Create Action Requires POST

UPDATE: I wrote the whole post below, and then I figured it out...oy, the risks of auto-generated code. Typically, I use script/generate scaffold to build my controllers and views, then I start modifying from there. The risk of using auto-generated code is that you don't know every detail...and, if you're gonna own a program, you gotta know ever detail.

Long story short: I started digging around at .../verification.rb and I finally ended up back at my own controller and found this that scaffold sticks this bit of code in at the top of every controller:

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :action => :list }

which is why I wasn't able call create with a GET. Yikes!




Boy, what a morning! I found a "feature" of Rails that I don't quite understand yet, but it certainly caused me a lot of headache. Maybe somebody else who is frustrated will benefit from this information.

Long story short: if you are going to call the create action in a controller, then you can only do so via POST. If you try to do so via GET you will get an error like the one below. In other words:

<html>
<body>
Form with POST method --> works
<form action="/books/create" method="post">
<input type="submit" name="submit" value="submit">
</form>

Form with GET method --> does not work
<form action="/books/create" method="get">
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>

Apparently, Rails has some sort of before_filter that requires certain types of actions to be called via certain HTTP methods. (Note: I am not using any of the Rails RESTful stuff, e.g. map.resources.)

I have yet to figure out exactly what filter causes this requirement. Also, I would be interested in knowing if there is a way to inspect all the before filters that will be run for a given action or controller. If you know, please post in the comments.

Special thanks to Dirkjan for helping me debug this far.

From development.log

Processing BooksController#create (for 127.0.0.1 at 2007-09-11 10:53:22) [GET]
Session ID: e042ea5ba771fd5423d39369ec8e1227
Parameters: {"action"=>"create", "controller"=>"books", "journal_id"=>"1"}
Redirected to http://localhost:3005/books/list
Filter chain halted as [#<ActionController::Filters::ClassMethods::ProcFilter:0x47e198c @filter=#<Proc:0x0391ce4c@C:/InstantRails-1.7-win/InstantRails/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/verification.rb:74>>] returned false.
Completed in 0.01000 (100 reqs/sec) | DB: 0.00000 (0%) | 302 Found [http://localhost/books/create?journal_id=1]


Processing BooksController#list (for 127.0.0.1 at 2007-09-11 10:53:22) [GET]
Session ID: e042ea5ba771fd5423d39369ec8e1227
Parameters: {"action"=>"list", "controller"=>"books"}
Book Columns (0.010000) SHOW FIELDS FROM books
SQL (0.000000) SELECT count(*) AS count_all FROM books 
Book Load (0.000000) SELECT * FROM books LIMIT 0, 10
Rendering within layouts/books
Rendering books/list
Completed in 0.03000 (33 reqs/sec) | Rendering: 0.01000 (33%) | DB: 0.01000 (33%) | 200 OK [http://localhost/books/list]

How to use IRC for Ruby on Rails (#rubyonrails)

14 years ago I used IRC to chat with people online about the Grateful Dead and Phish, but haven't since, and basically forgot about 99% of how it works.

Then, recently I wanted to join the #rubyonrails IRC channel to get some instant information, so from http://www.rubyonrails.com/community I learned that #rubyonrails was hosted at irc.freenode.net. I downloaded ChatZilla and tried to join.

Frustration. #rubyonrails requires some sort of nickname registration and it took me the longest time to figure out how the heck it works.

I searched for documentation and found all sorts, but what I was found was very dry and technical and full of details. Typically, I like that type of stuff--bare bones, just the facts.

But, here I needed the dummies course, and I found it at http://www.zymic.com/irc.php. I can't vouch for that site, but thank goodness they had some basic info...it wasn't 100% correct, but it got me close enough.

In ChatZilla > Preferences > General there is a "nickname" setting. When you connect to irc.freenode.net you need to register this nickname...obviously, it should be something sort of unique. (How to connect? Open Firefox and visit the URL irc://irc.freenode.net ...which should launch ChatZilla.)

After ChatZilla connects to irc.freenode.net, you will need to register your nickname, aka "nick". To register the nick, type:

/msg nickserv register <yourpassword>

where <yourpassword> is some password of your creation. This will register your nickname with some sort of central registration database.

Then, to enter the channel type:

/join #rubyonrails

On subsequent visits to IRC, ChatZilla will send your username to nickserv, but you will need to "identify" yourself this time. To do so, type:

/msg nickserv identify <yourpassword>

Then, join the channel like you did before:

/join #rubyonrails

Personally, I think of it like this:


  • register = create a login
  • identify = login
  • join = enter a chat room

Hopefully, these instructions will help someone else out along the way.

P.S. By the way, the delay between posts is explained by the fact that I was out of town for a few weeks visiting family.