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

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

问题描述

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

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

因此,今天的视图将存储在与昨天不同的另一个子文档中.为此,我尝试使用 upsert ,以便每天创建子文档查看产品时,计数将基于特定日期增加并记录.我尝试使用以下功能,但似乎无法达到我的预期目的.

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

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

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

还是不清楚"upsert"是否是您的实际意图,或者您只是假定这是为了使您的陈述生效而必须添加的内容.即使这是您的意图,它也会使事情复杂化,即使它不太可能始终存在.

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.

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

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.

为此,您实际上需要通过 bulkWrite() :

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
}

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

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.

因此,使用位置过滤的[<identifier>] <的主要问题是就像前两个语句所示,您实际上不能在 $push 取决于文档是否实际包含day的数组条目.当day中的表达式与当前day不匹配时,只会发生充其量不会应用任何更新.

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.

在最糟糕的情况下,由于MongoDB无法从语句中解密路径名",实际的"upsert"将引发错误,当然,您根本不能 $inc 作为新"数组元素不存在的东西. 需要的是 $push

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.

这使您失去了同时也不能执行 $inc $push 内的单个语句. MongoDB会错误地指出,您试图将修改相同路径" 视为非法操作.由于 $setOnInsert 与此类似, strong>仅适用于"upsert"操作,并不排除其他操作的发生.

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. 尝试匹配文档中包含现有数组元素的位置,然后更新该元素.在这种情况下,使用 $inc

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

尝试匹配文档所在的位置,但数组元素不存在,然后

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

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

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.

最后是 bulkWrite() 的问题.尽管这是对服务器的单个请求,并带有单个响应,但实际上仍然是三个(如果需要的话,则是两个)操作.没有解决办法,它比使用 findByIdAndUpdate() 甚至 updateOne() .

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.

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

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天全站免登陆