对于 migration 来说,它就是 Active record 编写的一套 DSL 用来修改数据库表的 schema. 不同于直接通过 SQL 来实现, 它能够更加容易的通过 ruby 的代码来实现.
本文主要有下面几个方面:
- 通过使用生成器来完成创建
- active record 提供的所有用来维护数据的方法
- 通过 rails 的内置 task 方法来完成migration
- migration和
schema.rb
之间的关联
migration 的概览
创建migration
创建独立的 migration
migration作为单独的文件存放在db/migration
文件夹下面, 每一个都是独立的 migration 类. 这个文件名有一定的规则就是时间戳加动作
, 如YYYYMMDDHHMMSS_create_products.rb
, 意思很明了. 就是在某个时间点,下划线加上 migration 的名称.而对于 migration 的类名, 也完全是根据文件名的移形名字来的,只是把它变成了驼峰命名. 如这个移形文件20080906120000_create_products.rb
实际对应就会生成一个CreateProducts
类, 而20080906120001_add_details_to_products.rb
同样就是AddDetailsToProducts
. 而这里文件名里的时间戳就是用来告诉 rails 如何按时间顺序来完成所有的移形. 所以有时候你图方便直接拷贝了一个移形文件, 这个时候就要注意时间戳了, 如果二者相互没有依赖可能也不会出现问题.
当然这里如果我们每次手动去拿一个时间戳然后创建一个移形文件就无趣了, rails 当然会提供方法的生成器来完成这个动作. 如下面我们创建一个移形:
$ bin/rails generate migration AddPartNumberToProducts
将会生成下面这样一个文件:
class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
end
end
这里, 如果在创建这个移形文件的时候, 文件名如果遵循了如AddXXXToYYY
或者RemoveXXXFromYYY
, active record 就会很贴心的直接在移形类里添加对应相似的方法.
$ bin/rails generate migration AddPartNumberToProducts part_number:string
class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
end
end
我们还可以直接给某个字段添加索引
$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
will generate
class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
add_index :products, :part_number
end
end
当然移除某个字段也不是问题
$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
generates
class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
def change
remove_column :products, :part_number, :string
end
end
当然一次添加多个也没有问题:
$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
generates
class AddDetailsToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
add_column :products, :price, :decimal
end
end
当然你可以写成只是创建某个对象CreateXXX
, 然后跟多个字段的形式, 完全没有问题(我说, 这样好吗 有必要吗?)
$ bin/rails generate migration CreateProducts name:string part_number:string
generates
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.string :part_number
end
end
end
其实, 上面各种各样的玩法也是一个开始, 很多时候你大可完全直接去编辑db/migrate
目录下的如YYYYMMDDHHMMSS_add_details_to_products.rb
文件来得快.
说到这里migrate
同样支持关联聚合关系的创建.( belongs_to)
$ bin/rails generate migration AddUserRefToProducts user:references
generates
class AddUserRefToProducts < ActiveRecord::Migration[5.0]
def change
add_reference :products, :user, index: true, foreign_key: true
end
end
这个 migrate 将会创建一个user_id
的外键,顺便给它索引. 关于add_reference
还有很多的选项可以看看 API 文档.
当然如果在 migration 的文件名添加了JoinTable
的话, 可以直接创建生成jointable
的命令.
$ bin/rails g migration CreateJoinTableCustomerProduct customer product
will produce the following migration:
class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
def change
create_join_table :customers, :products do |t|
# t.index [:customer_id, :product_id]
# t.index [:product_id, :customer_id]
end
end
end
对象生成器
对于对象生成器
或者骨架生成器
他们来说, 在使用他们的时候同时已经为你生成了对象的 migration, 这个 migration 已经包含了创建相关表, 如果你告诉 Rails 添加什么字段上面的都会为了一气搞定. 看看下面的例子:
$ bin/rails generate model Product name:string description:text
对于上面的对象生成器的执行结果就是会生成下面的 migration:
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.text :description
t.timestamps
end
end
end
这里你可以添加任意多的字段声明
使用类型定义
我们可以在使用生成器的时候直接给字段定义类型定义声明, 通过使用大括号的方式给它一个类型:
$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
将会生成这样的定义:
class AddDetailsToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :price, :decimal, precision: 5, scale: 2
add_reference :products, :supplier, polymorphic: true, index: true
end
end
有很多玩法了, 看看 API 文档好了.
怎么写一个migration
当我们通过生成器生成了 migration 之后,才刚开始.
创建表
通过create_table
我们可以创建表,算是最基本的函数了. 但是通常我们通过大部分的生成器就可以完成这个步骤, 看上去就像下面这样的:
create_table :products do |t|
t.string :name
end
这个将会创建一个products
的表, 然后一个name
的字段.(同时还有一个id
的字段). 默认得create_table
命令将会创建一个id
的主键. 你可以通过:primary_key
的选项来修改这个默认值, 但是这个情况下就需要修改对应的 model. 当然你可以不要这个主键, 只要给id:false
的参数就 ok. 如果你需要设置数据库特定的参数的时候直接在option
的参数里指定就可以. 如下面的例子:
create_table :products, options: "ENGINE=BLACKHOLE" do |t|
t.string :name, null: false
end
这个字符串会追加到创建数据库表的SQL
语句后,(在使用MySQL
或者MariaDB
的时候, 默认的引擎是InnoDB
)
同时你也完全可以通过
: comment
的选项给数据库表添加任何的注释.这个动作也就是通过追加选项注释到实际的 SQL 文实现.
创建 Join 表
migration 通过create_join_table
来创建HABTM
的关联表, 通常是这样的:
create_Join_table :products, :categories
这个将会创建一个categories_products
表, 有两个字段category_id
还有product_id
, 这些字段都被设置的: null
的选项,默认为false
.
create_join_table :products, :categories, column_options: { null: true }
默认最后这个关联表的名字将是来自于创建 migration 的时候给的2个表明通过字母排序得到的. 同时可以通过: table_name
的参数来指定表名.
create_join_table :products, :categories, table_name: :categorization
它将会创建一个categorization
的表.
create_join_table
还可以接受代码块,这样就可以添加任意的设置.或者额外的字段.
create_join_table :products, :categories do |t|
t.index :product_id
t.index :category_id
end
修改表
create_table
的表兄就是change_table
了. 用来修改现有的表结构,通常它和创建表的构成一样.但是代码块里yeild
出来的对象有更多神奇的用法.
change_table :products do |t|
t.remove :description, :name
t.string :part_number
t.index :part_number
t.rename :upccode, :upc_code
end
上面的定义是分别移除了description
和name
字段,添加了part_time
的字段并且给它添加了索引, 还有将upccode
重命名为upc_code
.
修改字段
还有和remove_column
与add_column
命令相似的change_column
,
change_column :products, :part_number, :text
这个动作就是将products
表里的part_number
设置为: text
类型, 注意 这里的操作是不可逆的.
另外还有change_column_null
和change_column_default
方法, 他们用来修改特定字段的约束和默认值.
change_column_null :products, :name, false
change_column_default :products, :approved, from: true, to: false
上面的动作就是将: name
字段添加不可空的约束,然后给approved
的默认值是false
.
注意上面这个动作可以通过
change_column
来完成, 但是那样就变成了不可逆的操作
字段修饰符
字段的类型定义可以在创建或者修改的时候设置.
-
limit
可以给string/text/binary/integer
类型的字段设置最大值. -
precision
定义了decimal
字段的precision
, 也就是多大. -
scale
,定义了 decimal 字段的精度, 也就是小数点后面的位数. -
polymorphic
给字段添加belong_to
的关联. -
null
, 字段是否可为空 -
default
给字段添加默认值, 注意 如果你使用动态值(如日期)默认值将会在第一次使用的时候指定. -
index
就是给某个字段添加索引 -
comment
就是给字段添加注释
有些特定的适配器可能会针对特定的数据库有特定的选项可以使用. 查阅 API.
外键
虽然不是要求的, 但是你可能会通过添加外键约束来 确保引用一致性.
add_foreign_key :articles, :authors
这个动作的结果就是会给articles
这个表添加一个外键author_id
, 它就是来自author
表的主键id
, 如果仅是通过上面的命令而不能够得到外键名,可以通过:column
和: primary_key
选项来指定.
Rails 通常会通过from_table
和column
两个变量来给每一个外键生成一个以fk_rails_
开头的10字符名字.同时也可以通过: name
选项来指定一个不同的名字.
Active record只支持单外键, execute and structure.sql are required to use composite foreign keys. See Schema Dumping and You.
移除外键也是很轻松的:
# let Active Record figure out the column name
remove_foreign_key :accounts, :branches
# remove foreign key for a specific column
remove_foreign_key :accounts, column: :owner_id
# remove foreign key by name
remove_foreign_key :accounts, name: :special_fk_name
当帮助方法不够用的情况
如果发现active record
提供的帮助方法不够用的时候可以直接执行SQL
来实现.
Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")
更多详情和单独的方法, 可以通过确认 API 文档来学习了解, 这里提供了 change up 和 down 的方法, 创建表的更多, 还有修改表.
修改表
使用 migration 的时候,更多的就是通过change
方法. 这个会对于大多数 case 管用. 因为 active record 都可以轻松 hold 住,当前支持这些方法:
add_column
add_foreign_key
add_index
add_reference
add_timestamps
change_column_default (must supply a :from and :to option)
change_column_null
create_join_table
create_table
disable_extension
drop_join_table
drop_table (must supply a block)
enable_extension
remove_column (must supply a type)
remove_foreign_key (must supply a second table)
remove_index
remove_reference
remove_timestamps
rename_column
rename_index
rename_table
change_table
只要不在代码块里使用change
, 或者chagne_default
或者remove
方法. 都是可以回滚撤销的. remove_column
也是可以回滚的, 只要在执行移除的时候告诉 rails 当前要移除的字段的类型就可以回滚.
remove_column :posts, :slug, :string, null: false, default: '', index: true
当然你在使用其他方法的时候,可以通过reversible
方法和写up
和down
方法来替换使用change
方法.
使用reversible
复杂的 migration 可能Active Record
并不知道如何来回滚操作,但是可以通过reversible
方法 来表明当你想要 migration 或者回滚的时候怎么办.
class ExampleMigration < ActiveRecord::Migration[5.0]
def change
create_table :distributors do |t|
t.string :zipcode
end
reversible do |dir|
dir.up do
# add a CHECK constraint
execute <<-SQL
ALTER TABLE distributors
ADD CONSTRAINT zipchk
CHECK (char_length(zipcode) = 5) NO INHERIT;
SQL
end
dir.down do
execute <<-SQL
ALTER TABLE distributors
DROP CONSTRAINT zipchk
SQL
end
end
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
end
通过使用reversible
可以来确保正确的 SQL 执行顺序,正如上面的例子里的目的, 如果 example 被回滚了, 这个down
块就会被执行, 结果就是移除home_page_url
, 然后drop 掉distrubutors
表.
当时通常呢, migration 是会做一点不可恢复的事情, 例如往往会删除掉一些数据的. 这样的情况下,将会在 down 的方法里报ActiveRecord::IrreversibleMigration
的错误.
使用up/down
方法
你可以使用老的方式up
和down
来代替change
方法.也就是说up
方法会描述如何转换当前的 schema, 还有down
方法会将up
方法的一切撤销. 换句话说, 数据库的 schema 会在 up 和 down 之后什么都没有发生一样. 举个例子, 在 up 方法里创建了一张表, 而在 down 的方法里就要 drop 掉这个表.应该很明确的表达回滚的顺序. 下面的写法和 reversible
的写法是等效的:
class ExampleMigration < ActiveRecord::Migration[5.0]
def up
create_table :distributors do |t|
t.string :zipcode
end
# add a CHECK constraint
execute <<-SQL
ALTER TABLE distributors
ADD CONSTRAINT zipchk
CHECK (char_length(zipcode) = 5);
SQL
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
def down
rename_column :users, :email_address, :email
remove_column :users, :home_page_url
execute <<-SQL
ALTER TABLE distributors
DROP CONSTRAINT zipchk
SQL
drop_table :distributors
end
end
如果操作是不可逆的,应该报ActiveRecord::IrreversibleMigration
这个错误异常, 如果某人尝试回滚操作,需要提示异常.
回滚
通过使用revert
方法可以让 active record 回滚操作.
require_relative '20121212123456_example_migration'
class FixupExampleMigration < ActiveRecord::Migration[5.0]
def change
revert ExampleMigration
create_table(:apples) do |t|
t.string :variety
end
end
end
这个revert
方法可以通过块来指定到底怎么如何回滚,这个方式可以来回滚特定部分的操作,例如,已经提交了ExampleMigration
然后却想要通过使用 active record 的校验来代替CHECK
的约束, 来达到校验zipcode
.
class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
def change
revert do
# copy-pasted code from ExampleMigration
reversible do |dir|
dir.up do
# add a CHECK constraint
execute <<-SQL
ALTER TABLE distributors
ADD CONSTRAINT zipchk
CHECK (char_length(zipcode) = 5);
SQL
end
dir.down do
execute <<-SQL
ALTER TABLE distributors
DROP CONSTRAINT zipchk
SQL
end
end
# The rest of the migration was ok
end
end
end