看一下 MongoDB 的 findAndModify
方法。
它几乎符合你的所有标准。
upsert
选项。findAndModify
的upsert参数文档: "为了避免多次插入,请确保查询字段拥有唯一索引。"此外,findAndModify
是上面提出的解决方案之一(“如果不存在,则插入”)。 我承认,这有点隐藏,哈哈。很可能,PyMongo的find_one_and_update
函数是使用findAndModify
实现的,因为它们具有相同的参数和返回类型。我对PyMongo最大的抱怨(也是唯一的一个)是它们缺乏坚持MongoDB API。 - John Crawford我最近遇到了这个问题,并使用了一些提示中提到的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"]
update
调用转换为带有upsert
功能的find
调用,成功地实现了在单个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"}
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
样式的查询以匹配子文档,然后使用客户端代码检查根并在没有匹配项时插入: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技能有所欠缺。
SHA-256
哈希。对于这个项目,我已经使用了xxHash
,因此选择对bson.json_util.dumps(myDoc)
输出进行xxHash
,其中myDoc
是我要上传的dict
、collections.OrderedDict
或bson.son.SON
对象。由于我正在使用Python进行鸭子类型等操作,使用json_util
可以给我后转换状态的SON文档,从而确保哈希生成在其他程序/语言中是平台无关的。请注意,哈希通常是有序相关的,因此使用像Python的dict
这样的无序结构将导致重复数据的不同哈希。如果用户向我提供一个dict
,我编写了一个简单的实用程序函数,递归地将dict
对象转换为使用Python的sorted
函数排序的bson.son.SON
对象。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"]