是否可以选择在 mongodb 中查找或插入

aki*_*iva 5 mongodb

我有一个 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
Run Code Online (Sandbox Code Playgroud)

这就像upsert选项,但是如果支持现有文档而不是新文档

PS如果可能的话,我宁愿不使用唯一索引

提前谢谢大家

Joh*_*ord 1

upsert我最近遇到了这个问题,并按照一些人的暗示使用了该标志。在确定我推荐的解决方案之前,我经历了多种方法,这是本答案中描述的最后一个选项。请原谅我使用 PyMongo 代码。希望翻译到您的项目中不会很困难。

首先,MongoDB 的文档upsert明确警告不要在没有唯一索引的情况下使用。看起来命令本身是使用标准的“查找/插入”方法实现的,并且不是原子的。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"]
Run Code Online (Sandbox Code Playgroud)

使用人造 NOOP,我成功地将调用转换updatefind具有upsert功能的调用,在单个 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.
})
Run Code Online (Sandbox Code Playgroud)

此代码的一个问题/功能是,它将匹配包含来自 的数据超集的文档<your doc>,而不仅仅是精确匹配。例如,考虑一个集合:

{"foo": "bar", "apples": "oranges"}
Run Code Online (Sandbox Code Playgroud)

上述代码会将集合中已有的一个文档与正在上传的以下任一文档进行匹配:

{"foo": "bar"}
{"apples": "oranges"}
{"foo": "bar", "apples", "oranges"}
Run Code Online (Sandbox Code Playgroud)

因此,它不是真正的“如果是新的则插入”,因为它无法忽略超集文档,但对于某些应用程序来说,这可能足够好,并且与强力方法相比会非常快。

如果子文档不完全匹配则插入

如果只匹配子文档就足够了:

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"]
Run Code Online (Sandbox Code Playgroud)

请注意,这$eq是依赖于顺序的,因此如果您正在处理不依赖于顺序的数据(例如Pythondict对象),则此方法将不起作用。

如果整个文档不完全匹配则插入

我可以想到四种方法,最后一种是我推荐的方法。

Upsert 优化的查找和插入

您可以通过根检查扩展以前的方法,添加客户端逻辑来检查根文档并在没有完全匹配的情况下插入:

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
Run Code Online (Sandbox Code Playgroud)

请注意,使用从结果中过滤已知字段find来减少数据使用并简化我们的等效性检查。我还加入了resp.matched_countfor 查询限制,这样我们就不会浪费时间查找我们知道尚不存在的文档。

请注意,此方法针对upsert(在单个插入函数中进行 2 个插入调用... yuk!!!!)进行了优化,您创建文档的频率高于查找现有文档的频率。在我遇到的大多数“如果新则插入”的情况下,更常见的事件是文档已经存在,在这种情况下您想要执行“首先查找并如果丢失则插入”方法。这导致了其他选择。

顺序相关查找和插入

执行$eq-style 查询来匹配子文档,然后使用客户端代码检查根目录,如果没有匹配则插入:

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
Run Code Online (Sandbox Code Playgroud)

同样$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
Run Code Online (Sandbox Code Playgroud)

可能有一种方法可以使用 JavaScript 在服务器端完成这一切。不幸的是,我的 JavaScript-fu 目前还缺乏。

哈希作为唯一索引(推荐)

让唯一索引要求适合您,按照另一个 SO 问题的答案中的建议,在文档信息的哈希上创建该索引: https: //stackoverflow.com/a/27993841/2201287。理想情况下,这个哈希值可以单独从数据生成,这样您就可以创建哈希值而无需与 MongoDB 交互。链接答案的作者SHA-256对 JSON 文档的字符串表示形式进行哈希处理。对于这个项目,我已经在使用xxHash,因此选择了xxHash输出bson.json_util.dumps(myDoc)上的myDocdictcollections.OrderedDictbson.son.SON想要上传的对象。由于我在 Python 中使用鸭子类型和所有这些爵士乐,使用json_util为我提供了 SON 文档的转换后状态,从而确保哈希生成与平台无关,以防我想在另一个程序中生成这些哈希/语言。请注意,哈希值通常与顺序相关,因此使用像 Python 这样的无序结构dict将导致重复数据产生不同的哈希值。如果用户递给我一个dict,我编写了一个简单的实用函数,它将dict对象递归地转换为bson.son.SON带有通过 Pythonsorted函数排序的键的对象。

一旦您拥有代表数据的哈希值或其他唯一值,并在 MongoDB 中为该键创建了唯一索引,您就可以使用简单的upsert方法来完成“如果新则插入”功能。

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"]
Run Code Online (Sandbox Code Playgroud)

所有数据库工作都在一个简短的命令中完成,并且索引速度非常快。困难的部分只是生成哈希值。

因此,您有多种可能适合您的特定情况的方法。当然,如果 MongoDB 刚刚支持根级等效性测试,这会容易得多,但哈希方法是一个很好的替代方案,并且可能提供最佳的整体速度。