下面的upstart语句不起作用,得到:
MongoServerError: E11000 duplicate key error collection: db.emails index: _id_ dup key: { _id: "8hh58975fw" }
我的目标是用相同的_id
编辑现有文档(或创建(如果存在))api中的代码是:
await db.collection('emails')
.updateOne(
{
_id: userId,
type: "profileCompletion",
time: new Date(),
},
{
$inc: { "count": 1},
},
{upsert: true},
)
但我从这个工作中应对了它:
await db.collection('analytics')
.updateOne(
{
_id: getDateMonYear(new Date(), "mmddyyyy"),
type: "Geo",
},
{
$inc: { "count": 1},
},
{upsert: true},
)
正如@Kal在回答中分享的那样,这个特定问题中的主要问题与time: new Date()
的使用有关。这个答案旨在帮助探索为什么会出现这种情况,帮助澄清在该答案中调整后的update
中并发如何仍然是一个问题,以及更安全的update
操作可能是什么样子。
Upsert修改
当作为upsert
的结果插入新文档时,有几种方法可以构建新文档。这一点目前记录在这里。与这种情况相关的是">设置";使用更新运算符的行为(由于更改被定义为{ $inc: { "count": 1}, }
)。相关段落:
如果没有文档与查询条件匹配,并且
<update>
参数是具有更新运算符表达式的文档,则该操作将根据<query>
参数中的相等子句创建一个基础文档,并应用<update>
参数中的表达式。
因此,给定123
的(新)userId
,1月1日执行的upsert
将查询集合,查找与以下内容匹配的文档:
{
_id: 123,
time: ISODate("2022-01-01..."),
type: 'profileCompletion'
}
如果没有,upsert
行为将触发,操作将向集合中插入类似于以下内容的文档:
{
_id: 123,
time: ISODate("2022-01-01..."),
type: 'profileCompletion',
count: 1
}
现在,我们继续讨论在操作的后续执行中会发生什么。
Upsert匹配
假设一个月后的2月初执行相同的操作(123
的userId
)
{
_id: 123,
time: ISODate("2022-02-01..."),
type: 'profileCompletion'
}
虽然具有给定_id
(和type
)的文档确实存在,但由于time
的差异,整个文档不匹配。与之前类似,这会触发upsert
行为,操作会尝试插入以下文档:
{
_id: 123,
time: ISODate("2022-02-01"),
type: 'profileCompletion',
count: 1
}
当然,由于包含123
的_id
值的现有文档而导致上述重复密钥错误,这会失败。换句话说,问题之一是每次执行此操作时时间戳都不同。
因此,从操作的<query>
部分删除time: new Date()
的更改同时做两件事:
- 它消除了最初插入的文档中的
time
字段 - 当匹配时,它随后删除
<query>
的该组件,从而导致现有文档按预期更新
并发性
当我在评论中声称您遇到的问题是并发的结果时,我(显然)是不正确的。更正确地说,我应该指出它可能是并发的结果。在更改为从操作中删除time
之后,这一情况仍然存在。
关于追加销售的文档包括以下注释:
如果大致同时发布多个相同的追加订单,则
update()
与upsert: true
一起使用可能会创建重复的文档。
然后链接到另一个以开头的部分
使用
update()
方法的upsert: true
选项,并且不在查询字段上使用唯一索引,在某些情况下,具有相似查询字段的update()
操作的多个实例可能会导致插入重复文档。
我最初指出该文档是";证明;并发性可能是一个问题。我相信@Kal最初正确地指出,本节末尾的以下文本暗示第二次操作应如预期的那样update
(增加了强调):
有了这个唯一的索引
update()
操作现在表现出以下行为:
- 只需执行一次
update()
操作即可成功插入新文档- 所有其他
update()
操作都将更新新插入的文档,增加分值
这对我来说是个新闻!我回到过去,看了看4.0版本的该部分的措辞(再次强调):
剩下的操作将是:
- 更新新插入的文档,或者
- 尝试插入重复项时失败。如果操作因重复索引键错误而失败,应用程序可以重试该操作,该操作将作为更新操作成功
对于版本4.2
,该文本已更改。经过一番挖掘,我发现了这个名为">重试update+upstart的谓词唯一索引冲突->尽可能插入";。问题描述表明,在4.2
的某些情况下,对这种行为进行了一些改进。
替代(更安全)重写
上述改进表明,服务器现在可以透明地重试在某些条件下遇到重复密钥异常的upsert
。该票证中有一个表列出了这些条件,但在我看来,它们就像是写入之间并发的结果,而不是错误的其他原因,包括这个特定问题中的time: new Date()
问题。
事实上,我们可以看到,即使是另一个答案中的修改操作,如果遇到重复密钥异常,也不会透明地重试。如果我理解正确的话,修改后的操作现在将具有<query
>CCD_ 41。这属于该表中描述的第6行,因为type
不是唯一索引定义的一部分。
在任何情况下,虽然修改后的操作不再在没有并发的情况下生成重复密钥异常,但您可能仍然希望重写该操作。插入文档时,您仍然可以向文档添加时间戳,但不应在操作的<query>
部分中指定时间戳。相反,它应该通过$set
或$setOnInsert
表示,作为所描述的更改的一部分,具体取决于操作执行update
时您想要的行为
大致如下:
await db.collection('emails')
.updateOne(
{
_id: userId
},
{
$setOnInsert:
{
type: "profileCompletion",
time: new Date()
},
$inc: { "count": 1},
},
{upsert: true},
)
感觉文档有点误导,所以我会在网站上发送一些反馈。
解决方案是删除:
time: new Date(),
来自查询:
await db.collection('emails')
.updateOne(
{
_id: userId,
type: "profileCompletion",
time: new Date(),
},
{
$inc: { "count": 1},
},
{upsert: true},
)
这确实是我的问题中工作问题和非工作问题之间的区别。
摘要:只要有唯一的索引,无论id是什么,都可以使用相同的_id进行Upsert。在我的案例中,_id
是派生的,并且是相同的,加上它是唯一的索引。
上面评论的用户发布的链接是有效的,但关于多线程等的评论与此问题无关