Rails db/schema.rb 数据库迁移后的数据库模式文件需要加入到版本控制中吗?

lehug · 2017年11月04日 · 最后由 mizuhashi 回复于 2017年12月15日 · 3291 次阅读

在实际项目,团队对于是否把 schema.rb 加入到版本控制中做了一些讨论。想延伸下,听听各位的意见。

目前我们把其加入到了版本控制,带来了一些问题: 经常冲突,然后解决冲突,存在解决冲突不佳导致的副作用。 同时发现不加入版本控制,交给 migrate 文件自行构建,暂未发现副作用。并且大部分 github 项目都将 schema.rb 加入到了.gitignore。

但是 rails guide 或者 rails 风格指引中都提到建议加到版本控制

6.3 数据库模式转储和源码版本控制
数据库模式转储是数据库模式的可信来源,因此强烈建议将其纳入源码版本控制。

db/schema.rb 文件包含数据库的当前版本号,这样可以确保在合并两个包含数据库模式文件的分支时会发生冲突。一旦出现这种情况,就需要手动解决冲突,保留版本较高的那个数据库模式文件。

其中提到了一个好处是

当需要部署 Rails 应用的新实例时,不必把所有迁移重新运行一遍,直接加载当前数据库的模式文件要简单和快速得多。

但是我们实际项目中有 200+ 的表,空库执行 rails db:migrate 耗时也不超过一分钟(甚至更少),所以我觉得此好处可忽略。

你们将其加入到版本控制了吗?不加入版本控制后,你们遇到了什么副作用?

这个问题。。。我觉得 schema.rb 是无辜的。。。 你们应该解决协同开发的问题,并不是 schema.rb 的问题。。。因为如果版本控制出问题的话。。。不止 schema.rb 会冲突啊。。。

要,用来看当前有什么字段。

zj0713001 回复

多人同时修改一个表的结构时,schema 中很容易出现冲突,尤其是 version 那。不把其 加入版本控制,而是通过 migrate 文件重建 schema.rb,这样就不会存在冲突的情况。另外拉去新代码携带新的 migrate 文件后,也必然会执行 db:migrate,这个动作必定会重建 schema.rb。那么我为何需要跟踪它呢?

Rei 回复

不加入版本控制,而是拉取新的 migrate 文件后,必然会执行rails db:migrate,执行后本地的 schema 文件会重建的呀。那么我不加入版本控制,也是可以实现获取数据库结构的目的的。

另外为何 homeland 项目的.gitignore 里面有 /db/schema.rb ?

lehug 回复

我就见过一个项目重跑 migrate 之后和线上数据库 schema 不一致,不知道过程做了什么,也没有把 schema.rb 加到 git 里,开发一直基于错误的 schema 做然后上线出问题,最后把线上的 schema 导出重新加回。schema 有冲突就应该当即解决冲突。

现在 homeland 没有 ignore db/schema.rb https://github.com/ruby-china/homeland/blob/master/.gitignore

运行 migration 的时间不是取决于表数而是 migration 的数量,看看 discourse 的 migration 把字段改了又改,重跑一次不会很浪费时间吗? https://github.com/discourse/discourse/tree/master/db/migrate

discourse 不算好的 Rails 范例,创始人用 .net 的风格编码,用前后端分离,不是遵循 Rails 最佳实践的项目。

Rei 回复

我自测了一下,数据库里面直接删除一个字段,然后执行一个空的 migrate,接着 schema.rb 会更新为当前数据库的结构。而不和 migrate 文件定义的一致。似乎 schema.rb 只是rails db:migrate执行后,rails 根据数据库结构重建的结果,只反应本地数据库的状态。

重跑 migrate 之后和线上数据库 schema 不一致的一定是手动修改过数据库,那么把 schema.rb 加到文件跟踪也不会根本性解决问题。因为 migrate 文件会影响数据库结构,除了rails db:schema:load外,schema.rb 并不会反影响到数据库结构。

