簡體   English   中英

是否可以選擇在mongodb中查找或插入

[英]is there an option to find-or-insert in 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選項,但相反,如果贊成現有文檔而不是新文檔

PS如果可能的話,我寧願不使用唯一索引

提前謝謝

我最近遇到過這個問題並使用了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"]

使用虛假的NOOP,我設法將update調用轉換為具有upsert功能的find調用,在單個MongoDB調用中成功實現了“insert if new”。 這大致轉換為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>的數據超集的<your doc> ,而不僅僅是完全匹配。 例如,考慮一個集合:

{"foo": "bar", "apples": "oranges"}

上面的代碼將集合中已有的一個文檔與上傳的以下任何文檔相匹配:

{"foo": "bar"}
{"apples": "oranges"}
{"foo": "bar", "apples", "oranges"}

因此,它不是真正的“插入新的”,因為它無法忽略超集文檔,但對於某些應用程序,這可能足夠好並且與蠻力方法相比將非常快。

如果子文檔不是完全匹配則插入

如果它足以匹配子文檔:

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 !!!! ),您創建文檔的次數比找到現有文檔的次數要多。 在我遇到的大多數“插入新的”情況中,更常見的事件是文檔已經存在,在這種情況下,您想要執行“先查找並插入,如果缺少”方法。 這導致了其他選擇。

訂單依賴查找和插入

執行$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

$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 ,因此在bson.json_util.dumps(myDoc)輸出上選擇了xxHash ,其中myDoc是我要上傳的dictcollections.OrderedDictbson.son.SON對象。 因為我在Python中使用duck-typing和所有爵士樂,所以使用json_util為我提供了SON文檔的轉換后狀態,從而確保哈希生成與平台無關,以防我想在另一個程序中生成這些哈希/語言。 請注意,哈希值通常依賴於順序,因此使用像Python的dict這樣的無序結構會導致重復數據的不同哈希值。 如果用戶給我一個dict ,我寫了一個簡單的實用函數,它遞歸地將dict對象轉換為bson.son.SON對象,其中的鍵通過Python的sorted函數sorted

一旦你有一個代表你的數據的哈希或其他唯一值,並在MongoDB中為該鍵創建了一個唯一索引 ,你就可以使用簡單的upsert方法來完成你的“insert if new”功能。

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剛剛支持根級別等價測試,那么這將更容易,但哈希方法是一個很好的選擇,並且可能提供最佳的整體速度。

看看MongoDB的findAndModify方法。

幾乎可以滿足您的所有標准。

  1. 在單個文檔中,它是原子的。
  2. 它有一個upsert選項。
  3. 默認情況下,它返回預先修改的文檔。
  4. 如果需要,也可以刪除文檔。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM