后续迁移“无法创建 table(错误号:150 "Foreign key constraint is incorrectly formed")”

Sequelize migration 'Can't create table (errno: 150 "Foreign key constraint is incorrectly formed")'

将 Sequelize 添加到我的项目时,我在开始迁移和添加外键约束方面遇到了很大的困难。

文档...好吧,还有改进的余地!
根据我的网上搜索,其他人似乎也在苦苦挣扎。

想要以一对多关系关联两个表:

模型 1:用户状态(值:'init'、'active'、'inactive' ...)
模型 2:用户(用户名、电子邮件、pwdhash、...)

首先,创建迁移文件:

$ sequelize model:generate --name Userstatus --attributes name:string,value:tinyint,comment:string
$ sequelize model:generate --name User --attributes username:string,pwdhash:string,email:string,statusId:tinyint

最终得到这四个文件(包括修改):

  1. migrations/xxxxxxxxxxxxx1-create-userstatus.js
"use strict";
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable("Userstatus", {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.TINYINT,
      },
      name: {
        type: Sequelize.STRING,
      },
      comment: {
        type: Sequelize.STRING,
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable("Userstatus");
  },
};
  1. migrations/xxxxxxxxxxxxx2-create-users.js
"use strict";
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable("Users", {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.BIGINT,
      },
      username: {
        type: Sequelize.STRING,
      },
      pwdhash: {
        type: Sequelize.STRING,
      },
      email: {
        type: Sequelize.STRING,
      },
      statusId: {
        type: Sequelize.TINYINT,
        references: {
          model: "Userstatus",
          key: "id",
        },
        onUpdate: "CASCADE",
        onDelete: "SET DEFAULT",
      },
      statusUntil: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      deletedAt: {
        allowNull: true,
        type: Sequelize.DATE,
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable("Users");
  },
};
  1. models/userstatus.js(运行时,与迁移无关)
  2. models/user.js(运行时,与迁移无关)

正在执行命令:

$ sequelize db:migrate

Sequelize CLI [Node: 14.17.0, CLI: 6.2.0, ORM: 6.9.0]

Loaded configuration file "db\config\config.js".
Using environment "development".
== xxxxxxxxxxxxx1-create-userstatus: migrating =======
== xxxxxxxxxxxxx1-create-userstatus: migrated (0.101s)

== xxxxxxxxxxxxx2-create-users: migrating =======

ERROR: Can't create table myDb.Users (errno: 150 "Foreign key constraint is incorrectly formed")

添加详细的logging to config表明这是一个MySQL由onDelete(不接受SET DEFAULT)引起的错误:
Can't create foreign key with ON DELETE SET DEFAULT

在某处,我也遇到了由类型不匹配(TINYINT 与 INTEGER)引起的完全相同的错误响应:

  1. migrations/xxxxxxxxxxxxx1-create-userstatus.js
"use strict";
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable("Userstatus", {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.TINYINT,
      },
  :
};
  1. migrations/xxxxxxxxxxxxx2-create-users.js
"use strict";
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable("Users", {
      :
      statusId: {
        type: Sequelize.INTEGER,
        references: {
          model: "Userstatus",
          key: "id",
        },
        onUpdate: "CASCADE",
      },
  :
};

经验教训:发生迁移错误时,首先关注 DB 和 SQL - 确保生成的 SQL 在手动执行时确实有效。

这是我个人最终花费比需要更多时间的地方 - 我在知道发生了什么之前尝试 'fix' Sequelize 迁移......真正的修复 正在打开日志记录以便直接访问 SQL。

UPDATE:直接在创建 table 的地方定义外键约束,产生另一个需要处理的问题 - 为了撤消此类迁移,'undo'-部分迁移需要持有额外的queryInterface.removeConstraint(....)命令,引入需要使用Promise.all([...])(example here)

也许这只是个人喜好问题...我现在选择在单独的迁移文件中定义外键约束,如下所示:

migrations/xxxxxxxxxxxxxx-fk-userstatus-associate.js
"use strict";
module.exports = {
  up: async (queryInterface, Sequelize) => {
    return await queryInterface.addConstraint("Users", {
      type: "FOREIGN KEY",
      fields: ["statusId"], // field name of the foreign key
      name: "fk_users_statusId",
      references: {
        table: "Userstatus", // Target model
        field: "id", // key in Target model
      },
      onUpdate: "CASCADE",
      onDelete: "RESTRICT",
    });
  },

  down: async (queryInterface, Sequelize) => {
    return await queryInterface.removeConstraint(
      "Users", // Source model
      "fk_users_statusId" // key to remove
    );
  },
};

获胜:

  • 平滑的撤消处理:$ sequelize db:migrate:undo
  • 我可以随意命名约束条件