如何用迁移修复Sequelize模型

84 阅读5分钟

问题

今天早上,我看了一下我在Univjobs编码的当前学生注册流程。当我打开学习领域的下拉列表时,我注意到每个字段都有一些重复的内容。这肯定是不对的。应该只有大约80-90个 "研究领域",这是我们在创建数据库时最初播种的东西。

image.png

为了确认这不只是前端渲染的问题,我看了一下从列表API返回的内容。

image.png

是的,有超过600个研究领域。我肯定做错了什么。

在对我们目前的数据库进行了一些调查,查看了模型,并回忆了我与这个项目的集成工程师的谈话,我意识到发生了什么。

我们刚刚转移到使用Elastic Beanstalk,我们必须更新我们的CI。在部署脚本的最后,我们正在运行最初的数据库播种机。现在,如果数据库约束已经被满足(比如唯一约束),数据库播种器应该会失败。我预计,如果我们试图两次添加相同的学习区域,它就会失败。

看一下学习领域的模型,我们可以注意到一些事情。

module.exports = function(sequelize, DataTypes) {
  const ListsAreaOfStudy = sequelize.define('lists_area_of_study', {
    area_of_study_id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      autoIncrement: true,
      primaryKey: true
    },
    name: {
      type: DataTypes.STRING(90),
      allowNull: false,
      unique: true // <== Strange, this seems to not have worked.
    }
  },{
    timestamps: true,
    underscored: true,
    tableName: 'lists_area_of_study',
    instanceMethods: {}
  })

  ListsAreaOfStudy.associate = (models) => {}

  return ListsAreaOfStudy;
};

现在,我并没有完全做错,我在name 字段上有一个unique 约束,但是我可能只在Sequelize模型文件中做了这个,而不是在我第一次用脚本创建这个模型时的迁移脚本中。

让我们看看这个模型在我们的数据库中是什么样子的。

CREATE TABLE `lists_area_of_study` (
  `area_of_study_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(90) NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`area_of_study_id`)
) ENGINE=InnoDB AUTO_INCREMENT=640 DEFAULT CHARSET=utf8;

是的,这很可能是发生了什么,我们在数据库中丢失了unique 的约束。

那么我们如何解决这个问题呢?因为我们现在处理的是数以百计的生产用户使用该应用程序的结果,并且在他们的账户和他们的学习领域之间建立了一种关系。显然,我们不能去手动改变这一切,所以我们需要使用Sequelize迁移的脚本。

方法

以下是我们在迁移脚本中要做的事情。

  1. 获取所有的研究领域
  2. 将所有多余的研究领域的关系重新分配给初始id(1-60)。
  3. 删除多余的(60,以此类推)。
  4. 启用唯一约束并将autoIncrement:true 字段设置为false。

这应该能解决我们的问题......但首先,我们要做一个额外的数据库备份--以防我们搞砸了什么。

创建迁移

sequelize migration:create --name "fix area of studies"

这将在我的migrations文件夹中为我创建一个新的迁移文件,名为20180721190331-fix-area-of-studies.js

现在,我们要开始建立它了。

获取所有的研究领域

首先,我们要获取所有的研究领域。

'use strict';

const Models = require('../../app/models');

module.exports = {
  up: (queryInterface, Sequelize) => {
    return new Promise(async (resolve, reject) => {
      let areaOfStudies;
      let map = {};

      try {
        // Get all area of studies
        areaOfStudies = await Models.ListsAreaOfStudy.findAll({ 
          where: {} 
        })
        console.log(areaOfStudies.length);
      }

      catch (err) {
        console.log(err);
      }
    })
  },

  down: (queryInterface, Sequelize) => {}
};

所以,这应该能得到我们所有的研究领域。

image.png

呀,有639个。

好的,接下来我们将把它们全部分类到地图上。

// Place all of them in a map by name
areaOfStudies.forEach((area) => {
  if (!map.hasOwnProperty(area.name)) map[area.name] = [];
  map[area.name].push(area.area_of_study_id);
})

让我们看看到目前为止我们有什么。

image.png

到目前为止还不错。现在,让我们对这些键进行迭代(同时也对一些花哨的ES6语法进行迭代),并打印出我们打算对每个值做什么。

// Now, for each item in the map, we have to 
// get the students that have that area of study
for (let [name, ids] of Object.entries(map)) {
  // Get all students that have this area of study
  let properId = ids[0];
  let scrapIds = ids.splice(1);

  for(let id of scrapIds) {
    console.log(
      `Students who have ${name} with ${id} should be => ${properId}
    `);
  }
}

让我们看看到目前为止我们有什么。

image.png

好吧,看起来我们肯定是在正确的轨道上。如果你没有注意到,我喜欢在做这种破坏性的事情时,把每一步都打印出来,以防止在我做了一些我不想做的事情时不得不重新开始。

重新分配关系

现在让我们实际操作一下。让我们重新分配这些关系。我们只需在这里添加一行...

// Now, for each item in the map, we have to 
// get the students that have that area of study
for (let [name, ids] of Object.entries(map)) {
  // Get all students that have this area of study
  let properId = ids[0];
  let scrapIds = ids.splice(1);

  for(let id of scrapIds) {
    console.log(
      `Students who have ${name} with ${id} should be => ${properId}
    `);
    await queryInterface.sequelize.query(
      `UPDATE student SET area_of_study_id = ${properId} where area_of_study_id = ${id};`
    )
  }
}

删除多余的研究领域

我们可以用这一行来清理其他我们不需要的东西,真的很容易。

await queryInterface.sequelize.query(`DELETE from lists_area_of_study WHERE area_of_study_id >= 60;`)

更新模型定义

我们需要做的最后一点是更新我们的模型定义,以便这种情况不会再次发生。 让我们在名字字段上添加一个唯一约束(这次是真的)。

await queryInterface.addConstraint('lists_area_of_study', ['name'], {
  type: 'unique',
  name: 'unique_area_of_study'
});

我们可以通过检查表的定义来确认这一点。下面是之前的情况。

CREATE TABLE `lists_area_of_study` (
  `area_of_study_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(90) NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`area_of_study_id`)
) ENGINE=InnoDB AUTO_INCREMENT=640 DEFAULT CHARSET=utf8;

