嵌套的 Javascript 承诺 - 从 Firestore 获取数据

Posted

技术标签:

【中文标题】嵌套的 Javascript 承诺 - 从 Firestore 获取数据【英文标题】:Nested Javascript promises - fetching data from firestore 【发布时间】:2021-02-24 00:26:09 【问题描述】:

在过去的 3 天里,我一直被这个错误困扰,我几乎尝试了一切,尝试以 1000 种方式构建承诺,但似乎没有任何效果。也许我正在失去“大局”,所以希望新的眼睛会有所帮助。感谢阅读:

我有一个在 Firebase Cloud Functions 中运行的计划函数。代码试图完成的是

    检查文档是否过期并将其更改为“非活动”>>这部分有效 如果一个文档被设置为非活动状态,我想看看我在 firestore 数据库中是否有任何其他相同“类型”的文档。如果没有其他相同类型的文档,那么我想从我的文档“类型”中删除该类型。

在我最近的尝试中(复制如下),我检查快照中是否有文档(这意味着有另一个相同类型的文档,因此不必删除该文档)。然后如果 res!== true,我会删除文档。

问题是由于某种原因, res 永远不会是真的......也许“res”承诺在“snapshot”承诺之前解决?

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.scheduledFunction = functions.pubsub
.schedule('0 23 * * *').timeZone('Europe/Madrid')
.onRun( async (context) => 

    const expiredDocs = await admin.firestore().collection('PROMOTIONS_INFO')
    .where('active','==',true)
    .where('expiration', '<=', new Date())
    .get()
    .then(async (snapshot) => 
        await Promise.all(snapshot.docs.map( async (doc) => 
            doc.ref.update(active: false)
            const type = doc.data().business.type
            const id = doc.data().id

            const exists = await admin.firestore().collection('PROMOTIONS_INFO')
                .where('active','==',true)
                .where('business.type','==', type)
                .where('id', '!=', id)
                .limit(1)
                .get()
                .then((snapshot) => 
                    snapshot.docs.map((doc)=>return true)
                ).then(async (res) => 
                    res===true ? null : 
                    (await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
                    .update('types', admin.firestore.FieldValue.arrayRemove(type)))
                )
            ))
        );
);

【问题讨论】:

如果你要使用() =&gt; /* code */ 风格的箭头函数,你需要从你所有的函数中return。这里还有一个“末日金字塔”,promise 和 async/await 旨在消除。考虑是否可以在结束.get() 之后等待expiredDocs 集合。删除.then()。然后等待exists 集合。然后循环遍历它并对每个值运行更新。 【参考方案1】:

为了达到您想要的结果,您可能需要考虑使用Batched Writes 并将您的代码拆分为不同的步骤。

一组可能的步骤是:

    获取所有仍处于活动状态的过期文档 没有过期的文件?记录结果和结束函数。 对于每个过期的文档: 将其更新为非活动状态 存储它的类型以供以后检查 对于要检查的每种类型,检查是否存在具有该类型的活动文档,如果不存在,则存储该类型以供以后删除。 没有要删除的类型?记录结果和结束函数。 删除所有需要删除的类型。 记录结果和结束函数。

在上述步骤中,第3步可以使用Batched Writes,第6步可以使用arrayRemove() field transform,可以去掉multiple elements at once,减轻数据库负担。


