Migration 資料庫更動
filed in Uncategorized on Apr.26, 2007
注意: Migration 有新的文法囉,修飾中 2008/03/17
為甚麼要用 Migration 呢?
我如果寫了一個很好的程式放在網路上給大家使用,然後又添加了很多功能,這時要怎麼把舊資料庫的 schema 更新呢? 如果有人想把軟體版本從第三版更新到第九版,另一個人又想把第四版更新到第六版, 又要怎麼更新呢? 如果有一幫人用的資料庫是 mySQL, 另一幫人用的是 Postgre, 那要怎麼辦? 別擔心,救星 Migration 來了!
寫軟體的模式通常是很快的寫出第一版,公佈給大家使用後再出功能比較強的第二,三版。 但從一版到另一版之中,資料庫的欄位不斷的增加和減少。 Migration 這套系統好用的地方就是可以把所有變動的地方紀錄在文字檔裡,然後打個 “rake migrate” 的指令,系統就會把現在用的版本更新到最新版。 這魔術是如何變的呢?我們就一步一步看吧。
我們用從頭寫一個簡單的程式叫”dog”來紀錄不同種類的狗。
rails dog
這時候螢幕會出現一大堆自動產生在 “dog” 目錄下的東西。
Migration 基本操作
使用 Migration 前的準備
1)資料庫設定
在開始寫 model 前,要把資料庫連線的訊息設好。 還記得要去那邊設嗎? 沒錯,就是 dog/config/database.yml 裡。 在 development 的地方輸入你要的資料庫名稱,使用者名稱,跟密碼。 我不介意用它自動填的 dog_development 來當資料庫。我的使用者名稱也是 root, 而且也沒有密碼,所以我不用改資料。但你如果這些值有自訂的話,要記得改喔。
2)建立 dog_development 資料庫
用資料庫管理程式來建立一個叫 “dog_development” 的資料庫。 這個資料庫是我們現在開發用的,所以才會叫 “development” 如果是測試用的我們就應該用另一個新的資料庫 “dog_testing”。 這一篇沒有講到 testing 所以先不用管 “dog_testing” 這個資料庫。
產生 model
設定好了後,我們可以開工了。下這個指令:
ruby script/generate model Dog
螢幕上出現的幾行字裡會看到一個
create db/migrate/001_create_dogs.rb
這表示 rails 的程式碼產生器已自動產出第一個資料庫 schema 的定義檔。 把 db/migrate/001_create_dogs.rb 打開。 第一版的資料庫裡要有什麼 table, table 裡要有什麼欄位等等都要寫在這裡面。 打開檔案之後你會看到有一個 class 叫 CreateDogs, 那個 class 有兩個 function, 叫 self.up 跟 self.down。 所有的 Migration 所產生的檔案將會有 self.up 跟 self.down。 Migration 看 self.up 的內容來決定升級的時候要做甚麼。如果要降級的話 Migration 看 self.down 的內容。
self.up
看看 self.up 的內容。 create_table :dogs … 表示如果要從 0 版升級到 1 版的話,我們要新增一個叫做 dogs 的 table。 當初 程式碼產生器知道要把 table 命名為 dogs 是因為我們叫它產生一個 Dog 的 model。 根據 rails 的命名模式,如果 model 叫 Dog, table 就叫 dogs, controller 就叫 dog_controller.rb 等等。 在 “t.cloumn :name, :string” 的地方就是要放新增的欄位名稱跟 datatype。
def self.up
create_table :dogs do |t|
t.column :breed, :string
t.column :avg_size, :string
t.column :aggressive, :boolean
end
create_table :cats do |t|
t.column :breed, :string
t.column :avg_size, :string
end
end
Datatype
上面寫的程式碼並不是 SQL, 原因是 Migration 可通用在不同的 database 上。如果想把 mySQL 換成 Postgre 的話,這些 Migration 檔案是不用改的。 Migration 會自動用資料庫了解的語言跟資料庫溝通。 Migration 所知道的 datatype 如下:integer, float, datetime, date, timestamp, time, text, string, binary, boolean。
id 欄位
看了上面這段碼之後你會覺得說,咦?怎麼沒有一個叫 id 的欄位?不是每個 rails 的 table 都要這個欄位嗎? 沒錯,就是因為這個原因我們才不用自己加。 既然每個 table 都要, 我們不用寫,Migration 就會自動加入 id 這欄位了。 但如果你有特別需求不要加 id 欄位, 加 ” :id => false ” 就好:
create_table :dogs, :id => false do |t| ...
注意: ” :id => false ” 這個東西是不必要設的,除非確定不要 “id” 這個欄位。
self.down
來看看 001_create_dogs.rb 裡 self.down 的內容。 我們如果更新軟體,推出去賣後又發現問題一大堆,這時要把資料庫還原成上一版的,又該怎麼還原呢? 依照 001_create_dogs.rb 這個檔案來說,如果 Migration 執行了這個檔,兩個 table 就會跑出來: cats 跟 dogs, 如果要還原成上一版的話,就要把 cats 跟 dogs 刪掉,所以在 self.down 程式裡,就該放 drop table 的指令。
def self.down
drop_table :dogs
drop_table :cats
end
001_create_dogs.rb 改好後記得要存檔喔。
執行 Migration
好了,總算可以變魔術了。下這個指令:
rake migrate
版本紀錄
下這個指令後連到資料庫裡面去看,應該會看到 dogs 跟 cats 這兩個 table,table 裡面也有我們剛剛加的欄位!好耶!不用學 SQL, Migration 就會幫我們增加欄位了。 資料庫再仔細看一下。 咦? 怎麼會多了一個叫 “schema_info” 的 table? 點進去看看,它只有一個欄位叫 “version”,裡面有一行資料寫著 “1″。 喔,原來這個 table 就是 Migration 用來紀錄我們目前用的版本是那版。 假如裡面登記的是 “3″ 的話,我們用的就是第三版。如果 dog/db/migration/ 下有到 006_xxxxx.rb 開頭的檔案,在跑 rake migrate 的時候,Migration 就會遵照 004_xxxx.rb, 005_xxxxxxx.rb, 和 006_xxxxx.rb 裡的內容來升級資料庫。
資料庫現有的 schema
我們的資料庫 schema 會改來改去,但目前資料庫的 schema 是長的甚麼樣子? 要看的話,就去看 dog/db/ 裡的一個檔案叫 schema.rb。 打開後你也可以看到裡面的版本是第一版:
ActiveRecord::Schema.define(:version => 1) do ....
這個檔案專門是 Migration 在用所以不能改喔。 如果這檔案不小心被你家的貓亂按鍵盤刪掉的話,不用擔心,Migration 可再生一個給你。 在 dog/ 的目錄下指令:
rake db_schema_dump
這個指令會把現有的資料庫狀態紀錄回 dog/db/shema.rb 裡。
如果你的資料庫垮了,可是你有把 dog/db/schema.rb 備份起來,要還原的話只要把備份的 schema.rb 放回 dog/db/ 目錄下,然後在 dog/ 的目錄下指令:
rake db_schema_import
注意:這動作會把舊的 table 跟其內容刪掉。
升級到第二版
我們的 database 用了一陣子之後,發覺到貓的體型都差不多,所以不需要 “avg_size” 這個欄位。 還有,我們想紀錄不同種類貓毛的長度,因為有人覺得長毛貓的毛很難清,所以我們要多加一個欄位叫 hair_length。 要改變的話如下:
在 dog/ 目錄下產生一個 migration 檔:
ruby script/generate migration change_cat_table
下這指令後你會看到 dog/db/migration/ 檔案夾裡有多一個檔案叫 002_change_cat_table.rb。 把這個檔案打開後你會看到一個 class 叫 ChangeCatTable。除了前面 002 的編碼跟 “_” 之外,”ChangeCatTable” 這個 class 的名稱 一定要跟 “002_change_cat_table.rb” 拼的一樣。 “change_cat_table” 這個名字是隨便我們取的,但注意以下的規定:
a) 名字中不要有 “-”。 例如 change-cat-table 是不對的。
b)名字不要跟 model 的名字一樣,像 002_dog 是不對的,因為我們已有一個 model 叫 Dog.
c) Migration 的檔名不要一樣,像 002_change_cat_table.rb 跟 003_change_cat_table.rb 是不能同時存在的。
修改 002_change_cat_table.rb 的內容
class ChangeCatTable < ActiveRecord::Migration
def self.up
add_column :cats, :hair_length, :integer, :null => false
remove_column :cats, :avg_size
end
def self.down
remove_column :cats, :hair_length
add_column :cats, :avg_size, :string
Cat.find(:all).each { |cats| cats.update_attribute :avg_size, "unknown" }
end
end
哇!這個檔案是在搞什麼?一行一行來看吧。
add_column 是增加 column 的指令。
:cats 是 table 的名字。
:hair_length 是我們要新增的欄位。
:integer 是 datatype。
:null => false 表示這個欄位不能空白。 除了 :null => false, 其它可用的設定值有:
:limit => 34 表示欄位的寬度最多只能有 34 個字母
:default => “gymnasium” 會把 “gymnasium” 當為預設值,也就是說每一筆新的資料在這欄位裡一剛開始的資料會是 “gymnasium” 這個字串。
回到上面的範列,remove_column 是刪除 column 的指令。 self.down 裡的內容我們等一下在討論,因為升級時用不到它。
執行 rake migrate
在 dog/ 目錄下
rake migrate
指令之後,我們去看資料庫,果真 cats 的 table 裡現在只有三個欄位: id, breed, 跟 hair_length。

