我有一个mongodb文档,我希望只在不存在的情况下添加到集合中,但不要更改现有文档。
换句话说,我正在寻找一种原子方式:1. find if a document exists (based on a given key criteria)
2. if it exists:
2.1 return it
otherwise:
2.1 add a new one
这就像upsert选项,但是如果赞成现有文档而不是新文档
P.S。如果可能,我不想使用unique indexes
提前感谢所有
答案 0 :(得分:1)
我最近遇到过这个问题并使用了upsert
标志,正如一些人所暗示的那样。在确定了我推荐的解决方案之前,我经历了多种方法,这是本答案中描述的最后一个选项。请原谅我使用PyMongo代码。希望它很难转化为您的项目。
首先,MongoDB's documentation明确警告不要使用upsert
而没有唯一索引。似乎命令本身是使用标准" find / insert"来实现的。方法并不是原子的。 2个并发客户端可能会失败,但每个客户端都会插入自己的文档副本。如果没有唯一的索引来强制执行重复项,MongoDB就会允许这样的事件发生!在实施解决方案时请记住这一点。
from pymongo import ReturnDocument
objID = db.collection.find_one_and_update(
myDoc,
{"$unset": {"<<<IHopeThisIsNeverInTheDB>>>": ""}}, #There is no NOOP...
{}, #We only want the "_id".
return_document=ReturnDocument.AFTER, #IIRC an upsert would return a null without this.
upsert=True,
)["_id"]
使用虚假的NOOP,我设法将update
来电转换为find
来自upsert
的{{1}}来电,成功实施&#34;插入新的&#34;在一个MongoDB调用中。这大致转换为MongoDB客户端操作:
db.collection.findAndModify({
query: <your doc>,
update: {$unset: {"<<<IHopeThisIsNeverInTheDatabase>>>": ""}}, // There is no NOOP...
new: true, // IIRC an upsert would return a null without this.
fields: {}, // Only want the ObjectId
upsert: true, // Create if no matches.
})
此代码的问题/功能是它将匹配包含<your doc>
数据超集的文档,而不仅仅是完全匹配。例如,考虑一个集合:
{"foo": "bar", "apples": "oranges"}
上述代码会将集合中已有的一个文档与上传的以下任何文档相匹配:
{"foo": "bar"}
{"apples": "oranges"}
{"foo": "bar", "apples", "oranges"}
因此,如果是新的&#34;那么它就不是真的&#34;因为它无法忽略超集文档,但对于某些应用程序而言,这可能足够好,并且与蛮力方法相比会非常快。
如果它足以匹配子文档:
q = {k: {"$eq": v} for k, v in myDoc.items()} #Insert "$eq" operator on root's subdocuments to require exact matches.
objID = db.collection.find_one_and_update(
q,
{"$unset": {"<<<IHopeThisIsNeverInTheDB>>>": ""}}, #There is no NOOP...
{}, #We only want the "_id".
return_document=ReturnDocument.AFTER, #IIRC an upsert would return a null without this.
upsert=True,
)["_id"]
请注意,$eq
依赖于顺序,因此如果您正在处理非依赖于顺序的数据(例如Python dict
对象),则此方法将无效。
我可以为此考虑4种方法,最后一种是我推荐的方法。
您可以使用根检查扩展以前的方法,添加客户端逻辑以检查根文档,如果没有完全匹配则插入:
q = {k: {"$eq": v} for k, v in myDoc.items()} #Insert "$eq" operator on root's subdocuments to require exact matches.
resp = collection.update_many(
q,
{"$unset": {"<<<IHopeThisIsNeverInTheDB>>>": ""}}, #There is no NOOP...
True,
)
objID = resp.upserted_id
if objID is None:
#No upsert occurred. If you must, use a find to get the direct match:
docs = collection.find(q, {k: 0 for k in myDoc.keys()}, limit=resp.matched_count)
for doc in docs:
if len(doc) == 1: #Only match documents that have the "_id" field and nothing else.
objID = doc["_id"]
break
else: #No direct matches were found.
objID = collection.insert_one(myDoc, {}).inserted_id
请注意,使用find
的结果过滤已知字段可以减少数据使用并简化我们的等效性检查。我还在resp.matched_count
中查询了查询限制,因此我们不会浪费时间查找我们不知道已经存在的文档。
请注意,此方法针对upsert
进行了优化(在单个插入函数中插入2个调用... yuk !!!! ),您可以更频繁地创建文档比你找到现有的。在大多数&#34;插入新的&#34;我遇到的情况,更常见的事件是文件已经存在,在这种情况下你想要做一个&#34;找到第一个&amp;插入如果缺少&#34;做法。这导致了其他选择。
执行$eq
- 样式查询以匹配子文档,然后使用客户端代码检查根,如果没有匹配则插入:
q = {k: {"$eq": v} for k, v in myDoc.items()} #Insert "$eq" operator on root's subdocuments to require exact matches.
docs = collection.find(q, {k: 0 for k in myDoc.keys()}) #Filter known fields so we isolate the mismatches.
for doc in docs:
if len(doc) == 1: #Only match documents that have the "_id" field and nothing else.
objID = doc["_id"]
break
else: #No direct matches were found.
objID = collection.insert_one(myDoc, {}).inserted_id
再次$eq
依赖于顺序,这可能会导致问题,具体取决于您的情况。
如果您想要与订单无关,则可以通过简单地展平JSON文档来构建查询。这会在地图树中使用重复的父项来查询您的查询,但这可能会有所不同,具体取决于您的用例。
myDoc = {"llama": {"duck": "cake", "ate": "rake"}}
q = {"llama.duck": "cake", "llama.ate": "rake"}
docs = collection.find(q, {k: 0 for k in q.keys()}) #Filter known fields so we isolate the mismatches.
for doc in docs:
if len(doc) == 1: #Only match documents that have the "_id" field and nothing else.
objID = doc["_id"]
break
else: #No direct matches were found.
objID = collection.insert_one(myDoc, {}).inserted_id
可能有一种方法可以使用JavaScript在所有服务器端执行此操作。不幸的是,我的JavaScript-fu目前还缺乏。
使唯一索引要求适合您,在此答案中为另一个SO问题建议的文档信息的哈希值上创建该索引:https://stackoverflow.com/a/27993841/2201287。理想情况下,此哈希可以仅从数据生成,允许您创建哈希而无需与MongoDB通信。链接答案的作者对JSON文档的字符串表示形式进行了SHA-256
哈希。对于此项目,我已使用xxHash
因此在xxHash
输出中选择了bson.json_util.dumps(myDoc)
myDoc
为dict
,collections.OrderedDict
,或我要上传的bson.son.SON
对象。由于我在Python中使用鸭子类型和所有爵士乐,因此使用json_util
为我提供了SON文档的转换后状态,从而确保哈希生成与平台无关,以防我想要用另一个程序/语言生成这些哈希。请注意,散列通常依赖于顺序,因此使用像Python dict
这样的无序结构会导致重复数据的不同散列。如果用户给我一个dict
,我写了一个简单的实用函数,它通过Python dict
将bson.son.SON
个对象递归转换为sorted
个对象。功能
一旦您拥有代表您的数据的哈希值或其他唯一值have created a unique index in MongoDB for that key,您就可以使用简单的upsert
方法完成您的&#34;如果新的&#34;功能
from pymongo import ReturnDocument
myDoc["xxHash"] = xxHashValue #32-bit signed integer generated from xxHash of "bson.json_util.dumps(myDoc)"
objID = db.collection.find_one_and_update(
myDoc,
{"$unset": {"<<<IHopeThisIsNeverInTheDB>>>": ""}}, #There is no NOOP...
{}, #We only want the "_id".
return_document=ReturnDocument.AFTER, #IIRC an upsert would return a null without this.
upsert=True,
)["_id"]
所有数据库工作都在一个简短的命令中进行,并且在索引方面非常快。困难的部分就是生成哈希值。
因此,您可以采用多种方法来满足您的特定情况。当然,如果MongoDB刚刚支持根级别等价测试,那么这将更容易,但哈希方法是一个很好的选择,并且可能提供最佳的整体速度。
答案 1 :(得分:1)
查看MongoDB的findAndModify
方法。
它可能符合几乎您的所有条件。
upsert
选项。