Rails Cookbook翻译(三)
处方3.3 使用Migrations开发你的数据库
问题:
你需要改变你的数据库模式(schema):你想添加列(columns),删除列,或者修改你的表的定义,并且当你做错了的时候,又想能够回滚到初始的状态而不丢失数据。
例如你和一个开发团队开发一个管理图书的数据库。由于2007年1月1日,图书工业界开始使用新的13位的ISBN格式来标示所有的书。你想准备改变你的数据库来应付这个转变。
这个升级的复杂性在于你们那个组可能没有为那个突然的转变做好准备。你想找到一种组织方式,它能够适用于每一个数据库实例的改变,并且每增加一个改变都需要在版本的控制之中,你也能够在需要的时候逆转这种改变。
解决方案:
使用Active Record migrations 来定义在不同状态的转换处理。
使用generator创建两个migrations:
$ ruby script/generate migration AddConvertedIsbn
create db/migrate
create db/migrate/001_add_converted_isbn.rb
$ ruby script/generate migration ReplaceOldIsbn
exists db/migrate
create db/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,它包含了为适应对数据模式的一个或一组改变的一个指令集合。在这个类中,这些操作被定义为up和down两个类方法,他定义了怎么实施改变以及逆转这个改变的功能。
当一个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表是否已经创建,并且包含一个其值为1的vesion字段,观察一下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=0的rake命令来逆转这个转化。通过调用down方法就可以做到这点:
def self.down
remove_column :books, :new_isbn
end
这个函数删除了new_isbn字段并更新了schema_info version为0,并不是所有的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)