OpenProject 数据库迁移实战指南:Rails Migration 约定、常用命令与 Squashing 压缩机制
2026/9/14 3:55:51 网站建设 项目流程

OpenProject 数据库迁移实战指南:Rails Migration 约定、常用命令与 Squashing 压缩机制

【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject

本篇技术指南聚焦 OpenProject 的数据库层开发与运维实践,涵盖db/CLAUDE.md约定的迁移代码规范、本地与 Docker 环境下的迁移/回滚/填充命令、config/database.yml的 Docker 使用禁忌,并深入剖析 OpenProject 在主版本之间对历史迁移进行 Squashing 压缩的机制与验证方法。读完本文,你将掌握在 OpenProject 仓库中安全编写、执行、压缩与校验数据库迁移的完整能力。

迁移代码规范:遵循 Rails 约定,并在主版本间 Squash

OpenProject 的数据库迁移遵循 Rails 迁移(Migration)约定:每次 schema 或数据变更都应通过独立、带时间戳的迁移文件表达,并放置在db/migrate/目录下。仓库中现存的大量迁移文件(如20240123151246_create_good_jobs.rb20260330100000_create_work_package_semantic_ids.rb20260831120000_migrate_version_to_target_versions_in_type_variants.rb等)就是这一约定的直接体现。

与普通 Rails 项目不同的是,OpenProject 会在每个主版本发布之间对迁移进行 Squashing(压缩合并)。其动机在于:迁移往往同时包含结构变更与数据变更,而数据迁移常依赖外部库或应用代码;随着版本演进,这些依赖可能变化,导致历史迁移难以维护且 bug 难以排查。通过压缩,OpenProject 可以不再维护过时的旧迁移。该策略的完整说明见 docs/development/migrations/README.md。

版本化规则:必须逐主版本升级

Squashing 带来了一个关键约束:每个主版本发布时,迁移会被压缩,因此一个安装实例必须先迁移到最近的主版本,才能继续迁移到当前版本。例如,要迁移到 OpenProject 16.x,必须先存在一个 OpenProject 15.x 的安装(当前仓库中聚合迁移文件1000016_aggregated_migrations.rb的存在即对应这一代版本)。并非所有迁移都会被压缩——最近一个主版本内新增的迁移保持原样,留待后续发布时再压缩。

常用迁移命令:本地开发与 Docker 环境

db/CLAUDE.md明确给出了两类执行环境下的命令,下面结合仓库实际情况逐一说明。

本地开发环境

bundle exec rails g migration MigrationName # 生成一个迁移 bundle exec rails db:migrate # 执行迁移 bundle exec rails db:rollback # 回滚最近一个迁移 bundle exec rails db:seed # 填充示例/种子数据
  • rails g migration生成的迁移文件会落在db/migrate/下,并自动获得时间戳前缀,遵循 Rails 默认命名与格式(可参考db/migrate/中任意现有文件)。
  • rails db:rollback默认回滚最近一次迁移;若当前数据库 schema 是经由压缩后的聚合迁移建立的,回滚会走SquashedMigration#down,其实现为直接抛出ActiveRecord::IrreversibleMigration,提示使用对应主版本的安装进行降级(详见 db/migrate/migration_utils/squashed_migration.rb)。
  • rails db:seed执行 db/seeds.rb 中的种子数据逻辑。
  • 本地数据库连接通过 config/database.yml.example 配置:development 库默认openproject_development(连接池pool: 100),test 库openproject_testpool: 5),production 库openprojectpool: 20),均使用 PostgreSQL 适配器与 unicode 编码。

Docker 环境

bin/compose exec backend bundle exec rails db:migrate # 执行迁移 bin/compose exec backend bundle exec rails db:seed # 填充数据

在 Docker 部署下,迁移与种子填充通过bin/compose exec backend进入后端容器内执行,而不是在宿主机直接运行。这与 docker-compose.yml 定义的 backend 服务结构对应,确保迁移在与应用一致的环境中运行。

关键注意:Docker 下config/database.yml必须不存在

db/CLAUDE.md中的CRITICAL提示是实践中最容易踩坑的一点:

CRITICAL:config/database.ymlmust NOT exist when using Docker (rename or delete it)

当使用 Docker(bin/compose)运行时,仓库根目录下的config/database.yml不能存在,需要将其重命名或删除。原因在于:容器环境(数据库主机、账号、密码等)由 Docker Compose 的环境变量与配置注入管理,若本地存在database.yml,其硬编码的连接信息会覆盖容器注入的配置,导致后端容器连接到错误的主机或凭据而启动/迁移失败。仓库仅提供 config/database.yml.example 作为非 Docker 场景的参考模板,本身并不提交database.yml到版本库,也印证了这一点。

深入原理:Squashing 压缩机制与源码实现

为了让上文“主版本间压缩迁移”的原则落地,OpenProject 在常规 Rails 迁移之上实现了一套完整机制,核心实现在 docs/development/migrations/README.md 与 db/migrate/migration_utils/squashed_migration.rb 中。

聚合迁移文件的组成