我們再來看看 dog/db/schema.rb 檔。 耶! “version =>” 變成 2 了!

降級版本
在降級版本之前我們要先建一個 Cat 的 model 才能展現 migration 的功能。請在 db/ 下此指令:
ruby script/generate model Cat --skip-migration
-skip-migration 是叫 Migration 不要自動生出一個 003_xxx.rb 檔,因為我們已經有 cats 這個 table 了,不需要用 Migration 來生另外一個。
model 產生好之後,記得去 cats 的 table 裡加幾行假資料,加完後準備手續就完畢了!

我們現在用的資料庫版本是二,以後如果有陸續寫出三,四,五版的話,就可以任意的從第二版跳到任何版本。 下指令時只要指定您要的版本就可以。
因為我們現在只有寫到第二版,所以只能從第二版跳回第一版。要怎麼跳呢?下這指令吧:
rake migrate VERSION=1
下這指令後,Migration 會去資料庫找出 schema_info 的 table. 還記得那裡面只有一個欄位吧? 那個欄位裡有個 “2″,所以 Migration 知道它必須從第二版降級到第一版。 降級的步驟當然是要去 002_change_cat_table.rb 裡的 self.down 找啦。 好,那我們現在就來看 self.down 裡面到底是在做甚麼。
第一行 “remove_column…” 是移除 “hair_length” 這個欄位。 為啥要移除呢? 因為這個欄位是從第一版升到第二版時加的,所以要從第二版降回第一版時就應該把這欄位刪掉。
第二行 add_column… 是把以前刪掉的 “avg_size” 欄位加回來。
第三行 “Cat.find….” 就比較複雜了。 我們要加回 “avg_size” 這個欄位,但如果資料庫裡已經有資料的話,我們並不知道每行以前的 “avg_size” 裡的值是甚麼,所以只好放 “unkown” 值進去。 Cat.find(:all) 就是去 cats 的 table 裡把所有的資料撈出來。 Cat.find(:all).each 就是把撈出來的資料一筆一筆的放進去 { } 裡面的程式來處理。 |cats| 就代表每一筆放進去的資料。 每筆放進去的資料它會叫 “update_attribute” 的功能。 :avg_size 是要作業的欄位。 “unkown” 是要放入作業欄位的值。 一執行後,:avg_size 這欄位會被加回去,但所有紀錄裡的值都是 “unknown”。
解釋了那麼多,來看看資料庫有沒有被還回第一版。有耶!cats 的 table 裡面 “hair_length” 不見了,”avg_size” 回來了,而且每行放進去的假資料裡都有 “unkown”。