然后是之后。

CREATE TABLE `lists_area_of_study` (
  `area_of_study_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(90) NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`area_of_study_id`),
  UNIQUE KEY `unique_area_of_study` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=640 DEFAULT CHARSET=utf8;

问题解决了。

这就是整个脚本。

'use strict';

const Models = require('../../app/models');

module.exports = {
  up: (queryInterface, Sequelize) => {
    return new Promise(async (resolve, reject) => {
      let areaOfStudies;
      let map = {};

      try {
        // Get all
        areaOfStudies = await Models.ListsAreaOfStudy.findAll({ where: { } })
        
        // Place all of them in a map by name
        areaOfStudies.forEach((area) => {
          if (!map.hasOwnProperty(area.name)) map[area.name] = [];
          map[area.name].push(area.area_of_study_id);
        })

        // Now, for each item in the map, we have to get the students that have that area of study
        for (let [name, ids] of Object.entries(map)) {
          // Get all students that have this area of study
          let properId = ids[0];
          let scrapIds = ids.splice(1);

          for(let id of scrapIds) {
            console.log(`Updating students who have ${name} with ${id} should be => ${properId}`);
            await queryInterface.sequelize.query(`UPDATE student SET area_of_study_id = ${properId} where area_of_study_id = ${id};`)
          }
        }

        // Then we'll delete all the old area of studies
        await queryInterface.sequelize.query(`DELETE from lists_area_of_study WHERE area_of_study_id >= 60;`)
        console.log("Deleted old")

        // Then, we'll ensure that's a unique constraint
        await queryInterface.addConstraint('lists_area_of_study', ['name'], {
          type: 'unique',
          name: 'unique_area_of_study'
        });
        console.log("Add unique constraint to lists_area_of_study")

        resolve();
      }

      catch (err) {
        console.log(err);
      }
    })
  },

  down: (queryInterface, Sequelize) => {}
};

sequelize ORM确实是用NodeJS构建强大网络应用的一个伟大工具。如果你正在考虑为NodeJS使用关系型数据库,并寻找一个可以使用的ORM,我会推荐Sequelize,因为它背后有一个非常好的社区,而且比其他一些工具更成熟。

然而,我对开始使用它的人的建议是,阅读文档。而且要读好它。Sequelize在生产中可能非常有用,但如果使用不当,也可能造成很多麻烦。

如果你想了解更多关于用Sequelize ORM构建应用程序的真实内容或教程,请告诉我!我已经经历了很多困难。我已经经历了它的壕沟。