首页 > 解决方案 > 将 async/await 和 Promises 与 Javascript(以及 Node.js)一起使用的正确方法是什么

问题描述

我实际上正在构建一个脚本来从 MySQL 数据库中提取数据,然后填充 MongoDB。在这个过程中,有一些异步的东西,比如建立与 MySQL(通过 Sequelize 库)和 MongoDB(通过 Mongoose 库)的连接,还有一些同步的东西,比如获取和转换数据。

我阅读了很多关于 async/await 和 Promises 的内容,并且我的脚本在全球范围内执行我想要的操作,但仍然存在一些问题。


这是代码:


迁移.class.mjs

import MigrationBase from './Base/MigrationBase.class.mjs';

export default class Migration extends MigrationBase
{
    constructor(config) {
        super(config);

        this.mysqlData = {};
        this.mongoData = {};
    }

    async run() {
        await this.selectMySQLData();
        let docs = await this.convertMySQLToMongo();
        await this.checkConvertedData(docs);
        await this.insertMongoData();
    }

    async selectMySQLData() {
        return new Promise(async resolve => {
            await this.runSequelize();
            console.log('B - Grabbing MySQL data\n');

            for(var key in this.mysqlModels) {
                if (this.mysqlModels.hasOwnProperty(key)) {
                    let search = { raw: true };
                    this.mysqlData[key] = await this.mysqlModels[key].findAll(search);
                }
            }

            await this.closeSequelize();
            resolve();
        });
    };

