数组中的 Upsert 和 $inc 子文档 [英] Upsert and $inc Sub-document in Array

查看:21
本文介绍了数组中的 Upsert 和 $inc 子文档的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下架构旨在记录总观看次数和仅特定一天的观看次数.

const usersSchema = new Schema({totalProductsViews: {type: Number, default: 0},产品视图统计:[{日:{类型:字符串,默认值:新日期().toISOString().slice(0, 10),唯一:真},计数:{类型:数字,默认值:0}}],});

所以今天的视图将存储在另一个不同于昨天的子文档中.为了实现这一点,我尝试使用 upsert 以便每天创建子文档查看产品时,计数将根据特定日期递增和记录.我尝试使用以下功能,但似乎没有按我预期的方式工作.

usersSchema.statics.increaseProductsViews = 异步函数 (id) {//仅基于日期.const todayDate = new Date().toISOString().slice(0, 10);const 结果 = 等待 this.findByIdAndUpdate(id, {$inc:{总产品浏览次数:1,'productsViewsStatistics.$[sub].count':1},},{upsert:真的,arrayFilters: [{'sub.day': todayDate}],新:真实});控制台日志(结果);返回结果;};

我错过了什么才能获得我想要的功能?任何帮助将不胜感激.

解决方案

您在这里尝试做的实际上需要您理解一些您可能尚未掌握的概念.两个主要的是:

  • 您不能将任何位置更新用作 upsert 的一部分,因为它需要存在数据

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

有点不清楚upsert"是否是您的实际意图,或者您是否只是假定这是您必须添加的内容才能使您的语句起作用.如果这是您的意图,它确实会使事情复杂化,即使不太可能finByIdAndUpdate() 用法,这暗示您实际上希望文档"始终存在.>

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

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

usersSchema.statics.increaseProductsViews = 异步函数 (_id) {//仅基于日期.const todayDate = new Date().toISOString().slice(0, 10);等待 this.bulkWrite([//尝试匹配现有元素并更新它(不要更新插入){更新一":{过滤器":{_id,productViewStatistics.day":今天日期},更新": {$inc":{"总产品浏览次数": 1,productViewStatistics.$.count":1}}}},//尝试在元素不存在但文档存在的地方 $push - (不要 upsert ){更新一":{过滤器":{_id,productViewStatistics.day":{$ne":今天日期}},更新": {"$inc": { "totalProductViews": 1 },"$push": { "productViewStatistics": { "day": todayDate, "count": 1 } }}}},//最后尝试在文档"根本不存在的地方进行 upsert,//只有当你真的是认真的 - 所以可选{更新一":{过滤器":{_id},更新": {$setOnInsert":{总产品视图":1,"productViewStatistics": [{ "day": todayDate, "count": 1 }]}}}])//如果确实需要,返回修改后的文档返回 this.findById(_id);//不是原子的,而是所有邪恶中较小的}

所以这里有一个很好的理由为什么 位置过滤[] 运算符在此处不适用.主要原因是预期目的更新多个匹配的数组元素,而您只想更新一个.这实际上在 positional $ 中有一个特定的运算符 运算符就是这样做的.但是,它的条件必须包含在查询谓词(UpdateOne 语句中的 "filter" 属性)中,正如演示的那样在 bulkWrite() 以上.

所以使用位置过滤的主要问题[$inc$push 取决于文档是否实际包含 day 的数组条目.所有将发生的事情是充其量当当前 dayarrayFilters 中的表达式不匹配时不会应用更新.

最坏的情况是实际的upsert"将抛出错误,因为 MongoDB 无法从语句中破译路径名",当然您根本无法$inc 不存在的东西新"数组元素.这需要一个$push.

这给你留下了你也不能同时执行$inc$push单个 语句中.MongoDB 会错误地将您尝试修改相同路径"视为非法操作.$setOnInsert 因为虽然该运算符适用于更新插入"操作,但它并不排除其他操作的发生.

因此逻辑步骤退回到代码中的注释也描述的内容:

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

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

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

最后是 bulkWrite() 的问题.虽然这是对服务器的单个请求和单个响应,但它仍然是有效的三个(或两个,如果您只需要这些)操作.没有办法解决这个问题,它比使用 findByIdAndUpdate 发出链接的单独请求更好() 甚至 updateOne().

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

因此,实际的批量"过程只会修改具有基于呈现的逻辑提交的三个语句中的一个的文档,最重要的是这些语句的顺序声明,这很重要.但是,如果您确实想在修改后返回文档",那么唯一的方法就是使用 单独的 请求来获取文档.

这里唯一的警告是很小的可能性,因为读取和更新是分开的,所以除了数组更新插入"之外,文档还可能发生了其他修改.真的没有办法解决这个问题,除非可能将三个单独的请求链接"到服务器,然后决定哪个响应文档"实际应用了您想要实现的更新.

因此,在这种情况下,通常认为单独阅读是弊大于利.这并不理想,但它是坏人中的最佳选择.

<小时>

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

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

The following schema is intended to record total views and views for a very specific day only.

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}
    }],
});