const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.scheduledFunction = functions.pubsub
.schedule('0 23 * * *').timeZone('Europe/Madrid')
.onRun( async (context) => 
    // get instance of Firestore to use below
    const db = admin.firestore();
    
    // this is reused often, so initialize it once.
    const promotionsInfoColRef = db.collection('PROMOTIONS_INFO');

    // find all documents that are active and have expired.
    const expiredDocsQuerySnapshot = await promotionsInfoColRef
        .where('active','==',true)
        .where('expiration', '<=', new Date())
        .get();

    if (expiredDocsQuerySnapshot.empty) 
        // no expired documents, log the result
        console.log(`No documents have expired recently.`);
        return; // done
     
    
    // initialize an object to store all the types to be checked
    // this helps ensure each type is checked only once
    const typesToCheckObj = ;
    
    // initialize a batched write to make changes all at once, rather than call out to Firestore multiple times
    // note: batches are limited to 500 read/write operations in a single batch
    const makeDocsInactiveBatch = db.batch();
    
    // for each snapshot, add their type to typesToCheckObj and update them to inactive
    expiredDocsQuerySnapshot.forEach(doc => 
        const type = doc.get("business.type"); // rather than use data(), parse only the property you need.
        typesToCheckObj[type] = true; // add this type to the ones to check
        makeDocsInactiveBatch.update(doc.ref,  active: false ); // add the "update to inactive" operation to the batch
    );
    
    // update database for all the now inactive documents all at once.
    // we update these documents first, so that the type check are done against actual "active" documents.
    await makeDocsInactiveBatch.commit();
    
    // this is a unique array of the types encountered above
    // this can now be used to check each type ONCE, instead of multiple times
    const typesToCheckArray = Object.keys(typesToCheckObj);
    
    // check each type and return types that have no active promotions
    const typesToRemoveArray = (await Promise.all(
        typesToCheckArray.map((type) => 
            return promotionsInfoColRef
                .where('active','==',true)
                .where('business.type','==', type)
                .limit(1)
                .get()
                .then((querySnapshot) => querySnapshot.empty ? type : null) // if empty, include the type for removal
        )
    ))
    .filter((type) => type !== null); // filter out the null values that represent types that don't need removal
    
    // typesToRemoveArray is now a unique list of strings, containing each type that needs to be removed
    
    if (typesToRemoveArray.length == 0) 
        // no types need removing, log the result
        console.log(`Updated $expiredDocsQuerySnapshot.size expired documents to "inactive" and none of the $typesToCheckArray.length unique types encountered needed to be removed.`);
        return; // done
    
    
    // get the types document reference
    const typesDocRef = promotionsInfoColRef.doc('types');

    // use the arrayRemove field transform to remove all the given types at once
    await typesDocRef.update(types: admin.firestore.FieldValue.arrayRemove(...typesToRemoveArray) );

    // log the result
    console.log(`Updated $expiredDocsQuerySnapshot.size expired documents to "inactive" and $typesToRemoveArray.length/$typesToCheckArray.length unique types encountered needed to be removed.\n\nThe types removed: $typesToRemoveArray.sort().join(", ")`);

注意:错误检查被省略,应该执行。

批次限制

如果您希望达到每批次 500 次操作的限制,您可以在批次周围添加一个包装器,以便它们根据需要自动拆分。此处包含一种可能的包装器:

class MultiBatch 
    constructor(dbRef) 
        this.dbRef = dbRef;
        this.batchOperations = [];
        this.batches = [this.dbRef.batch()];
        this.currentBatch = this.batches[0];
        this.currentBatchOpCount = 0;
        this.committed = false;
    
    
    /** Used when for basic update operations */
    update(ref, changesObj) 
        if (this.committed) throw new Error('MultiBatch already committed.');
        if (this.currentBatchOpCount + 1 > 500) 
            // operation limit exceeded, start a new batch
            this.currentBatch = this.dbRef.batch();
            this.currentBatchOpCount = 0;
            this.batches.push(this.currentBatch);
        
        this.currentBatch.update(ref, changesObj);
        this.currentBatchOpCount++;
    
    
    /** Used when an update contains serverTimestamp, arrayUnion, arrayRemove, increment or decrement (which all need to be counted as 2 operations) */
    transformUpdate(ref, changesObj) 
        if (this.committed) throw new Error('MultiBatch already committed.');
        if (this.currentBatchOpCount + 2 > 500) 
            // operation limit exceeded, start a new batch
            this.currentBatch = this.dbRef.batch();
            this.currentBatchOpCount = 0;
            this.batches.push(this.currentBatch);
        
        this.currentBatch.update(ref, changesObj);
        this.currentBatchOpCount += 2;
    
    
    commit() 
        this.committed = true;
        return Promise.all(this.batches.map(batch => batch.commit()));
    

要使用它,请将原始代码中的 db.batch() 替换为 new MultiBatch(db)。如果批处理中的更新(如someBatch.update(ref, ... ))包含字段转换(如FieldValue.arrayRemove()),请确保改用someMultiBatch.transformUpdate(ref, ... ),以便将单个更新正确计为2个操作(读取和写入)。

【讨论】:

有效!我非常感谢您的时间和全面的回答。我在想,鉴于我们有 typesToRemoveArray,也许避免对数据库进行如此多的读/写的一种方法是读取 Types 文档,合并代码中的两个数组,然后将其写入数据库。这会对多批次方法产生影响吗?另外,你对运行时间有什么考虑吗? @Mireia 你说的很对。今天重新审视代码时,我记得arrayRemove 一次支持多个元素 - 当作为单独的参数传入时 - 这可以使用扩展 (...) 运算符来实现。这完全消除了第二批操作。就运行时间而言,上面的代码可能是您能够实现的最精简的代码。最慢的部分将是当您有大量要检查的类型时检查类型是否仍然存在 - 但检查每种类型一次有助于保持较低的性能。 如果您因为更新时间超过 60 秒而开始出现超时,您可以将 function's timeout 增加到最多 9 分钟,或者改为每天运行多次(后者是更好的选择)。只需将 0 23 * * * 更改为 0 5,11,17,23 * * * 即可每 6 小时运行一次。 太棒了!谢谢。出于好奇,您知道使用带有多个元素的arrayRemove 算作1 读写还是1xelement?【参考方案2】:

我假设在这种情况下 res 未定义并且评估为假。

在带有 res 参数的 .then 承诺之前,您有一个返回 void 的先前 .then 承诺:

//...
.then((snapshot) => 
    snapshot.docs.map((doc)=>return true) // <--- This is not actually returning a resolved value
).then(async (res) => 
    res===true ? null : 
    (await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
    .update('types', admin.firestore.FieldValue.arrayRemove(type)))
)
//...

根据您的意图,您需要在之前的承诺中返回一个值。看起来您正在创建一个布尔值数组,它是您拥有的 snapshot.docs 数量的长度,因此如果您在前面的 .then 子句中简单地放置一个 return 语句,res 将类似于, [true, true, true, true, /* ... */]

//...
.then((snapshot) => 
    return snapshot.docs.map((doc)=>return true)
).then(async (res) => 
    res===true ? null : 
    (await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
    .update('types', admin.firestore.FieldValue.arrayRemove(type)))
)
//...

【讨论】:

【参考方案3】:

snapshot.docs.map((doc)=&gt;return true) 返回像 [true, false] 这样的数组,而不是像 true 这样的布尔值。 所以.then( async (res) =&gt; res===true ? null : await admin.firestore(... 不能工作。 和

也许你应该像下面这样修改。

.then((snapshot) => 
    snapshot.docs.length > 0 ? null : 
        await admin.firestore(...

【讨论】:

以上是关于嵌套的 Javascript 承诺 - 从 Firestore 获取数据的主要内容,如果未能解决你的问题,请参考以下文章

承诺按顺序运行嵌套承诺并在第一次拒绝时解决

Google Cloud Functions - 警告避免嵌套承诺承诺/不嵌套

javascript中的嵌套MongoDB查询

ES6新特性之 promise

ES6新特性之 promise

处理嵌套承诺的最佳方法(蓝鸟)[重复]