Sequelize 动态播种

Sequelize dynamic seeding

我目前正在使用 Sequelize.js 播种数据并对关联 ID 使用硬编码值。这并不理想,因为我真的应该能够动态地执行此操作吗?例如,将用户和个人资料与 "has one" 和 "belongs to" 关联。我不一定想用硬编码 profileId 来为用户播种。我宁愿在创建配置文件后在配置文件种子中这样做。创建配置文件后,将 profileId 动态添加到用户。在使用 Sequelize.js 时,这可能和正常约定吗?还是在使用 Sequelize 播种时仅硬编码关联 ID 更常见?

也许我播种错了?我应该使用 Sequelize 将一对一数量的种子文件与迁移文件放在一起吗?在 Rails 中,通常只有 1 个种子文件,如果需要,您可以选择拆分成多个文件。

总的来说,只是在这里寻求指导和建议。这些是我的文件:

users.js

// User seeds

'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.bulkInsert('Person', [{
        name: 'John Doe',
        isBetaMember: false
      }], {});
    */

    var users = [];
    for (let i = 0; i < 10; i++) {
      users.push({
        fname: "Foo",
        lname: "Bar",
        username: `foobar${i}`,
        email: `foobar${i}@gmail.com`,
        profileId: i + 1
      });
    }
    return queryInterface.bulkInsert('Users', users);
  },

  down: function (queryInterface, Sequelize) {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.bulkDelete('Person', null, {});
    */
    return queryInterface.bulkDelete('Users', null, {});
  }
};

profiles.js

// Profile seeds

'use strict';
var models = require('./../models');
var User = models.User;
var Profile = models.Profile;


module.exports = {
  up: function (queryInterface, Sequelize) {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.bulkInsert('Person', [{
        name: 'John Doe',
        isBetaMember: false
      }], {});
    */

    var profiles = [];
    var genders = ['m', 'f'];
    for (let i = 0; i < 10; i++) {
      profiles.push({
        birthday: new Date(),
        gender: genders[Math.round(Math.random())],
        occupation: 'Dev',
        description: 'Cool yo',
        userId: i + 1
      });
    }
    return queryInterface.bulkInsert('Profiles', profiles);
  },

  down: function (queryInterface, Sequelize) {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.bulkDelete('Person', null, {});
    */
    return queryInterface.bulkDelete('Profiles', null, {});
  }
};

如您所见,我只是对两者都使用了硬编码 for 循环(不理想)。

您可以使用 sequelizes create-with-association 功能将它们一起放在一个文件中,而不是为用户和配置文件使用不同的种子。

此外,当使用一系列 create() 时,您必须将它们包装在 Promise.all() 中,因为播种接口需要 Promise 作为 return 值。

up: function (queryInterface, Sequelize) {
  return Promise.all([
    models.Profile.create({
        data: 'profile stuff',
        users: [{
          name: "name",
          ...
        }, {
          name: 'another user',
          ...
        }]}, {
        include: [ model.users]
      }
    ),
    models.Profile.create({
      data: 'another profile',
      users: [{
        name: "more users",
        ...
      }, {
        name: 'another user',
        ...
      }]}, {
        include: [ model.users]
      }
    )
  ])
}

不确定这是否真的是最好的解决方案,但这就是我在种子文件中自己维护外键的方法。

警告:在使用 sequelize 一年多之后,我开始意识到我的建议是一个非常糟糕的做法。我会在底部解释。

tl;博士:

  1. 从不使用播种机,只使用迁移
  2. 永远不要在迁移中使用你的 sequelize 模型,只写显式 SQL

我的另一个建议仍然成立,您使用一些 "configuration" 来驱动种子数据的生成。 (但是应该通过迁移插入种子数据。)

vv 不要这样做 vv


这是我更喜欢的另一种模式,因为我认为它更灵活且更容易理解。我在这里提供它作为已接受答案的替代方案(顺便说一句,对我来说这似乎很好),以防其他人发现它更适合他们的情况。


策略是利用您已经定义的 sqlz 模型来获取由其他播种机创建的数据,使用该数据生成您想要的任何新关联,然后使用 bulkInsert 插入新行。

在这个例子中,我正在跟踪一组人和他们拥有的汽车。我的 models/tables:

  • Driver:真人,可能拥有一辆或多辆真车
  • Car:不是特定的汽车,而是某人可能拥有的类型汽车(即make + model )
  • DriverCar: 真人拥有的真车,有颜色和购买年份

我们假设之前的播种机已经将所有已知的 Car 类型存储在数据库中:该信息已经可用,并且当我们可以将这些数据捆绑在系统。我们还将假设那里已经有 Driver 行,要么通过播种,要么因为系统是 in-use.

目标是以自动化方式从这两个数据源生成一大堆 fake-but-plausible DriverCar 关系。

const {
    Driver,
    Car
} = require('models')

module.exports = {

    up: async (queryInterface, Sequelize) => {

        // fetch base entities that were created by previous seeders
        // these will be used to create seed relationships

        const [ drivers , cars ] = await Promise.all([
            Driver.findAll({ /* limit ? */ order: Sequelize.fn( 'RANDOM' ) }),
            Car.findAll({ /* limit ? */ order: Sequelize.fn( 'RANDOM' ) })
        ])

        const fakeDriverCars = Array(30).fill().map((_, i) => {
            // create new tuples that reference drivers & cars,
            // and which reflect the schema of the DriverCar table
        })

        return queryInterface.bulkInsert( 'DriverCar', fakeDriverCars );
    },

    down: (queryInterface, Sequelize) => {
        return queryInterface.bulkDelete('DriverCar');
    }
}

这是部分实施。但是,它省略了一些关键细节,因为有上百万种方法可以给那只猫剥皮。这些碎片都可以收集在"configuration,"标题下,我们现在应该谈谈它。


当你生成种子数据时,你通常有这样的要求:

  • 我想至少创建一百个,或者
  • 我希望从可接受的集合中随机确定它们的属性,或者
  • 我想创建一个形状完全像 this
  • 的关系网

您可以尝试 hard-code 将这些内容添加到您的算法中,但这是困难的方法。我喜欢做的是在播种器的顶部声明 "configuration",以捕获所需种子数据的框架。然后,在 tuple-generation 函数中,我使用该配置程序生成实际行。该配置显然可以随心所欲地表达。我尝试将它们全部放入一个 CONFIG object 中,这样它们就可以保持在一起,这样我就可以轻松地在播种器实现中找到所有引用。

您的配置可能会为您的 findAll 调用暗示合理的 limit 值。它还可能指定用于计算要生成的种子行数的所有因素(通过明确说明 quantity: 30,或通过组合算法)。

作为思考的食物,这里有一个非常简单配置的例子,我用这个Driver汽车系统来确保我有 2 个司机,每个司机拥有一辆重叠的汽车(在 运行 时间随机选择特定汽车):

const CONFIG = {
    ownership: [
        [ 'a', 'b', 'c', 'd' ], // driver 1 linked to cars a, b, c, and d
        [ 'b' ],                // driver 2 linked to car b
        [ 'b', 'b' ]            // driver 3 has two of the same kind of car
    ]
};

我实际上也用过那些字母。在 运行 时,播种器实施将确定仅需要 3 个唯一的 Driver 行和 4 个唯一的 Car 行,并将 limit: 3 应用于 Driver.findAll,并且limit: 4Car.findAll。然后它会为每个唯一字符串分配一个真实的 randomly-chosen Car 实例。最后,在生成关联元组时,它使用字符串来查找选定的 Car,从中提取外键和其他值。

毫无疑问,有更奇特的方法来指定种子数据的模板。随心所欲地给那只猫剥皮。希望这能让您清楚如何将所选算法与实际的 sqlz 实现结合起来以生成连贯的种子数据。


为什么以上内容不好

如果您在迁移或种子文件中使用您的 sequelize 模型,您将不可避免地造成应用程序无法从零开始成功构建的情况。

如何避免发疯:

  1. 从不使用播种机,只使用迁移

(你可以在播种机中做的任何事情,你都可以在迁移中做。在我列举播种机的问题时请记住这一点,因为这意味着 none 这些问题对你有任何好处。)

默认情况下,sequelize 不会保留哪些播种者的记录 运行。是的,您可以将其配置为保留记录,但如果该应用程序已经在没有该设置的情况下部署,那么当您使用新设置部署您的应用程序时,它仍然会 re-run 所有播种机最后一次。如果那不安全,您的应用程序就会崩溃。我的经验是种子数据不能也不应该重复:如果它不立即违反唯一性约束,它会创建重复的行。

运行 seeders 是一个单独的命令,您需要将其集成到您的启动脚本中。这很容易导致 npm 脚本的激增,使应用程序启动更难遵循。在一个项目中,我将仅有的 2 个播种器转换为迁移,并将 startup-related npm 脚本的数量从 13 个减少到 5 个。

很难确定,但很难理解播种者的顺序 运行。还请记住,运行ning 迁移和播种器的命令是分开的,这意味着您不能有效地交错它们。您必须先 运行 所有迁移,然后 运行 所有播种机。随着数据库随时间变化,您将 运行 遇到我接下来描述的问题:

  1. 切勿在迁移中使用您的续集模型

当您使用 sequelize 模型获取记录时,它会显式获取它知道的每一列。因此,想象这样一个迁移序列:

  • M1:创建表 Car & Driver
  • M2:使用Car&Driver模型生成种子数据

那行得通。 Fast-forward 到您向 Car 添加新列的日期(例如,isElectric)。这涉及:(1) 创建一个 migraiton 来添加列,以及 (2) 在 sequelize 模型上声明新列。现在您的迁移过程如下所示:

  • M1:创建表 Car & Driver
  • M2:使用Car&Driver模型生成种子数据
  • M3: 添加 isElectric 到 Car

问题是您的续集模型总是反映 final 模式,而没有承认实际数据库是由突变的有序累积构建的事实。因此,在我们的示例中,M2 将失败,因为任何 built-in 选择方法(例如 Car.findOne)将执行 SQL 查询,例如:

SELECT
  "Car"."make" AS "Car.make",
  "Car"."isElectric" AS "Car.isElectric"
FROM
  "Car"

您的数据库将抛出异常,因为当 M2 执行时 Car 没有 isElectric 列。

在仅落后一次迁移的环境中不会出现此问题,但如果您雇用新的开发人员或在本地工作站上破坏数据库并从头开始构建应用程序,您就会感到厌烦。