开发者问题收集

在运行批量迁移时,knex 迁移中使用的 ObjectionJS 模型报告“关系不存在”

2021-02-28
1156

通过 API 或 CLI 批量运行 knex 迁移时,如果迁移使用 ObjectionJS 模型,则迁移可能会失败。这种情况尤其可能发生在 knexfile 本身被解析为异步函数的情况下。

设置

为了更好地解释这一点,这里有一个示例:

database.js

// This file stores logic responsible for providing credentials.

async function getKnexfile() {
  // Some asynchronous behaviour that returns valid configuration.
  // A good use case for this can be retrieving a secret stored in AWS Secrets Manager
  // and passing it to the connection string part of the config.
  // 
  // For this example, let's assume the following is returned:
  return {
    client: 'pg',
    connectionString: 'pg://user:password@host:5432/database'
  };
}

module.exports = { getKnexfile };

knexfile.js

module.exports = require('./database').getKnexfile();

现在让我们考虑两个将同时运行的迁移文件。

001_build_schema.js

exports.up = async (knex) => {
  await knex.schema.createTable('mytable', (table) => {
    table.string('id').unique().notNullable().primary();
    table.string('text', 45);
  });
}

exports.down = async (knex) => {
  await knex.schema.dropTable('mytable');
}

在第二个迁移文件中,我们首先导入其中一个模型。我没有提供该模型的完整源代码,因为最终,它的定义方式对于这个例子来说并不重要。然而,重要的是(就我而言)这个模型使用了多个插件,例如 knexSnakeCaseMappers() ,再加上我的配置是异步获取的,因此需要一些创造性的编码。该模型的部分源代码将在最后定义。

002_insert_data.js

const MyModel = require('./MyModel');

exports.up = async (knex) => {
  await MyModel.query().insert({text: 'My Text'});
}

exports.down = async (knex) => {
  // Do nothing, this part is irrelevant...
}

问题

不起作用的是将两个迁移作为批处理运行。这意味着触发一批迁移(即通过 CLI)会导致它们失败,如下所示:

# We are currently at the base migration (i.e. migrations were not ran yet).

knex migrate:latest

上述操作将导致以下错误:

migration file "002_insert_data.js" failed

migration failed with error: insert into "mytable" ("text") values ($1) returning "id" - relation "mytable" does not exist

DBError: insert into "mytable" ("text") values ($1) returning "id" - relation "mytable" does not exist

这似乎没有等待迁移(即迁移 002 在迁移 001 完成之前运行),但实验表明事实并非如此。或者至少,问题并不像迁移不能一个接一个地运行那么简单,因为使用简单的 console.log 语句已经表明这些文件实际上是并发执行的。

此外,使用类似于以下的脚本逐个运行迁移(即不是批量运行)将导致迁移成功,并且数据将适当地填充到数据库中:

knex migrate:up && knex migrate:up

在确保使用的模式完全相同(设置 .withSchema('schema_name') )后,我发现问题一定与在事务中运行的迁移有关,但使用标志 disableTransactions: true 已被证明是一个糟糕的解决方案,因为在发生崩溃的情况下,数据库将处于未知状态。

这是 MyModel.js

const { Model, knexSnakeCaseMappers, snakeCaseMappers } = require('objection');

// The below line imports an async function that returns the connection string. This is
// needed as knex() expects the provided argument to be an object, and accepts async function
// only for the connection field (which is why previously defined getKnexfile cannot be used).
const getConnectionStringAsync = require('./database');

const db = knex({
  client: 'pg',
  connection: knexfile.getConnectionString,
  ...knexSnakeCaseMappers(),
});

Model.knex(db);


module.exports = class MyModel extends Model {
  // The implementation of the model goes here...
  // The table name of this model is set to `mytable`.
}
的部分源代码
2个回答

我通过实现两个目标成功解决了这个问题:

  1. 迁移是在事务中运行的,这意味着用于与数据库通信的实际 knex 对象在迁移之间共享并且是相同的。因此,使用哪个 knex 对象很重要。
  2. 我的异步配置获取设置导致在运行使用模型的迁移时出现多个连接,因为模型会初始化自己的连接。

从那里开始,解决方案就很明显了: 在所有模型和迁移命令中使用相同的 knex 对象 。这可以通过相对简单的方式实现,通过以下方式调整使用模型的迁移文件:

002_insert_data.js

// Import the model as previously (name can be changed for clarity).
const MyModelUnbound = require('./MyModel');


exports.up = async (knex) => {
  // Bind the existing knex connection to the model.
  const MyModel = MyModelUnbound.bindKnex(knex);

  await MyModel.query().insert({text: 'My Text'});
}

// ...

It's important to note, that the above code sets the knexfile configuration in the model, adding the knexSnakeCaseMapper plugin, which will not be applied to the knex configuration generated by the getKnexfile() function. This could be fixed by moving that configuration to the getKnexfile() method (or in case where the API is used, duplicating that definition in the knexfile configuration in that place).

这完全解决了我的问题,现在批量运行迁移可以正常工作。我仍然不完全确定的一件事是为什么初始行为会发生。我想象中的事务工作方式是基于迁移的(即 1 次迁移 = 1 次事务),这意味着事情应该以某种方式进行。

我目前的理论是,当第一次迁移的事务完成时,以及当为第二次迁移中的模型建立下一个连接时,可能存在一些竞争条件。无论哪种方式,绑定原始 knex 对象(在调用迁移 API 或 CLI 期间构建)都可以解决问题。

Marceli Wac
2021-02-28

考虑到 Marceli 的回复,您也可以直接在查询中绑定事务,例如:

exports.up = async (knex) => {
  await MyModel.query(knex).insert({text: 'My Text'});
}

如果您的模型中有连接,则此方法效果会更好

Marianna Spirandelli
2022-01-11