介紹到此,基本的 Migration 功能都有碰到了,如要特殊的客製化,看看下面不同的話題。
不要自動產出 Migration 檔
剛剛看過了,如果不要讓電腦自己產生 001_xxx.rb 等的 Migration 檔的話,在下 generate 指令時加個 “–skip-migration” 指令
ruby script/generate model Cat --skip-migration
或是把它生出來的檔案刪掉也可以。
設定 table 的 encoding
Table 裡存的資料不會每次都存英文的,如要存中文的話,可以在建 table 時把整個 table 的編碼改成 UTF8
create_table(:dogs, :options => 'DEFAULT CHARSET=UTF8') do |t|
多欄位的索引
如要加多欄位的索引 (multiple-column index), 傳入欄位名稱即可。
def self.up add_index :cars, [:license_plate, :state] end
在這個例子我們的 table 是 “cars”, 索引是由 “license_plate” 跟 “state” 的欄位組成。
自己加或刪除 foreign key
目前 Migration 還沒支援 foreign key,所以你如果有用到 foreign key 的話要自己下 SQL 指令。 把 SQL指令放在 self.up 或 self.down 裡頭就好:
execute 'ALTER TABLE employees ADD CONSTRAINT fk_employees_unions FOREIGN KEY ( union_id ) REFERENCES unions(id);
上面這一行純粹是 SQL 指令。 它在 employees 的 table 裡加了一個 foreign key 叫 union_id,對印 unions table 裡的 id 欄位。
加了 foreign key 之後,要刪除 table 時資料庫一定會抱怨,這時只要先下指令叫資料庫不要查是否有 foreign key:
def self.down execute "SET FOREIGN_KEY_CHECKES=0" drop_table "employees" ... end
強行刪掉舊的 table
如果資料庫裡已有你要新增的 table, Migration 跑一半會失敗,並抱怨說 table 已經有在資料庫了。 要強制把舊的 table 刪掉並叫資料庫不要抱怨的話,加個 :force => true
create_table "toys", :force => true do ...
可通用的 Database
除了 DB2 之外,寫出來的 Migration 檔案可通用在以下的資料庫:MySQL, PostgreSQL, SQLite, SQL Server, Sybase, Oracle。
常用到的 Migration 指令
* 新增 table: create_table(table 的名稱, 其他變數)
* 刪除 table: drop_table(table 的名稱)
* 改 table 名稱: rename_table(舊 table 名稱, 新 table 名稱)
* 新增 column: add_column(table 的名稱, column 的名稱, type, 其他變數)
* 改 column 名稱: rename_column(table 的名稱, 舊 column 的名稱, 新 column 的名稱)
* 改 column 格式: change_column(table 的名稱, column 的名稱, type, 其他變數)
* 刪除 column: remove_column(table 的名稱, column 的名稱)
* 增加 index: add_index(table 的名稱, column 的名稱, index_type)
* 移除 index: remove_index(table 的名稱, column 的名稱)
對這些功能的用法如果還有問題的話可以去看看 Migration 的 API 喔。
May 16th, 2007 on 3:04 pm
寫的很棒喔,謝謝你的分享
December 12th, 2007 on 1:17 am
剛學沒多久,不過發現…
rake migrate
似乎不能用,需用:
rake db:migrate
December 12th, 2007 on 2:09 am
對阿,語法改了,我也把 blog 更新了,謝謝。