必须显式捕获duplicatekeyerror,因其在旧版pymongo中不被exception覆盖,且仅用except exception会掩盖网络中断、权限不足等真实异常;mongoengine中需捕获notuniqueerror。

直接捕获 DuplicateKeyError 是最稳妥的做法,别用 except Exception: 包裹,也别先查再插。
为什么不能只写 except Exception:
旧版 PyMongo(如 3.2.2)中 DuplicateKeyError 继承自 OperationFailure,并不总被 Exception 覆盖;即使新版能捕获,也会把网络中断、权限不足等真正异常一并吞掉,掩盖真实问题。
- 必须显式导入:
from pymongo.errors import DuplicateKeyError - 推荐组合捕获:
except (DuplicateKeyError, OperationFailure):,既精准又留有余地 - mongoengine 用户注意:它会把冲突转为
NotUniqueError,不是原生DuplicateKeyError
upsert=True 能替代 insert_one() 吗
能,但语义完全不同——它不抛异常,也不等于“安全插入”,而是“匹配则更新字段,不匹配则新建”。这适合“存在则补字段,不存在则建全量”的场景,但不适合靠异常来判断是否已存在。
-
update_one({"email": "a@b.com"}, {"$set": {"name": "Alice"}}, upsert=True):只改name,其他字段(如created_at)保留原值或为空 -
$setOnInsert可设默认值(如"created_at": datetime.now()),但它只在真正插入时生效,无法用于业务校验逻辑 - 并发下两个请求同时
upsert同一email,仍会有一个因唯一索引失败而报E11000,upsert不消除竞态,只改变错误触发点
如何从 DuplicateKeyError 提取冲突字段和值
e.details 里有原始线索,但需要手动解析才能映射到业务语义。直接打印 e.details 只能看到 {"keyPattern": {"email": 1}, "keyValue": {"email": "user@example.com"}} 这类结构,对前端或日志不友好。
- 单字段索引:提取
e.details["keyPattern"]的 key(如"email"),再查业务字典映射为“邮箱” - 复合索引(如
{"username": 1, "tenant_id": 1}):keyValue是完整对象,需整体提示“用户名与租户ID组合已存在”,不能拆开说“用户名重复” - 空值陷阱:MongoDB 默认忽略
null和缺失字段,多个文档该字段都为空时,不视为冲突——若业务要求“空也唯一”,得用部分索引或预处理
为什么加了唯一索引还插进重复数据
大概率是索引没生效,或者建在了允许空值的字段上。MongoDB 对 null 和缺失字段不做唯一性检查,这是最容易被忽略的设计事实。
- 确认索引真实存在:
db.users.getIndexes()查看unique: true是否生效,且key字段拼写与插入字段完全一致(区分大小写) - 检查数据现状:运行
db.runCommand({collMod: "users", index: {keyPattern: {"email": 1}, dryRun: true, unique: true}}),它会报出所有违反唯一性的文档 ID - 已有重复数据时,
createIndex会静默失败或报错,必须先清理再建索引,不能跳过验证
真正麻烦的不是怎么捕获异常,而是怎么让 keyPattern 和业务字段名对上号,以及怎么解释复合索引冲突——这两处不处理好,用户看到的永远是“数据库报错了”,而不是“这个邮箱已被注册”。











