[英]is there an option to find-or-insert in mongodb
我最近遇到過這個問題並使用了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種方法,最后一種是我推薦的方法。
您可以使用根檢查擴展以前的方法,添加客戶端邏輯以檢查根文檔,如果沒有完全匹配則插入:
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
是我要上傳的dict
, collections.OrderedDict
或bson.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
方法。
它幾乎可以滿足您的所有標准。
upsert
選項。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.