【问题标题】:Sequelize dynamic seedingSequelize 动态播种
【发布时间】:2017-08-26 08:53:54
【问题描述】:

我目前正在使用 Sequelize.js 播种数据,并为关联 ID 使用硬编码值。这并不理想,因为我真的应该能够动态地做到这一点吗?例如,将用户和配置文件与“拥有一个”和“属于”关联相关联。我不一定想用硬编码的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 循环(不理想)。

【问题讨论】:

    标签: javascript sequelize.js seeding


    【解决方案1】:

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

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

    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]
          }
        )
      ])
    }
    

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

    【讨论】:

    • 谢谢@simon.ro!这是一个很大的帮助,很高兴知道我不是唯一一个为此苦苦挣扎的人
    【解决方案2】:

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

    tl;博士:

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

    我的另一个建议仍然支持您使用一些“配置”来驱动种子数据的生成。 (但种子数据应该通过迁移插入。)

    vv 不要这样做 vv


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


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

    在此示例中,我正在跟踪一组人和他们拥有的汽车。我的模型/表格:

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

    我们假设以前的播种机已将所有已知的Car 类型存储在数据库中:该信息已经可用,并且当我们可以将这些数据捆绑到系统中时,我们不想给用户带来不必要的数据输入负担。我们还将假设那里已经有 Driver 行,无论是通过播种还是因为系统正在使用中。

    我们的目标是以自动化的方式从这两个数据源生成一大堆虚假但似是而非的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');
        }
    }
    

    这是部分实现。然而,它忽略了一些关键细节,因为有上百万种方法可以剥去那只猫的皮。这些部分都可以在“配置”标题下收集,我们现在应该讨论它。


    当您生成种子数据时,您通常有如下要求:

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

    您可以尝试将这些内容硬编码到您的算法中,但这很难。我喜欢做的是在播种机顶部声明“配置”,以捕获所需种子数据的骨架。然后,在元组生成函数中,我使用该配置以程序方式生成真实行。该配置显然可以随心所欲地表达。我尝试将它们全部放入一个 CONFIG 对象中,以便它们保持在一起,这样我就可以轻松地找到播种器实现中的所有引用。

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

    作为思考的食物,这里是一个非常简单配置的示例,我将它与这个 DriverCar 系统一起使用,以确保我有 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: 4 应用于Car.findAll。然后它会为每个唯一的字符串分配一个真实的、随机选择的Car 实例。最后,在生成关联元组时,它使用字符串查找选定的Car,从中提取外键和其他值。

    毫无疑问,为种子数据指定模板的方式更为新颖。随心所欲地剥那只猫的皮。希望这可以清楚地说明您如何将您选择的算法与您的实际 sqlz 实现结合起来以生成连贯的种子数据。


    为什么上面的不好

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

    如何避免疯狂:

    1. 切勿使用播种机,仅使用迁移

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

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

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

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

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

    当您使用 sequelize 模型来获取记录时,它会显式地获取它知道的每一列。所以,想象一下这样的迁移顺序:

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

    这会奏效。快进到向 Car 添加新列的日期(例如,isElectric)。这包括:(1) 创建一个 migraiton 以添加列,以及 (2) 在 sequelize 模型上声明新列。现在您的迁移过程如下所示:

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

    问题在于您的续集模型始终反映 final 模式,而没有承认实际数据库是通过有序增加突变构建的这一事实。因此,在我们的示例中,M2 将失败,因为任何内置的选择方法(例如 Car.findOne)都会执行如下 SQL 查询:

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

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

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

    【讨论】:

    • Tom,您关于使用迁移插入数据(例如带有品牌和型号的 Car)的建议对我来说很有意义,因为这些数据很可能需要使应用程序正常运行。您说“永远不要使用播种机”,但是对于上面示例中的假生成驱动程序等测试数据呢?
    • 永远不要使用播种机。该规则没有例外。如果您需要自动插入数据,请使用迁移。它们具有相同的能力。 “播种机”只是 Sequelize 处理不佳的迁移。通过检查环境变量,您甚至可以编写一个在应用程序在生产或 CI(或反之亦然)中运行时执行无操作的迁移。 (虽然,sqlz 会将“no-op”解释为“迁移成功完成”。而且 sqlz 没有为此提供任何技术,因此您必须构建自己的解决方案。也许现在有一个 npm 包......)
    猜你喜欢
    • 2019-11-20
    • 2018-03-05
    • 1970-01-01
    • 2017-06-06
    • 2018-08-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多