先日めでたくリリースしたTwitterボットのBe Vimmerですが、Migrationにバグがありました。
class CreateVimCommands < ActiveRecord::Migration def change create_table :vim_commands do |t| t.string :mode_id #しまった!integerだった!! t.string :command t.string :description t.timestamps end add_index :vim_commands, [:mode_id, :command], :unique end end
初回リリースバージョンではこれでも一応動いていたんですが、今朝の仕様変更を実装するために以下のコードを加えたところ、Cronの実行時にエラーが発生しました。
VimCommand.where(language: lang).joins(:mode).where("modes.label NOT LIKE 'EX%'").select('vim_commands.id')
PGError: ERROR: operator does not exist: integer = character varying LINE 1: ..."vim_commands" INNER JOIN "modes" ON "modes"."id" = "vim_com... ^ HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts. : SELECT COUNT(vim_commands.id) FROM "vim_commands" INNER JOIN "modes" ON "modes"."id" = "vim_commands"."mode_id" WHERE "vim_commands"."language" = 'en' AND (modes.label NOT LIKE 'EX%')
しかもローカルのSqlite3なら動いていたのに、HerokuのPostgresの時に初めて遭遇するというイヤらしさ!
仕方がないのでMigrationを追加してカラムの型をstringからintegerに変更しました。
class ChangeModeId < ActiveRecord::Migration def up remove_index :vim_commands, [:language, :mode_id, :command] change_column :vim_commands, :mode_id, :integer #デフォルトのインデックス名は長すぎるようなのでカスタマイズ add_index :vim_commands, [:language, :mode_id, :command], unique: true, name: "idx_l_m_c" end def down #However, string type is not valid remove_index :vim_commands, [:language, :mode_id, :command] change_column :vim_commands, :mode_id, :string add_index :vim_commands, [:language, :mode_id, :command], unique: true end end
ローカルで動作確認して、HerokuでもMigrationを実行!・・・してみたら、またエラーが(T T)
PGError: ERROR: column "mode_id" cannot be cast to type "pg_catalog.int4" : ALTER TABLE "vim_commands" ALTER COLUMN "mode_id" TYPE integer
どうもネットの情報を見ていると、Postgresの場合、特殊なFunctionを追加する必要があるようです。
How to convert a table column to another data type - Postgres OnLine Journal
しかし、動いているのはHerokuだし、どうやってFunctionなんて追加するんだ〜!?と困ってしまったんで、さらにググってみました。
するとこんな情報を発見。
ruby on rails - Altered my database structure in development, so tried to reset Heroku deployed database. Erased it but now I can't migrate my db over - Stack Overflow
この情報を参考にして、一回新しいカラムを追加して新旧のカラムをスワップするように変更しました。
class ChangeModeId < ActiveRecord::Migration def up #http://stackoverflow.com/questions/8503156/altered-my-database-structure-in-development-so-tried-to-reset-heroku-deployed remove_index :vim_commands, [:language, :mode_id, :command] rename_column :vim_commands, :mode_id, :old_mode_id add_column :vim_commands, :mode_id, :integer VimCommand.reset_column_information VimCommand.each {|e| e.update_attribute(:mode_id, e.old_mode_id.to_i) } remove_column :vim_commands, :old_mode_id add_index :vim_commands, [:language, :mode_id, :command], unique: true, name: "idx_l_m_c" end def down raise IrreversibleMigration end end
ロールバックの仕方はよくわからないので、「raise IrreversibleMigration」で済ましてしまいました。(すみません、適当で・・・)
まとめ
Localの次はいきなりProductionではなく、やっぱりHeroku上にStaging環境を用意しておいた方が良いと思いました。
意外とSqlite3とPostgresで挙動に違いがありますね。
Migrationの実行途中でエラーが起きると、手作業でのロールバックが必要になったり、最悪データベース全体をゼロから作り直したりする必要があるので、特に注意です。
以上、ご参考までに。
あわせて読みたい
これとほぼ同じ内容をもう少し詳しく書き直してみました。
Railsでカラムのデータ型を変更する場合の手順 - give IT a try