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

时间:2014-03-17 12:50:17

标签: 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

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

P.S。如果可能,我不想使用unique indexes

提前感谢所有

2 个答案:

答案 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种方法,最后一种是我推荐的方法。

Upsert-Optimized查找和插入

您可以使用根检查扩展以前的方法,添加客户端逻辑以检查根文档,如果没有完全匹配则插入:

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) myDocdictcollections.OrderedDict,或我要上传的bson.son.SON对象。由于我在Python中使用鸭子类型和所有爵士乐,因此使用json_util为我提供了SON文档的转换后状态,从而确保哈希生成与平台无关,以防我想要用另一个程序/语言生成这些哈希。请注意,散列通常依赖于顺序,因此使用像Python dict这样的无序结构会导致重复数据的不同散列。如果用户给我一个dict,我写了一个简单的实用函数,它通过Python dictbson.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方法。

它可能符合几乎您的所有条件。

  1. 在单个文档中,它是原子的。
  2. 它有一个upsert选项。
  3. 默认情况下,它会返回预先修改的文档。
  4. 如果需要,也可以删除文档。