如何利用UUIDs创建新的数据库

46 阅读5分钟

为什么?

迁移的原因是,我们希望Domain Layer1的代码能够创建Domain Objects,而不需要依赖对数据库的往返操作。

在自动增加ID的情况下,这是不可能的(当然,除非我们要做一些黑客的事情)。

能够在不依赖数据库连接的情况下创建域对象是令人向往的,因为这意味着我们的单元测试可以快速运行,而且将创建对象持久化对象之间的关注点分开是一个好主意。

这种技术也简化了我们如何使用域事件来允许其他子域和边界上下文对我们系统的变化做出反应

我意识到,对于如何将一个有40多个表的现有生产数据库从自动递增的ID迁移到UUID2,外面并没有很多信息,所以这里我记录了我是如何完成的。


完成工作

最好的办法是重新创建一个新的数据库,其中有相同的表,但有不同的主键数据类型。从那里,我可以把旧的数据插入新的数据库,然后交换生产数据库。

第1步:转储prod数据库+导入模式

我使用的是MySQL,所以我能够把整个生产数据库转储到一个独立的文件中,然后用MySQL工作平台的Data Import 工具把它导入本地。

第2步:创建每个表的数据的JSON数据文件

我做的下一件事是将每个模型的所有行导出为json文件,格式为out/TableName.json

import models from '../../src/infra/models'
import * as fs from 'fs'
import * as path from 'path'

/**
 * Each file gets written out to `/out/{modelName}.json`
 */

const writeToFile = (filePath, data: any) => {
  return new Promise((resolve, reject) => {
    fs.writeFile(path.join(__dirname, filePath), 
      JSON.stringify(data), 'utf8', () => {
      return resolve();
    });
  })
}


class CreateDatabaseData {
  // All sequelize models
  private models: any

  constructor (models: any) {
    this.models = models;
    this.init();
  }

  async init () {
    // ['User', 'Product', etc...]
    const modelNames = Object.keys(models);

    for(const modelName of modelNames) {
      const Model = this.models[modelName];
      console.log("Getting all the data for: " + modelName)
      // Select * from current moodel
      const rawData = await Model.findAll({});
      console.log('Writing....');
      // Write it to the json file in the out/ folder
      await writeToFile('out/' + modelName + ".json", rawData);
    }

    console.log('Done.')
    process.exit(1);
  }
}

new CreateDatabaseData();

为了执行这个,我不得不给Node.js增加一点内存来运行这个。

node --max_old_space_size=8192 -r ts-node/register scripts/uuid-migration/create-files.ts

最后,我有40个表,所有的现有数据都在json文件中。

第3步:放弃本地生产数据库

我们放弃本地生产数据库,并创建一个新的模式。

drop schema app_database
create schema app_database

第4步:改变所有的Sequelize模型以使用UUIDs

通常,在我们的Sequelize项目中,我们会把所有的模型放在一个/models 。这一步意味着要进入每个模型,更新每个主键和外键关系,以使用UUIDs。

  user_id: {
-   type: DataTypes.INTEGER(11),
+   type: DataTypes.UUID,
-   autoIncrement: true,
+   defaultValue: DataTypes.UUIDV4,
    allowNull: false,
    primaryKey: true
  },

Sequelize允许你选择使用UUIDV4或UUIDV1作为默认值。它还允许你提供[你自己的UUID生成函数]。

第5步:更新初始迁移和任何播种机文件

我们不仅应该更新模型,而且还应该更新初始迁移文件,以改变

  user_id: {
-   type: Sequelize.INTEGER(11),
+   type: Sequelize.UUID,
-   autoIncrement: true,
+   defaultValue: Sequelize.UUIDV4,
    allowNull: false,
    primaryKey: true
  },

我们做的事情和第4步差不多,但在我们的初始迁移文件中。还有一点不同的是,我们在这里可以访问Sequelize ,但不能访问DataTypes

第6步:运行迁移

在我们更新了迁移文件并更新了模型之后,除了任何播种机文件之外,现在是时候运行sequelize迁移了。

npx sequelize db:migrate --env production && npx sequelize db:seed:all --env production

第7步:确定表的创建顺序

为了让我们插入所有保存在JSON文件中的现有数据,我们需要知道以何种顺序插入数据,以便不引用还不存在的表。

看一下通过我的迁移文件创建的表的历史,我可以弄清楚这个顺序。

const modelOrder = [
  'User', // first table ever created
  'Product',
  // ...
  // ... more tables
  // last table
]

这花了我一点时间,但在我把它们都保存在一个数组中之后,我就可以进入最后一步了。

第8步:导入数据

最后的部分是实际导入数据。

脚本的一般伪代码是。

Loop through each of the tables in the order created
  for each table 
    get the data file
    for each row in the data file
      prepareRowWithUUIDs(row attributes, row data)

In prepareRowWithUUIDs(row attributes, row data)
  for each attribute in row attributes
    if the type is UUID
      use the old auto-incremented row data to hash it into a new UUID
  
  return row data

这里是最后的脚本。

import * as fs from 'fs'
import * as path from 'path'
import models from '../../src/infra/models'
import { get } from 'lodash'
const createUUID = require('uuid-by-string')

const modelOrder = [
  'User', // first table ever created
  'Product',
  // ...
  // ... more tables
  // last table
]

const OpenDataFile = (fileName) => {
  return JSON.parse(fs.readFileSync(path.join(__dirname, `out/${fileName}.json`), 'utf8'));
}

class MigrateToUUID {
  private models: any;

  constructor () {
    this.init();
  }

  async init () {
    const models = await getModels();
    this.models = models;

    for (let modelName of modelOrder) {
      const model = this.models[modelName];
      const data = OpenDataFile(modelName);
      console.log(`Inserting data into ${modelName}...`)
      await this.insertDataToTable(model, data, modelName);
    }

    process.exit(1);
  }

  prepareRowWithUUIDs (rowAttributes: Object, rowData: Object): any {
    for (let [i, attrName] of Object.keys(rowAttributes).entries()) {
      const attr = rowAttributes[attrName];
      
      if (get(attr, 'type.key') === "UUID") {
        const oldId = rowData[attrName];
        const newUUID = createUUID(String(oldId));
        rowData[attrName] = newUUID;
      }
    }

    return rowData;
  }

  async insertDataToTable (modelInstance: any, data: any[], modelName: string) {
    for(let row of data) {
      row = this.prepareRowWithUUIDs(modelInstance.attributes, row);
      try {
        await modelInstance.create(row);
      } catch(err) {
        console.log(`Error occured in model => ${modelName}`);
        console.log(err);
      }
    }
  }
}

new MigrateToUUID();

最后,在这一点上--我所要做的就是确保所有的东西都还能用,并把旧的生产数据库换成利用UUIDs的新数据库。

总的来说,这个过程相当痛苦的。

关于UUID性能的讨论

我很好奇UUIDs与ints相比的性能,以及我现在是否应该担心性能上的权衡。