1. 程式人生 > >Rails Cookbook翻譯(三)

Rails Cookbook翻譯(三)

Rails Cookbook翻譯(三)

處方3.3 使用Migrations開發你的資料庫

問題:

你需要改變你的資料庫模式(schema):你想新增列(columns,刪除列,或者修改你的表的定義,並且當你做錯了的時候,又想能夠回滾到初始的狀態而不丟失資料。

例如你和一個開發團隊開發一個管理圖書的資料庫。由於200711日,圖書工業界開始使用新的13位的ISBN格式來標示所有的書。你想準備改變你的資料庫來應付這個轉變。

這個升級的複雜性在於你們那個組可能沒有為那個突然的轉變做好準備。你想找到一種組織方式,它能夠適用於每一個數據庫例項的改變,並且每增加一個改變都需要在版本的控制之中,你也能夠在需要的時候逆轉這種改變。

解決方案:

使用Active Record migrations 來定義在不同狀態的轉換處理。

使用generator建立兩個migrations:

$ ruby script/generate migration AddConvertedIsbn

createdb/migrate

createdb/migrate/001_add_converted_isbn.rb

$ ruby script/generate migration ReplaceOldIsbn

existsdb/migrate

createdb/migrate/002_replace_old_isbn.rb

象下面那樣定義第一個migration,

convert_isbn作為輔助方法,它提供了ISBN的轉換演算法。

db/migrate/001_add_converted_isbn.rb:

class ConvertIsbn < ActiveRecord::Migration

def self.up

add_column :books, :new_isbn, :string, :limit => 13

Book.find(:all).each do |book|

Book.update(book.id,:new_isbn=>convert_isbn(book.isbn))

end

end

def self.down

remove_column :books, :new_isbn

end

#Convert from 10 to 13 digit ISBN format

def self.convert_isbn(isbn)

isbn.gsub!('-','')

isbn = ('978'+isbn)[0..-2]

x = 0

checksum = 0

(0..isbn.length-1).each do |n|

wf = (n % 2 == 0) ? 1 : 3

x += isbn.split('')[n].to_i * wf.to_i

end

if x % 10 > 0

c = 10 * (x / 10 + 1) - x

checksum = c if c < 10

end

return isbn.to_s + checksum.to_s

end

end

第二個狀態轉換如下所示:

db/migration/002_replace_old_isbn.rb:

class ReplaceOldIsbn < ActiveRecord::Migration

def self.up

remove_column :books, :isbn

rename_column :books, :new_isbn, :isbn

end

def self.down

raise IrreversibleMigration

end

end

討論:

Active Record migration定義了一種增量式模式更新的方法。每一個migration都是一個class,它包含了為適應對資料模式的一個或一組改變的一個指令集合。在這個類中,這些操作被定義為updown兩個類方法,他定義了怎麼實施改變以及逆轉這個改變的功能。

當一個migration第一次產生時,如果資料庫中沒有schema_info表,Rails就會建立了一個名叫schema_info的表。這個表包含了一個integer型別的欄位來命名版本,這個vision欄位跟蹤了對模式最近實施migration的版本號,每一個migration都在其檔名中包含了一個唯一的版本號,(這個檔名第一部分是版本號,緊接著只下劃線,然後是常常被用來描述migration到底做什麼的檔名)

要使用migration,使用一個rake任務:

$rake db:migrate

如果這個命令不帶任何引數,rake使用比儲存在schema_info表裡的版本高的所有migration來更新schema。你也可以指定migration的版本:

$ rake db:migrate VERSION=12

你也可以使用簡單的命令讓資料庫回滾到一箇舊的版本。例如,你的schema最新版本是13,但是版本13存在問題,你可以使用上一個命令來回滾到版本12

解決方案中開始於一個擁有唯一一個books表,它包含了10-digit ISBNs的列:

mysql> select * from books; 
+----+------------+-----------------+
| id | isbn| title|
+----+------------+-----------------+
|1 | 9780596001 | Apache Cookbook |
|2 | 9780596001 | MySQL Cookbook|
|3 | 9780596003 | Perl Cookbook |
|4 | 9780596006 | Linux Cookbook|
|5 | 9789867794 | Java Cookbook|
|6 | 9789867794 | Apache Cookbook |
|7 | 9781565926 | PHP Cookbook|
|8 | 9780596007 | Snort Cookbook|
|9 | 9780596007 | Python Cookbook |
| 10 | 9781930110 | EJB Cookbook|
+----+------------+-----------------+
10 rows in set (0.00 sec) 

在這兩種狀態轉換的過程的第一部分,我添加了一個名為new_isbn新列,然後將已存在的10-digitISBN轉換後複製到新的13-isbn列。這個轉換我們使用了我們已經定義了的convert_isbn工具方法。在up方法中新增新的列,然後迭代的將所有的表中的books進行轉換,並將其結果存到new_isbn欄位。

def self.up

add_column :books,:new_isbn, :string, :limit => 13

Book.reset_column_information

Book.find(:all).each do |book|

Book.update(book.id, :new_isbn => convert_isbn(book.isbn))

end

end

我們運行了第一個migration(db/migration/001_add_converted_isbn.rb),我們使用下面這個命令(version需要大寫)來更新我們的schema

$ rake db:migrate VERSION=1

(in /home/rob/bookdb)

我們確認一下schema_info表是否已經建立,並且包含一個其值為1vesion欄位,觀察一下books表,顯示new_isbn欄位已經被正確轉換了:

mysql> select * from schema_info; select * from books;
+---------+
| version |
+---------+
|1 |
+---------+
1 row in set (0.00 sec)
+----+------------+-----------------+---------------+
| id | isbn| title| new_isbn|
+----+------------+-----------------+---------------+
|1 | 9780596001 | Apache Cookbook | 9789780596002 |
|2 | 9780596001 | MySQL Cookbook| 9789780596002 |
|3 | 9780596003 | Perl Cookbook| 9789780596002 |
|4 | 9780596006 | Linux Cookbook| 9789780596002 |
|5 | 9789867794 | Java Cookbook| 9789789867790 |
|6 | 9789867794 | Apache Cookbook | 9789789867790 |
|7 | 9781565926 | PHP Cookbook| 9789781565922 |
|8 | 9780596007 | Snort Cookbook| 9789780596002 |
|9 | 9780596007 | Python Cookbook | 9789780596002 |
| 10 | 9781930110 | EJB Cookbook| 9789781930119 |
+----+------------+-----------------+---------------+
10 rows in set (0.00 sec) 

在這裡,我們也可以通過使用VERSION=0rake命令來逆轉這個轉化。通過呼叫down方法就可以做到這點:

def self.down

remove_column :books, :new_isbn

end

這個函式刪除了new_isbn欄位並更新了schema_info version0,並不是所有的migrations都是可以逆轉的,所以你應該小心的備份你的資料庫來防止資料丟失。在我們現在這個例子中,我們丟失了所有new_isbn列的資料並不存在什麼問題,因為isbn列依然存在。

一旦所有的開發者都滿足這個新的ISBN格式,並能與他們的程式碼一起工作,我們就可以使用第二個migration來完成這個徹底的轉換:

$ rake db:migration VERSION=2

(in /home/rob/projects/migrations)

VESION=2時可選的,因為我們在向更高的版本遷移。

為了完成這個轉變,第二個migration刪除了isbn欄位,並且重新命名了new_isbn欄位來代替原來的。這個migration是不可逆轉的。如果我們降低一級版本號來使用rake命令,self.down方法就會丟擲異常。當然我們也可以定義一個self.down來重新命名這個欄位,然後重新使用原來的10-digit ibsn欄位,但顯然這是沒有必要的。

mysql> select * from schema_info; select * from books;

+---------+

| version |

+---------+

|2 |

+---------+

1 row in set (0.00 sec)

+----+-----------------+---------------+

| id | title| isbn|

+----+-----------------+---------------+

|1 | Apache Cookbook | 9789780596002 |

|2 | MySQL Cookbook| 9789780596002 |

|3 | Perl Cookbook| 9789780596002 |

|4 | Linux Cookbook| 9789780596002 |

|5 | Java Cookbook| 9789789867790 |

|6 | Apache Cookbook | 9789789867790 |

|7 | PHP Cookbook| 9789781565922 |

|8 | Snort Cookbook| 9789780596002 |

|9 | Python Cookbook | 9789780596002 |

| 10 | EJB Cookbook| 9789781930119 |

+----+-----------------+---------------+

10 rows in set (0.00 sec)