数组中的Upsert和$ inc子文档

时间:2019-03-18 08:44:12

标签: node.js mongodb mongoose

以下架构仅用于记录特定日期的总观看次数和观看次数。

const usersSchema = new Schema({
    totalProductsViews: {type: Number, default: 0},

    productsViewsStatistics: [{
        day: {type: String, default: new Date().toISOString().slice(0, 10), unique: true},
        count: {type: Number, default: 0}
    }],
});

因此,今天的视图将存储在不同于昨天的另一个子文档中。为了实现这一点,我尝试使用upsert,以便在查看产品的每一天都会创建子文档,并且计数将基于特定的一天进行递增和记录。我尝试使用以下功能,但似乎无法达到我的预期目的。

usersSchema.statics.increaseProductsViews = async function (id) {
    //Based on day only.
    const todayDate = new Date().toISOString().slice(0, 10);

    const result = await this.findByIdAndUpdate(id, {
            $inc: {
                totalProductsViews: 1,
                'productsViewsStatistics.$[sub].count': 1
            },
        },
        {
            upsert: true,
            arrayFilters: [{'sub.day': todayDate}],
            new: true
        });
    console.log(result);
    return result;
};

要获得我想要的功能,我会错过什么?任何帮助将不胜感激。

1 个答案:

答案 0 :(得分:0)

实际上,您想要做的事情是要求您了解一些可能尚未掌握的概念。两个主要的是:

  • 您不能在位置更新中使用任何位置更新,因为它需要数据才能显示

  • 将项目添加到与“ upsert”混合的数组中通常是一个问题,您无法在单个语句中完成。

还是不清楚“ upsert”是否是您的实际意图,或者您只是假定这是为了使您的陈述生效而必须添加的内容。即使这是您的意图,它也会使事情复杂化,即使它不太可能给出finByIdAndUpdate()用法,这可能暗示,您实际上期望的是“文档” 始终存在。

无论如何,很明显您实际上希望“在找到时更新数组元素,或在找不到的地方插入新数组元素” 。实际上,这是一个两个写入过程,当您考虑“ upsert”情况时,也是一个三个过程。

为此,您实际上需要通过bulkWrite()调用语句:

usersSchema.statics.increaseProductsViews = async function (_id) {
  //Based on day only.
  const todayDate = new Date().toISOString().slice(0, 10);

  await this.bulkWrite([
    // Try to match an existing element and update it ( do NOT upsert )
    {
      "updateOne": {
        "filter": { _id, "productViewStatistics.day": todayDate },
        "update": {
          "$inc": {
            "totalProductsViews": 1,
            "productViewStatistics.$.count": 1
          }
        }
      }
    },

    // Try to $push where the element is not there but document is - ( do NOT upsert )
    {
      "updateOne": {
        "filter": { _id, "productViewStatistics.day": { "$ne": todayDate } },
        "update": {
          "$inc": { "totalProductViews": 1 },
          "$push": { "productViewStatistics": { "day": todayDate, "count": 1 } }
        }
      }
    },

    // Finally attempt upsert where the "document" was not there at all,
    // only if you actually mean it - so optional
    {
      "updateOne": {
        "filter": { _id },
        "update": {
          "$setOnInsert": {
            "totalProductViews": 1,
            "productViewStatistics": [{ "day": todayDate, "count": 1 }]
          }
        }
    }
  ])

  // return the modified document if you really must
  return this.findById(_id); // Not atomic, but the lesser of all evils
}

因此,这里有一个很好的理由说明positional filtered [<identifier>]运算符在这里不适用。主要原因是预期目的更新多个匹配的数组元素,而您只想更新一个。实际上,positional $运算符中有一个特定的运算符可以做到这一点。但是,必须满足以下条件:必须包含在查询谓词"filter"语句中的UpdateOne属性)中,如{的前两个语句所示{3}}。

因此,使用bulkWrite()的主要问题在于,正如前两个语句所示,您实际上不能在positional filtered [<identifier>]$inc之间切换,这取决于文档是否实际包含day的数组条目。当当前dayarrayFilters中的表达式不匹配时,只会发生充其量不会应用任何更新。

在最糟糕的情况下,由于MongoDB无法从语句中解密“路径名”,实际的“ upsert”将引发错误,当然,您根本无法{{3 }}作为“新”数组元素不存在的东西。 需要一个$push

这使您失去了不能语句中同时执行$inc$push的机制。 MongoDB会错误地指出您试图“修改相同路径” 作为非法操作。 $inc大致相同,因为该操作符适用于“ upsert”操作,但不排除其他操作不会发生。

因此,逻辑步骤可以归结为代码中的注释所描述的内容:

  1. 尝试匹配文档包含现有数组元素的位置,然后更新该元素。在这种情况下使用$push

  2. 尝试匹配文档所在的位置,但数组元素不存在,然后在给定的日期$setOnInsert中使用{默认count,并适当更新其他元素

  3. 如果您确实确实打算补充文档(而不是数组元素,因为上述步骤),然后最终实际尝试创建包括新数组的新属性的upsert。

最后是$inc的问题。尽管这是对服务器的单个请求,并带有单个响应,但实际上仍然是三个(如果需要的话,则为两个)操作。无法解决这个问题,它比使用$push甚至bulkWrite()发出链状的单独请求要好。

当然,从您尝试实现的代码角度来看,可操作的主要区别在于方法不会返回修改后的文档。根本无法从任何“批量”操作中获得“文档响应”。

这样,实际的“批量”流程将仅使用提交的三个陈述中的一个来修改文档,该陈述基于所给出的逻辑,最重要的是顺序陈述,这很重要。但是,如果您实际上想在修改后“退回文档” ,那么唯一的方法就是使用单独请求来获取文档。

这里唯一需要说明的是,由于读取和更新是分开的,因此除了“数组上载”之外,文档可能还会发生其他小的修改。真的没有办法解决,除非可能将三个单独的请求“链接”到服务器,然后确定哪个“响应文档” 实际应用了您想要实现的更新。

因此,在这种情况下,通常认为罪恶较少单独进行读取。这不是理想的选择,但它是一堆不好的东西中最好的选择。


作为最后的提示,我会强烈建议实际将day属性存储为BSON日期而不是字符串。实际上,它需要较少的字节来存储,并且以这种形式更加有用。因此,以下构造函数可能是最清晰,最少的hacky:

 const todayDate = new Date(new Date().setUTCHours(0,0,0,0))