数据库迁移

页面摘要: 数据库迁移在模式同步之前运行一次性脚本,以在升级过程中保留数据。迁移文件导出一个 up() 函数,按字母顺序运行一次。在随后的模式同步期间,Strapi 会删除此前由它管理、但已不再存在于内容类型模式中的表、列、索引和外键。

数据库迁移的存在是为了对数据库运行一次性查询,通常是在升级 Strapi 应用时修改表结构或数据。这些迁移在应用启动时自动运行,并在 Strapi 启动时也会执行的自动模式同步之前执行。

🚧 实验性功能

数据库迁移处于实验阶段。该功能仍在开发中,将持续更新和改进。在此期间,欢迎在 GitHub Discussions 或社区 Discord 上寻求帮助。

理解数据库迁移文件

迁移通过存储在 ./database/migrations 中的 JavaScript 迁移文件运行。

Strapi 会自动检测迁移文件,并在下次启动时按字母顺序运行一次。每个新文件仅执行一次。

启动时发生了什么 {#startup-sequence}

理解操作顺序有助于预测你的数据会发生什么。每次启动时,Strapi 执行以下步骤:

  1. Strapi 加载模式:内容类型和组件被转换为数据库模型,然后验证关系。
  2. Strapi 运行待处理的迁移:你在 /database/migrations 中的迁移文件先运行,随后是 Strapi 自身的内部迁移。每个迁移在自己的事务中运行,并且已应用的迁移会被跟踪,因此不会运行两次。
  3. Strapi 同步数据库模式:它将内容类型模式与数据库进行比较,然后应用差异。首先创建表和列,然后删除不再属于模式的表,最后修改剩余的表。
  4. Strapi 持久化新模式:生成的模式被存储在数据库中,并成为下次启动时的参考。
NOTE

迁移在模式同步之前运行,因此 up() 函数看到的仍是数据库的先前状态。请针对旧模式编写迁移,而不是针对你要迁移到的目标模式。

NOTE

当没有待处理迁移且自上次启动以来模式未发生变化时,步骤 2 和 3 会被完全跳过。Strapi 通过比较内容类型模式来检测变更,而不是检查数据库,因此直接对数据库所做的手动更改不会被检测或回滚。

模式同步期间的数据丢失 {#data-loss}

在模式同步期间,Strapi 会删除此前由它管理、且不再匹配内容类型模式的表、列、索引和外键。这一过程自动发生,没有任何警告或确认提示,并且在开发和生产环境中表现一致。因此,从代码中删除一个内容类型会在下次启动时删除其表及其数据。

Strapi 从未管理过的表(例如你直接在数据库中自己创建的表)则保持不变。

迁移是跨此类变更保留数据的方式:在模式同步移除旧结构之前,使用迁移来复制或转换数据。

WARNING

Strapi 不支持向下迁移。如果你需要回滚迁移,必须手动完成。向下迁移已在计划中,但目前没有时间表。

TIP

forceMigration 数据库配置参数 控制此行为。将其设置为 false 会跳过所有删除操作。新模式仍会被记录为参考,因此被跳过删除的对象将不再被 Strapi 跟踪,如果你之后将该参数设回 true,它也不会被删除。runMigrations 参数仅控制你在 /database/migrations 中的文件是否运行,对模式同步没有影响。

迁移文件应导出一个 up() 函数,用于升级时(例如添加新表 my_new_table)。

up() 函数在数据库事务中运行,这意味着如果迁移期间某个查询失败,整个迁移将被取消,且不会对数据库应用任何更改。如果在迁移函数内创建了另一个事务,它将充当嵌套事务。

NOTE

没有用于手动执行数据库迁移的 CLI。

创建迁移文件

创建迁移文件:

  1. 在 ./database/migrations 文件夹中,创建一个以迁移日期和名称命名的新文件(例如 2022.05.10T00.00.00.name-of-my-migration.js)。请确保文件名遵循此命名模式,因为文件的字母顺序决定了迁移的运行顺序。

  2. 将以下模板复制粘贴到之前创建的文件中:

'use strict'

async function up(knex) {}

module.exports = { up };
  1. 通过在 up() 函数内添加实际的迁移代码来填充模板。up() 接收一个 Knex instance(Knex 实例),该实例已处于事务状态,可用于运行数据库查询。

迁移文件示例


module.exports = {
  async up(knex) {
    // You have full access to the Knex.js API with an already initialized connection to the database

    // Example: renaming a table
    await knex.schema.renameTable('oldName', 'newName');

    // Example: renaming a column
    await knex.schema.table('someTable', table => {
      table.renameColumn('oldName', 'newName');
    });

    // Example: updating data
    await knex.from('someTable').update({ columnName: 'newValue' }).where({ columnName: 'oldValue' });
  },
};

在迁移中使用 Strapi 实例

DANGER

如果用户选择不直接使用 Knex 进行迁移,而是使用 Strapi 实例,重要的是用 strapi.db.transaction() 包裹迁移代码。否则,如果发生错误,迁移可能无法回滚。

使用 Strapi 实例的迁移文件示例

module.exports = {
  async up() {
    await strapi.db.transaction(async () => {
      // Your migration code here

      // Example: creating new entries
      await strapi.documents('api::article.article').create({
        data: {
          title: 'My Article',
        },
      });

      // Example: custom service method
      await strapi.service('api::article.article').updateRelatedArticles();
    });
  },
};

读取迁移进度心跳

Strapi 的部分内部迁移会处理大量数据,可能运行数分钟。为了表明它们仍在工作,它们会记录一条周期性进度行,限制为每 60 秒一条消息:

[document-id] still running (120s) · articles 12000/450000

前缀标识正在运行的内部迁移,计数器显示目前已处理了多少行。这些消息是追加而非就地重写的,因此在日志文件中仍清晰可读。

NOTE

进度心跳仅由 Strapi 的内部迁移发出。你自己的迁移文件中不可用。

使用 TypeScript 代码处理迁移

默认情况下,使用 TypeScript 时,Strapi 会在源目录而非构建目录中查找迁移文件。这意味着除非你将 Strapi 配置为查找正确的位置,否则 TypeScript 迁移不会被正确找到和执行。

要在 Strapi 中启用 TypeScript 迁移,你需要在数据库配置中将 useTypescriptMigrations 参数设为 true。该设置告诉 Strapi 在构建目录而非源目录中查找迁移。

以下是在数据库设置中的配置方式:

JavaScript

module.exports = ({ env }) => ({
  connection: {
    // Your database connection settings
  },
  settings: {
    useTypescriptMigrations: true
  }
});

TypeScript

export default ({ env }) => ({
  connection: {
    // Your database connection settings
  },
  settings: {
    useTypescriptMigrations: true
  }
});

此外,如果你想在 TypeScript 迁移的同时继续使用现有的 JavaScript 迁移,可以在 tsconfig.json 文件的编译器选项中设置 allowJs: true,如 数据库配置文档 中所述。