如上成立的话,还是感觉 schema.rb 在版本跟踪中没有意义。

schema.rb 有冲突不处理,ignore 掉冲突就消失了吗?

设想有一个 table:

create_table :users do |t|
  t.string :name

  t.timestamps
end

这时分出两个分支,同时对 :name 进行修改:

# branch-1
rename_column :users, :name, :display_name
# branch-2
rename_column :users, :name, :full_name

如果有 schema.rb,在合并的时候会当即发现有冲突,两个分支都对 schema.rb 的同一行进行了修改,需要立即处理冲突。

如果没有 schema.rb,冲突会延后到执行 migration 的时候。如果有 CI 它会报异常,如果没有 CI,源码没有冲突,那就要拉下来 migration 的时候才发现冲突,此时代码已经被 merge 了。

schema.rb 能让冲突更早发现,更容易处理。

我找到 discourse 为什么不 check-in schema.rb 的讨论 https://meta.discourse.org/t/schema-rb-vs-migrations/14983

他们似乎基于这两个理由

1. schema.rb 不能用 db 的本地特性

这时候应该用 structure.sql。

2. schema.rb/structure.sql 依赖于开发的本地数据库,可能会 check-in 不合版本的修改

开发环境和生产环境应该一致,用 docker 来维护。

至于开发者提交了坏的 structure.sql,应该提交者自己 review 和上游开发者 review,而不是大家都没有一个完整版的 schema,同时期待线上不会出问题。

找到了一些额外的文章:

我再重新梳理下我的思考:

  • schema.rb 是一个自动生成的文件,rails db:migrate等命名执行后,此文件会重新生成。
  • schema.rb 表述本地当前的数据库结构,migration 外手动修改数据库后,执行rails db:migrate会反应到 schema.rb 上
  • schema.rb 究竟应该被谁版本控制?我的观点是 migrate 文件,而不是版本控制工具。只有 migrate 文件才能表述程序的迭代。上面提到的那个点,很容易破坏 schema.rb,信任这样的 schema.rb 然后向下迭代是危险的。而空库 migrate 后的 schema 才是唯一表述迭代后数据库该有的结构。
  • 大多观点在 2011 年就已经加入,schema.rb 中的注释是 07 年加入的。我不认为在当前的计算机配置下,rails db:migrate会比'rails db:schema:load慢多少,所以我不认为这一点它可以快速迁移数据库到你期望的状态` 还依然会是优点。另外 rails 项目一般也不庞大~
  • 多人协作时,大部分都负责不同的模块,同时修改某个表导致的冲突不多。git 的不智能,导致的 version 冲突的问题,让其发生并且去花费时间解决,并没有意义。
  • 合并到主分支,一定是顺序执行的。假如同时修改一个表,那么最后一个提交到主分支的人在提交前就应该发现到了 migrate 冲突并解决。理论上存在可能性,同时删除了一个字段,但是概率很低。并且现在的开发理念中,都会有各种测试手段与流程。取舍之间,我选择舍弃会造成冲突的 schema.rb 跟踪,换来存在小概率 migrate 定义冲突情况的发生,并通过测试等手段解决掉这个副作用问题。
  • 它加入到版本控制确实也不会对团队造成太大的生产力浪费,但是频次还是比较高的。基于上面的一些观点,我还是觉得加入到版本控制没有大的优点,那么把它移出版本跟踪,对团队还是会减少一些此文件冲突烦恼的。
  • 再次强调下,我认为 schema.rb 应该被 migrate 文件控制,而不是版本工具。
lehug 回复

去掉 scheme 后,明显劣势是没有地方看当前数据库有什么表和字段了。当然,可以学 discourse 那样把 schema 导到 model 注释里,等于丢掉自带的东西引入一个依赖。

11 楼 已删除

你一直在重复这个 别人也做了说明 你一直在重复 schema 能重建 并且容易冲突的问题,完全听不进去别人的意见. 并且在实际项目中 如果只用 migration 文件很容易出现忘记提交问题 这时候如果时间过长 是没办法直接 migrate 成功的 需要一份 schema 来恢复一个基准数据库 再进行 migration

