Programmingtoday is a race between software engineers striving to build bigger and betteridiot-proof programs, and the Universe trying to produce bigger and betteridiots. So far, the Universe is winning. - Rick Cook
Migrations(数据库迁移)可以让你用 Ruby 程序来修改数据库结构。相较于直接进数据库系统使用 SQL 修改结构(例如使用 phpMyAdmin 工具来修改),使用 Migrations 可以让我们有记录地进行数据库修改,每次变更就是一笔 Migration 记录。在没有 Migration 之前,如果你手动修改了数据库,那么你就必须通知其他开发者也进行一样的修改步骤。另外,在正式布署的服务器上,你也必须追踪并执行同样的变更才行。而这些步骤如果没有记录下来,就很容易出错。
Migrations 会自动追踪哪些变更已经执行过了、那些还没有,你只要新增 Migration 档案,然后执行 rake db:migrate 就搞定了。它会自己搞清楚该跑哪些 migrations,如此所有的开发者和正式布署的服务器上,就可以轻易的同步最新的数据库结构。另外一个优点是: Migration 是独立于数据库系统的,所以你不需要烦恼各种数据库系统的语法差异,像是不同型态之类的。当然,如果要针对某个特定数据库系统撰写专属功能的话,还是可以透过直接写 SQL 的方式。
新增一个 Migration 档案
执行以下指令,就会在db/migrate/ 目录下产生如20110203070100_migration_name.rb 的档案
rails g migration migration_name
注意到在 migration_name.rb 前面有着如 YYYYMMDDHHMMSS的时序前置,用来表明执行的顺序。在早先的 Rails 版本中,是使用编号 1,2,3 来指名执行的顺序,但是如果有不同分支多人开发就可能会有重复的编号,因此在 Rails 2.1 之后的版本改采用时间戳章,让 Rails 能够应付多人开发的状况。
migration_name 常见的命名方式有Add
欄位名
To
表格名
或是Remove
欄位名
From
表格名
,不过这没有一定,能描述目的即可。
让我们打开这个档案看看:
class MigrationName < ActiveRecord::Migration
def up
end
def down
end
end
在这个类别中,包含了两个类别方法分别是up 和 down。其中 up 会在执行这个 migration 时执行,反之 down 会在滚回(Roll back)这个 Migration 时执行。例如,我们在 up 时新增一个数据库表格(table),那么就可以在 down 的时候把这个table删除。
Migration 可用的方法
在up或down方法里,我们有以下方法可以使用:
对数据表做修改:
-
create_table(name,options) 新增数据表
-
drop_table(name)移除数据表
-
rename_table(old_name,new_name) 修改数据表名称
-
change_table修改数据表字段
个别修改数据表字段:
-
add_column(table,column, type, options) 新增一个字段
-
rename_column(table,old_column_name, new_column_name) 修改域名
-
change_column(table,column, type, options) 修改字段的型态(type)
-
remove_column(table, column) 移除字段
新增、移除索引:
-
add_index(table,columns, options) 新增索引
-
remove_index(table,index) 移除索引
记得将所有外部键 foreign key 加上索引
新增和移除 Table
执行 rails g model 时,Rails就会顺便新增对应的 Migration 档案。以上一章产生的categoriesmigration为例:
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.string :name
t.integer :position
t.timestamps
end
add_column :events, :category_id, :integer
add_index :events, :category_id
end
end
其中的 timestamps 会建立叫做 created_at 和 updated_at 的时间字段,这是Rails的常用惯例。它会自动设成数据新增的时间以及会后更新时间。
疑,这里怎么不是用up
和down
方法? Rails 3.1 版新增了change
方法可以很聪明的自动处理大部分down
的情况,上述情况的down
就是移除catrgories
数据表和移除events
的category_id
字段,因此就不需要分别写up
和down
了。如果Rails无法判断,会在跑rake db:migrate时提醒你不能用change
,需要分开写up
和down
。
修改 Table
我们来试着新增一个字段吧:
rails g migration add_description_to_categories
打开db/migrate/20110411163049_add_description_to_categories.rb
class AddDescriptionToCategories < ActiveRecord::Migration
def change
add_column :categories, :description, :text
end
end
完成后,执行bundleexec rake db:migrate
便会实际在数据库新增这个字段。
数据库的字段定义
为了能够让不同数据库通用,以下是Migration中的数据型态与实际数据库使用的型态对照:
Rails中的型态 |
说明 |
MySQL |
Postgres |
SQLite3 |
:string |
有限长度字符串 |
varchar(255) |
character varying(255) |
varchar(255) |
:text |
不限长度文字 |
text |
text |
text |
:integer |
整数 |
int(4) |
integer |
integer |
:float |
浮点数 |
float |
float |
float |
:decimal |
十进制数 |
decimal |
decimal |
decimal |
:datetime |
日期时间 |
datetime |
timestamp |
datetime |
:timestamp |
时间戳章 |
datetime |
timestamp |
datetime |
:time |
时间 |
time |
time |
datetime |
:date |
日期 |
date |
date |
date |
:binary |
二进制 |
blob |
bytea |
blob |
:boolean |
布尔值 |
tinyint |
boolean |
boolean |
:references |
用来参照到其他Table的外部键 |
int(4) |
integer |
integer |
另外,字段也还有一些参数可以设定:
-
:null
是否允许NULL,默认是允许 -
:default
默认值 -
:limit
用于string、text、integer、binary指定最大值
例如:
create_table :events do |t|
t.string :name, :null => false, :limit => 60, :default => "N/A"
t.references :category #
等同於
t.integer :category_id
end
参考数据:ActiveRecord::ConnectionAdapters::TableDefinition
域名惯例
我们已经介绍过了 timestamps方法会自动新增两个时间字段,Rails 还保留了几个名称作为惯例之用:
域名 |
用途 |
id |
默认的主键域名 |
{tablename}_id |
默认的外部键域名 |
created_at |
如果有这个字段,Rails便会在新增时设定时间 |
updated_at |
如果有这个字段,Rails便会在修改时设定时间 |
created_on |
如果有这个字段,Rails便会在新增时设定时间 |
updated_on |
如果有这个字段,Rails便会在修改时设定时间 |
{tablename}_count |
如果有使用 Counter Cache 功能,这是默认的域名 |
type |
如果有这个字段,Rails便会启动STI功能(详见ActiveRecord章节) |
lock_version |
如果有这个字段,Rails便会启动Optimistic Locking功能(详见ActiveRecord章节) |
Migration 搭配的 Rake任务
-
rakedb:create 依照目前的 RAILS_ENV 环境建立数据库
-
rakedb:create:all 建立所有环境的数据库
-
rakedb:drop 依照目前的 RAILS_ENV 环境删除数据库
-
rakedb:drop:all 删除所有环境的数据库
-
rakedb:migrate 执行Migration动作
-
rakedb:rollback STEP=n 回复上N个 Migration 动作
-
rakedb:migrate:up VERSION=20080906120000 执行特定版本的Migration
-
rakedb:migrate:down VERSION=20080906120000 回复特定版本的Migration
-
rakedb:version 目前数据库的Migration版本
-
rakedb:seed 执行 db/seeds.rb 加载种子数据
如果需要指定Rails环境,例如production,可以输入 RAILS_ENV=production rake db:migrate
种子数据 Seed
种子数据Seed的意思是,有一些数据是应用程序跑起来必要基本数据,而这些数据的产生我们会放在db/seeds.rb这个档案。例如,让我们打开来,加入一些基本的Category数据:
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first)
Category.create!( :name => "Science" )
Category.create!( :name => "Art" )
Category.create!( :name => "Education" )
输入rakedb:seed
就会执行这个档案了。通常执行的时机是第一次建立好数据库和跑完Migration之后。
数据 Migration
Migrations 不只可以用来变更数据表定义,它也很常用来迁移数据。新增或修改字段时,还蛮常也需要根据现有的数据,来设定新字段的值。这时候我们就会在 Migration 利用 ActiveRecord 来操作数据。
不过,如果你在Migration中修改了数据表字段,随即又使用这个Model来做数据更新,那么因为Rails会快取数据表的字段定义,所以会无法读到刚刚修改的数据表。这时候有几个办法可以处理:
第一是呼叫reset_column_information 重新读取数据表定义。
第二是在 Migration 中用 ActiveReocrd::Base 定义一个新的空白 Model 来暂时使用。
第三是用 execute 功能来执行任意的 SQL。
Production上跑Migration注意事项
当有上万笔数据的时候,如果有修改数据库表格ALTER TABLE
的话,他会Lock table无法写入,可能会跑好几个小时很难事前预估。建议用staging server用接近production的数据来先测试会跑多久。
-
http://www.engineyard.com/blog/2011/making-migrations-faster-and-safer/
-
http://backstage.soundcloud.com/2011/05/introducing-the-large-hadron-migrator-3/
bulk参数
:bulk=> true
可以让变更数据库字段的Migration更有效率的执行,如果没有加这个参数,或是直接使用add_column
、rename_column
、remove_column
等方法,那么Rails会拆开SQL来执行,例如:
change_table(:users) do |t|
t.string :company_name
t.change :birthdate, :datetime
end
会产生:
ALTER TABLE `users` ADD `im_handle` varchar(255)
ALTER TABLE `users` ADD `company_id` int(11)
ALTER TABLE `users` CHANGE `updated_at` `updated_at` datetime DEFAULT NULL
加上:bulk =>true
之后:
change_table(:users, :bulk => true) do |t|
t.string :company_name
t.change :birthdate, :datetime
end
会合并产生一行SQL:
ALTER TABLE `users` ADD COLUMN `im_handle` varchar(255), ADD COLUMN `company_id` int(11), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL
这对已有不少数据量的数据库来说,会有不少执行速度上的差异,可以减少数据库因为修改被Lock锁定的时间。
更多在线资源
-
Rails 指南手册: Migrations(数据库迁移)http://guides.ruby.tw/rails3/migrations.html