数据库迁移
migration, 是 DB migration 的缩写。 这要写起来的话,也是厚厚的一本书。
migration 是什么?
我们修改 DB 的时候,原始的办法是:
- 手动修改数据库的表 Users,增加一列 name ( 通过 SQL 语句:
alter table ...
) - 回来修改 ruby 代码,调用 User.first.name
- 但是,你的工作伙伴如何知道你为某个数据库的表增加了一列呢?
- 如果你告诉了他要做这个事儿, 他是否要手动在自己的机器上做这个事儿吗?
大家记住: 我们要把上面的过程自动化!
也就是说:
整个项目组使用统一的 sql , 某个人每次修改数据库表之后,都要更新这个 sql 文件。
例如, 十年前很多项目都是这样:
- 把建表的 SQL 语句放到 cvs/svn 中.
- 小李如果修改了表结构, 那么就提交一个 commit, 更新 SQL 语句
- 小王每次更新代码后, 都要重新把 SQL 文件导入到数据库中.
- 但是这样做有不小的缺点:
测试数据很宝贵,很多测试数据是非常复杂的,例如:有 5 个表,每个表之间都有互相的依赖关系。 那么,一旦小王把数据库的表结构变化,小李重新导入 sql 文件后,小李本地原来的测试数据都没有了。 重新构建测试数据又要半天。大家不知道什么时候 SQL 发生了改变。
所以,我们要找到一种办法来解决上面遇到的问题. 让它:
- 可以自动化的执行.
- 不必对现有的测试代码造成影响. (只修改应该修改的地方)
- 可以对数据库进行高级的操作: 例如回滚到某个时间点.
- 这个办法, 就是 Database Migration.
Migration 初体验
Migration 在 Rails 中是非常简单的. 它就是 Rails 的一部分.
在 Rails 中, 所有的 migration, 都是用命令 rails generate migration 创建出来的. 它位于 db/migrate 目录下.
在使用 Migration 之前, 要先配置好数据库
配置 MYSQL 数据库
- 使用 config/database.yml 来连接 mysql:
development:
adapter: mysql2
database: db_name_dev
username: koploper
password:
host: localhost
- 修改 Gemfile, 增加:
gem 'mysql2'
对于 Linux, 要使用下面的命令安装好第三方包.
$ sudo apt-get install libmysqlclient-dev
- 安装好各种 gem:
$ bundle install
- 创建数据库:
rails db:create # rails 6.x
配置 SQLite 数据库
默认 Rails 就带了, 略过。
开始 Migration
下面是一个最简单的 migration 的例子:
# db/migration/20161021103259_create_books.rb
class CreateBooks < ActiveRecord::Migration
def up
create_table :books do |t|
t.string :title
t.timestamps
end
end
def down
drop_table :books
end
end
上面代码,代表了一个 migration. 叫做: CreateBooks.
每个 migration, 都有 2 个方法. up 和 down.
up: 就是从过去,往未来的时间方向上发展
down: 在时间上倒退。
(因为它可以移过来,再移回去。 在不断的 up/down 中, 数据库实现了迁移. 这就是这个名字的由来.)
下面是数据库迁移的一个例子:
在某个商业项目中, 从 2016 年 2 月, 到 2016 年 9 月, 数据库的结构发生了 177 次变化.
例子:
每个文件名都由两部分组成: 时间戳 + 事件.
20210925211625_create_download_counts.rb
20210925211625_create_positions.rb
20210925211625_add_name_to_price_strategies.rb
所以,当团队中,任意一个新手,加入的话,不需要你提供给他任何 sql 文件。让他直接运行 $ rails db:migrate
就可以了。
我也可以使用 rails db:rollback
回退到任意时刻。
所以,可以认为,migration 是衡量一个项目的水平的重要指标。 如果一个项目,没有 migration 的话
那么,这个项目就特别难于开发. 原因在于:
- 数据库结构难以获取
- 开发成员之间的表结构难以统一。
- 所以, 数据库迁移是极其重要的.
使用原则
记得:
-
任何对数据库的操作(改变表的结构),必须使用 migration
- 创建表
- 修改表
- 删除表
- 新增列
- 修改列
- 删除列
-
migration 一旦创建好,并且上传到了远程服务器,就绝对不能做改动。
例子
创建
例如:我想新建一个表 users, 它有个属性: name, age
就通过 rails generate migration
命令创建:
$ rails g migration CreateUsers
可以看到, 上面的命令, 建立了文件: db/migrate/20210925211625_create_users.rb
打开这个文件, 并且编辑它的内容, 如下:
class CreateUsers < ActiveRecord::Migration
def up
# 建立 users 表
create_table :users do |t|
# 建立列: name, 类型是string
t.string :name
# 建立列: age, 类型是 integer
t.integer :age
# 建立created_at 与 updated_at , 类型都是 datetime
t.timestamps
end
end
def down
# 删掉 users 表
drop_table :users
end
end
up/down 与 change 的区别.
( 如果你用 rails 6.x 来创建的话,得到的 migration , 一般没有 up, down 方法。因为,rails 非常智能,能自动的,把 up, down
方法合并成:change
例如,如果 up 方法中,是 create_table, 那么,rails 就会自动判断出,在 down 方法中,就用 drop_table。
但是,还是有些时刻,rails 无法自动判断 up/down, 例如: 改变某个列的类型。这个时候,还的 使用经典的 up/down 方法。
对于新手, 建议使用 up/down 方法. 免得被 change 弄糊涂.)
运行 rails db:migrate
接下来, 运行 rails db:migrate
:
$ rails db:migrate
== 20210925211625 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0078s
== 20210925211625 CreateUsers: migrated (0.0079s) =============================
我们打开这个数据库,发现数据库中,新增了一个表 users.
sqlite> .tables
schema_migrations users
回滚
$ rails db:rollback
== 20210925211625 CreateUsers: reverting ======================================
-- drop_table(:users)
-> 0.0012s
== 20210925211625 CreateUsers: reverted (0.0068s) =============================
可以看到,table 中就少了:users
sqlite> .tables
schema_migrations
schema_migrations
可能有的小伙伴会比较奇怪: schema_migrations
这个是干嘛的呢?
这个表专门记录当前数据库的 “迁移 ID” 是多少。 Rails 就是通过比较它和 db/migrate 中文件的差异来判断, 当前的 rails,的数据库,是否是最新的。
例如, 运行 CreateUsers
这个 migration 之前:
sqlite> select * from schema_migrations;
20210925211625
运行完之后:
sqlite> select * from schema_migrations;
20210922214619
20210925211625
多出来的一行: 20210925211625
, 刚好就是我们新建的 migration :20210925211625_create_users
名字的一部分。
如何修改一个列?
错误的做法:
运行回滚操作: rails db:rollback
修改 migration 文件的内容。 在其中增加 change_column 方法. (具体代码略)
$ rake db:migrate
绝对错误!因为,记住: 一旦创建好 migration 文件(特别是已经提交到了远程的话),就绝对不要去修改它!
正确的做法是:
新建个 migration. 运行它.
例子:
例如,我想把 age 列,改名字,改成: nian_ling, 我应该:
- 运行命令:
$ rails g migration rename_age_to_nian_ling_from_users
- 运行上面命令,产生的文件。
# db/migrate/20210926211625_rename_age_to_nian_ling_from_users.rb
class RenameAgeToNianLingFromUsers < ActiveRecord::Migration
def change
# 手动增加下面这行代码:
rename_column :users, :age, :nian_ling
end
end
- 运行 rails db:migrate
$ rails db:migrate
== 20210926211625 RenameAgeToNianLingFromUsers: migrating =====================
-- rename_column(:users, :age, :nian_ling)
-> 0.0111s
== 20210926211625 RenameAgeToNianLingFromUsers: migrated (0.0112s) ============
就能够看到,schema_migrations 表中, 增加了一条记录, 就是我们刚才运行的 migration 的时间戳.
sqlite> select * from schema_migrations;
20210922214619
20210925211625
20210926211625
并且, users 表中,age 列变成了 nian_ling 列。
sqlite> .schema
...
CREATE TABLE "users" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" varchar(255),
"nian_ling" integer,
"created_at" datetime,
"updated_at" datetime
);
sqlite>
常见的 migration 方法
注: 以下方法都写在 up, down 或者 change 方法中.
- create_table
例如, 创建表 'students' :
create_table :students do |t|
t.string :chinese_name
t.integer :age
t.timestamps
end
- drop_table
例如, 删掉表 'students' :
drop_table :students
- add_column
例如, 向 'students' 表中, 增加一个列 'name', 它的类型是字符串:
add_column :students, :name, :string
- remove_column
例如, 从 'students' 表中, 删除列 'name':
remove_column :students, :name
- rename_column
例如, 把 'students' 表的 'chinese_name' 列, 重命名为 'zhong_wen_ming_zi':
ruby
rename_column :students, :chinese_name, :zhong_wen_ming_zi
- add_index
例如, 把 'students' 表的 name 列建立索引:
add_index :students, :name
- remove_index
例如, 把 'students' 表的已经存在的 name 索引删掉:
remove_index :students, :name
评论区