每个主版本对应一个聚合迁移文件,例如核心应用的 db/migrate/1000016_aggregated_migrations.rb,各模块也有自己的聚合文件。这些文件继承自SquashedMigration(其本身继承自ActiveRecord::Migration[8.0]),并通过四个声明式入口描述目标 schema:

  • extensions列表:声明需要创建的数据库扩展,如Extensions::BtreeGistExtensions::PgTrgmExtensions::UnaccentExtensions::VersionNameCollation。扩展会最先被加载,以便后续表可以引用它们。以 db/migrate/extensions/pg_trgm.rb 为例,它声明extension "pg_trgm",并在扩展缺失时输出安装postgresql-contrib模块的警告提示。
  • tables列表:列出需要创建的全部表(含列、索引、约束等)。每张表一个专属类,存放于 db/migrate/tables/(模块对应迁移位于模块内部),并遵循“表文件应与模型文件同处一个模块/核心”的放置原则。以 db/migrate/tables/announcements.rb 为例,Tables::Announcements定义了textshow_untilactive等列及(show_until, active)联合索引。
  • squashed_migrations列表:列出本文件压缩掉的迁移名,仅包含本版本压缩的迁移,此前已被压缩的不再列出。聚合文件内通过squashed_migrations *%w[...]逐条引用,如1000016_aggregated_migrations.rb中的squashed_migrations列表以1000015_aggregated_migrations开头,随后是20241030154245_create_project_life_cycles20241119131205_create_reminders等被压缩的迁移(见 db/migrate/1000016_aggregated_migrations.rb)。
  • modifications:插件可以通过modifications段修改某张表,这是例外用法,适用于功能强归属该模块且不被其他模块使用的场景(见 squashed_migration.rb 的类方法定义)。

文件命名与依赖顺序

聚合迁移文件名以10[两位顺序号]0[压缩版本号]为时间戳前缀,例如1000016_aggregated_migrations.rb:顺序号用于解析依赖(例如某模块向核心表添加外键,则该模块必须排在核心之后运行),同时保证迁移名唯一。压缩版本号则标识其对应的主版本。

压缩执行流程

SquashedMigration#up调用Migration::MigrationSquasher.squash(self.class.squashed_migrations, self.class.minimum_version),在其块内依次创建扩展、创建表、执行modifications(见 squashed_migration.rb)。minimum_version类属性默认值为"16"(见 squashed_migration.rb),用于down时提示用户降级所需的 OpenProject 版本。其余辅助工具(列操作、权限重命名、设置重命名、typed_dag 等)集中在 db/migrate/migration_utils/ 目录。

发布新主版本时的压缩步骤

docs/development/migrations/README.md给出了完整的压缩操作清单,核心步骤包括:

  1. 检查SquashedMigration上的 Rails 版本:若两个 OpenProject 版本间 Rails 升级,需同步修改ActiveRecord::Migration[RAILS_VERSION]超类引用,并验证其对目标结构的影响。
  2. 重命名既有聚合迁移:将时间戳改为反映上一主版本,例如发布 OP 17.0 时,把db/migrate/1000015_aggregated_migrations.rb重命名为1000016_aggregated_migrations.rb;新模块首次获得聚合文件时需确保不与既有顺序号冲突。
  3. 删除被压缩的迁移:压缩截至上一主版本的最后一个补丁版本;删除时以被压缩文件为清单,逐条处理。
  4. 创建/调整表类:将删除迁移中的建表与改表逻辑移入 db/migrate/tables/ 对应表文件(Tables::Xxx类),列归属模块时可放入模块聚合文件的modifications段。
  5. 创建/调整扩展类:将索引与排序规则等扩展移入 db/migrate/extensions/ 对应扩展文件。
  6. 忽略数据变更:聚合文件只描述数据库结构,原迁移中移动存量数据的数据变更代码不再需要。
  7. 清理失去引用的代码:删除只被被压缩数据迁移引用的后台任务、库、服务或 scope。
  8. 更新最低版本:提高SquashedMigration类的minimum_version,反映新的迁移要求(如 OP 16 要求 15、OP 17 要求 16)。

验证压缩是否引入 schema 变化

压缩过程完成后,需要通过结构对比与数据对比双重验证,具体步骤同样记录在 docs/development/migrations/README.md。

结构验证:压缩前先执行rails db:drop db:create db:migrate生成db/structure.sql并另存为structure_unsquashed.sql;完成压缩后再次执行相同命令生成新的structure.sql;用diff对比两份文件应无差异。若存在已知缺憾,可用独立迁移另行修复。

数据验证:找一份含数据的数据库,先pg_dump --column-inserts -U [user_name] -d [database_name] > [database_name]-orig.sql导出原库并用psql [another_database_name] < [database_name]-orig.sql载入;切到压缩前的提交(如 dev 分支)执行rails db:migrate后导出[database_name]-dev.sql;再rails db:drop db:create重建,切到压缩后的提交,重新载入原 dump 并rails db:migrate,导出[database_name]-squashed.sql;最后git diff对比两份导出,确保数据一致。

特殊处理:good_job 的迁移文件

一个值得注意的例外是 good_job(OpenProject 的后台任务队列实现)。good_job 升级时会依据“是否存在预期名称的迁移文件”来重新生成迁移,因此其迁移文件不能被删除;正确做法是将其内容清空并把结构移入tables类,与普通 Squashing 处理方式相同。仓库db/migrate/下可见20240123151246_create_good_jobs.rb20240123151247_create_good_job_settings.rb等一整套 good_job 相关迁移,以及 db/migrate/tables/good_jobs.rb、good_job_batches.rb 等对应表类,正是该处理策略的落地证据。

小结

OpenProject 的数据库层既遵循标准 Rails 迁移约定,又在主版本间引入 Squashing 压缩机制来控制迁移维护成本:日常开发使用bundle exec rails g migration / db:migrate / db:rollback / db:seed,Docker 环境通过bin/compose exec backend bundle exec rails ...执行并严禁存在config/database.yml;而涉及主版本升级时,则需要依据聚合迁移文件(extensions/tables/squashed_migrations/modifications)与SquashedMigration的实现完成压缩、删除、表类迁移与最低版本更新,并通过结构 diff 与数据 dump 对比验证结果。

【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询