谢谢提醒。重新认识了下,我是有了答案,希望找到一个有说服力的反方观点,感觉还是没找到~

schema.rb 带来的好处:

  1. 更快的 db setup。
  2. 一目了然的数据库模式。
  3. 及早发现冲突。
  4. 符合约定,易于上手。

没有 schema.rb 带来的问题:

  1. db:migrate 越来越慢,影响开发和 CI。
  2. 需要在本地跑一遍 migration 才能看数据库模式。
  3. 发现冲突的时机延后到 CI 或更晚。
  4. 违反约定,让人迷惑。

如果这不能说服楼主,我感觉也没什么办法阻止楼主干傻事了。

Rei 回复

谢谢,这样优缺点已经很明确了。希望这个也能给别的愿意思考与质疑的人一些参考。

Specify whether schema dump should happen at the end of the
db:migrate rake task. This is true by default, which is useful for the
development environment. This should ideally be false in the production
environment where dumping schema is rarely needed.
mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true

补充一个 ActiveRecord::Core 中的注释,既然在 production 环境应该是 false,所以我认为楼主的决定没有错。

schema.rb 如果冲突,直接重跑 migration 不就可以解决冲突了吗?

justin 回复

是的,重跑下就解决了。有一个场景:我们借助 coding.net,多人提交合并请求,都携带了 schema.rb,那么前一个合并后,后面的全部都会提示合并冲突。另外多人协作,大多数情况下,我不需要关心别人的 schema 改动,那么这种冲突解决就对我们来说浪费时间。

因为有人忘记提交 schema.rb. 现在每次提交都要 git checkout db/schema.rb, 然后手动把自己的修改粘贴到 schema 上去。心好累。

ThxFly 回复

为什么需要手动维护这个文件?

lehug 回复

因为 master 的 schema 滞后于真实 schema。某次提交有迁移但没 schema 就会发生这种情况。人都比较懒,没人修复这次过失

ThxFly 回复

schema 文件会在 rails db:migrate 后重写的。只要不基于 master 执行 rails db:schema:load 那么 master 滞后的问题可以忽略。既然你们团队对 schema.rb 文件也不重视,很多人为了避免冲突而选择不提交它,那么变成一个团队内的约定,都不提交,也未尝不可。你单独为了一个老旧的约定,而做一些对团队其他人意义不大的事情,我觉得没必要~

不 check schema.rb 的前提是你们要能保证 migration 一直可以运行。但据我观察,超过两年的项目很难保证这一点。另外,migration 很快的前提是,你们要保证 migration 文件里不写 data migration。

hooopo 回复

我现在有个观点:1:要保证 migrate 的连续性,保证其可以顺序执行。2:定期或者大版本更新后,清理一次 migrate 文件目录,将多个 migrate 文件合并到一个,为 migrate 文件夹瘦身(需要注意不能创建新的 migrate,而是用老的已有的 version,否则会视为新的 migrate 而清表重建)。 这样是不是一种更好的代码维护方式?

lehug 回复

确实可行 但瘦身可比合并 schema。rb 冲突复杂多了,并且危险大。

其实 check schema 最大的优点就是不会出现:你本地 work,其他人或其他环境不 work。

SVN 也不是不能用。

hooopo 回复

空库执行下 migrate,然后把 schema.rb 覆盖到最后一个 version 中,然后把其他的 migrate 删掉,这样就合并了,还是比较轻松的😀

@Rei @hooopo

但是 schema 本来就是 db dump 出来的,永远反映的是 db 现在的状态,假如开发环境别的分支有 migration,在当前分支 commit 之后这个 wip 的 migration 就会合到 master 上....

我认为 schema 的正确性是没法保证的,如果常常要从头 init 整个环境,我觉得合并 migration 才是唯一能用的方案。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号