    convertMySQLToMongo() {
        return new Promise(async resolve => {
            console.log('D - Convert MySQL data to MongoDB\n');

            let customersDocument = this.defaultDocuments.customers;
            let personalInfosDocument = this.defaultDocuments.personal_infos;
            let billingInfosDocument = this.defaultDocuments.billing_infos;
            // ... etc ...

            await Object.entries(this.mysqlData.customer).forEach(async keyRow => {
                let [key, row] = keyRow;

                await Object.entries(row).forEach(async keyValue => {
                    customersDocument = await this._processCustomersFields(customersDocument, 'Customer', keyValue);
                    personalInfosDocument = await this._processPersonalInfosFields(personalInfosDocument, 'PersonalInfo', keyValue);
                    billingInfosDocument = await this._processBillingInfosFields(billingInfosDocument, 'BillingInfo', keyValue);
                    // ... etc ...
                    
            });

            resolve([
                customersDocument,
                personalInfosDocument,
                billingInfosDocument,
                // ... etc ...
            ]);
        });
    };

    checkConvertedData([
        customersDocument,
        personalInfosDocument,
        billingInfosDocument,
        // ... etc ...
    ]) {
        return new Promise(resolve => {
            console.log('E - Checking converted data');

            if (! this._isNull(customersDocument, 'Customers')) {
                this.mongoData.customers = customersDocument;
            }
            
            if (! this._isNull(personalInfosDocument, 'PersonalInfos')) {
                this.mongoData.personal_infos = personalInfosDocument;
            }
            
            if (! this._isNull(billingInfosDocument, 'BillingInfos')) {
            
            }   this.mongoData.billing_infos = billingInfosDocument;
            // ... etc ...
            
            resolve();
        });
    }

    async insertMongoData() {
        return new Promise(async resolve => {
            await this.runMongoose();
            console.log('G - Insert MongoDB data.');
            
            await this.mongoModels.customers.create(this.mongoData.customers);
            await this.mongoModels.personal_infos.create(this.mongoData.personal_infos);
            await this.mongoModels.billing_infos.create(this.mongoData.billing_infos);
            // ... etc ...

            await this.closeMongoose();
            resolve();
        });
    };

    _processCustomersFields(defaultDoc, docName, [colName, val]) {
        return new Promise(resolve => {
            switch (colName) {
                case 'id_customer':
                    console.log(`${docName}: ${colName} => ${val}`);
                    defaultDoc.id = val;
                    break;
                case 'email_customer':
                    console.log(`${docName}: ${colName} => ${val}`);
                    defaultDoc.email = val;
                    break;
                case 'password_customer':
                    console.log(`${docName}: ${colName} => ${val}`);
                    defaultDoc.password = val;
                    break;
                // ... etc ...
            }
    
            resolve(defaultDoc);
        });
    }

    _processPersonalInfosFields(defaultDoc, docName, [colName, val]) {
        return new Promise(resolve => {
            switch (colName) {
                // ... Same kind of code as in _processCustomersFields() ...
            }
            
            resolve(defaultDoc);
        });
    }

    _processBillingInfosFields(defaultDoc, docName, [colName, val]) {
        return new Promise(resolve => {
            switch (colName) {
                // ... Same kind of code as in _processCustomersFields() ...
            }
            
            resolve(defaultDoc);
        });
    }
    
    _isNull(document, mongoName) {
        if (document !== null) {
            console.log(`\n${mongoName}:\n`, JSON.stringify(document));
            return false;
        } else {
            console.log(`Error processing \`${mongoName}\` data!`);
            return true;
        }
    }
    
    _valueExists(val) {
        return (val !== null && val !== "" && typeof val !== "undefined")
            ? true
            : false
        ;
    }
}

MigrationBase.class.mjs

import Sequelize from 'sequelize';
import DataTypes from 'sequelize';
import Mongoose from 'mongoose';
import Crypto from 'crypto';
import Models from '../../../models.mjs';
import Schemas from '../../../schemas.mjs';

export default class MigrationBase
{
    constructor(config) {
        this.config = config;
        this.sequelize = this.createSequelize();
        this.mongoose = Mongoose;
        this.defaultDocuments = this.initDefaultDocuments();
        this.mysqlModels = this.initMysqlModels();
        this.mongoModels = this.initMongoSchemas();
        this.mysqlData = {};
        this.mongoData = {};
    }

    createSequelize() {
        return new Sequelize(
            this.config.mysql.dbName,
            this.config.mysql.dbUser,
            this.config.mysql.dbPass,
            this.config.sequelize
        );
    }

    initDefaultDocuments() {
        const defaultDocument = {
            "deleted_at": 0 // Thu Jan 01 1970 01:00:00 GMT+0100
        };

        let defaultDocuments = {
            "customers": Object.assign({}, defaultDocument),
            "personal_infos": Object.assign({}, defaultDocument),
            "billing_infos": Object.assign({}, defaultDocument)
            // ... etc ...
        };

        return defaultDocuments;
    }

    initMysqlModels() {
        return {
            "customer": Models.Customer(this.sequelize, DataTypes),
            "billing_address": Models.BillingAddress(this.sequelize, DataTypes),
            // ... etc ...
        };
    }

    initMongoSchemas() {
        return {
            "customers": this.mongoose.model('Customer', Schemas.Customers),
            "personal_infos": this.mongoose.model('PersonalInfo', Schemas.PersonalInfos),
            "billing_infos": this.mongoose.model('BillingInfo', Schemas.BillingInfos),
            // ... etc ...
        }
    }

    async runSequelize() {
        console.log('A - Connection to MySQL');

        try {
            await this.sequelize.authenticate();
            console.log('Connection to MySQL has been established successfully.\n');
        } catch (err) {
            console.error("Unable to connect to the MySQL database:", err + '\n');
        }
    }

    async closeSequelize() {
        console.log('C - Closing MySQL connection.\n');

        await this.sequelize.close();
    };

    runMongoose() {
        return new Promise(async resolve => {
            console.log('F - Connection to MongoDB');

            try {
                await this.mongoose.connect(
                    `mongodb://${this.config.mongo.dbHost}:${this.config.mongo.dbPort}/${this.config.mongo.dbName}`,
                    { useNewUrlParser: true, useUnifiedTopology: true }
                );

                console.log('Connection to MongoDB has been established successfully.');
            } catch (err) {
                console.error('Unable to connect to the MongoDB database: ', err);
            }

            resolve();
        });
    }

    async closeMongoose() {
        console.log('H - Closing MongoDB connection.');
        await this.mongoose.connection.close();
    };
}

这是日志输出:

A - Connection to MySQL
Connection to MySQL has been established successfully.

B - Grabbing MySQL data

C - Closing MySQL connection.

D - Convert MySQL data to MongoDB

Customer: id_customer => 1
Customer: email_customer => contact@example.com
Customer: password_customer => 0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d
// ... etc ...
PersonalInfo: id_customer => 1
PersonalInfo: lastname_customer => Doe
PersonalInfo: firstname_customer => John
// ... etc ...
E - Checking converted data

Customers:
 {"deleted_at":0,"id":"000000000000000000000001","email":"contact@example.com","password":"0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d", ... etc ... }

PersonalInfos:
 {"deleted_at":0,"customer_id":"000000000000000000000001","last_name":"Doe","first_name":"John", ... etc ... }

BillingInfos:
 {"deleted_at":0}
BillingInfos: id_customer => 1
BillingInfo: company => ExampleCompany
F - Connection to MongoDB
BillingInfos: lastname => Doe
BillingInfo: firstname => John
Connection to MongoDB has been established successfully.
G - Insert MongoDB data.
/home/user/Workspaces/namespace/project-name/node_modules/mongoose/lib/document.js:2757
    this.$__.validationError = new ValidationError(this);
                               ^

ValidationError: BillingInfos validation failed: id_customer: Cast to ObjectId failed for value "1" (type number) at path "customer_id", values: Path `values` is required., id: Path `id` is required.

在这里,我们可以看到正确的顺序:

A - Connection to MySQL

B - Grabbing MySQL data

C - Closing MySQL connection

D - Convert MySQL data to MongoDB

然后我们可以看到E - Checking converted data,但转换过程并没有完成,尽管 await 语句和它返回一个 Promise 的事实。

之后,我们还可以看到BillingInfos: id_customer => 1BillingInfo: company => ExampleCompany表示转换过程仍在循环中进行。

然后F - Connection to MongoDB

然后另一个转换日志BillingInfos: lastname => DoeBillingInfo: firstname => John(转换过程仍在循环中做事)。

然后G - Insert MongoDB data.

最后是验证错误,因为某些 Mongo 文档不完整,因此规则未完整。


问题?

所以问题是我在这里做错了什么?

正如我所说,我阅读了很多关于 async/await 和 Promises 的内容,但仍然在努力理解它为什么不起作用。

提前致谢,如果您需要更多信息,请告诉我。

标签: javascriptasynchronousmongooseasync-awaitsequelize.js

解决方案


那是因为await 在你试图在你的convertMySQLToMongo()函数中执行的 forEach() 内部不起作用。

有很多方法可以解决它,其中一种方法是使用for ... of而不是forEach()

for (const keyRow of Object.entries(this.mysqlData.customer)) {
    let [key, row] = keyRow;
  
    for (const keyValue of Object.entries(row)) {
        customersDocument = await this._processCustomersFields(customersDocument, 'Customer', keyValue);
        personalInfosDocument = await this._processPersonalInfosFields(personalInfosDocument, 'PersonalInfo', keyValue);
        billingInfosDocument = await this._processBillingInfosFields(billingInfosDocument, 'BillingInfo', keyValue);
    }
}

推荐阅读