So today views will be stored in another subdocument different from yesterday. To implement this I tried to use upsert so as subdocument will be created each day when product is viewed and counts will be incremented and recorded based on a particular day. I tried to use the following function but seems not to work the way I intended.

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;
};

What do I miss to get the functionality I want? Any help will be appreciated.

解决方案

What you are trying to do here actually requires you to understand some concepts you may not have grasped yet. The two primary ones being:

  • You cannot use any positional update as part of an upsert since it requires data to be present

  • Adding items into arrays mixed with "upsert" is generally a problem that you cannot do in a single statement.

It's a little unclear if "upsert" is your actual intention anyway or if you just presumed that was what you had to add in order to get your statement to work. It does complicate things if that is your intent, even if it's unlikely give the finByIdAndUpdate() usage which would imply you were actually expecting the "document" to be always present.

At any rate, it's clear you actually expect to "Update the array element when found, OR insert a new array element where not found". This is actually a two write process, and three when you consider the "upsert" case as well.

For this, you actually need to invoke the statements via 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
}

So there's a real good reason here why the positional filtered [<identifier>] operator does not apply here. The main good reason is the intended purpose is to update multiple matching array elements, and you only ever want to update one. This actually has a specific operator in the positional $ operator which does exactly that. It's condition however must be included within the query predicate ( "filter" property in UpdateOne statements ) just as demonstrated in the first two statements of the bulkWrite() above.

So the main problems with using positional filtered [<identifier>] are that just as the first two statements show, you cannot actually alternate between the $inc or $push as would depend on if the document actually contained an array entry for the day. All that will happen is at best no update will be applied when the current day is not matched by the expression in arrayFilters.

The at worst case is an actual "upsert" will throw an error due to MongoDB not being able to decipher the "path name" from the statement, and of course you simply cannot $inc something that does not exist as a "new" array element. That needs a $push.

That leaves you with the mechanic that you also cannot do both the $inc and $push within a single statement. MongoDB will error that you are attempting to "modify the same path" as an illegal operation. Much the same applies to $setOnInsert since whilst that operator only applies to "upsert" operations, it does not preclude the other operations from happening.

Thus the logical steps fall back to what the comments in the code also describe:

  1. Attempt to match where the document contains an existing array element, then update that element. Using $inc in this case

  2. Attempt to match where the document exists but the array element is not present and then $push a new element for the given day with the default count, updating other elements appropriately

  3. IF you actually did intend to upsert documents ( not array elements, because that's the above steps ) then finally actually attempt an upsert creating new properties including a new array.

Finally there is the issue of the bulkWrite(). Whilst this is a single request to the server with a single response, it still is effectively three ( or two if that's all you need ) operations. There is no way around that and it is better than issuing chained separate requests using findByIdAndUpdate() or even updateOne().

Of course the main operational difference from the perspective of code you attempted to implement is that method does not return the modified document. There is no way to get a "document response" from any "Bulk" operation at all.

As such the actual "bulk" process will only ever modify a document with one of the three statements submitted based on the presented logic and most importantly the order of those statements, which is important. But if you actually wanted to "return the document" after modification then the only way to do that is with a separate request to fetch the document.

The only caveat here is that there is the small possibility that other modifications could have occurred to the document other than the "array upsert" since the read and update are separated. There really is no way around that, without possibly "chaining" three separate requests to the server and then deciding which "response document" actually applied the update you wanted to achieve.

So with that context it's generally considered the lesser of evils to do the read separately. It's not ideal, but it's the best option available from a bad bunch.


As a final note, I would strongly suggest actually storing the the day property as a BSON Date instead of as a string. It actually takes less bytes to store and is far more useful in that form. As such the following constructor is probably the clearest and least hacky:

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

这篇关于数组中的 Upsert 和 $inc 子文档的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