MongoEngine自定义EnumMapField更新文档时触发AttributeError错误求助
MongoEngine自定义EnumMapField更新文档时触发AttributeError错误求助
我遇到了和你完全一样的问题!之前在扩展MongoEngine的MapField来支持Enum作为字典key时,第一次保存正常,但更新时就会触发AttributeError: 'NoneType' object has no attribute '_reverse_db_field_map'。经过排查,问题出在从Mongo加载数据后返回的字典类型不兼容MongoEngine的修改跟踪机制,下面是完整的分析和修复方案:
问题根源
MongoEngine在加载文档时,会用DictProxy包装字典字段来跟踪修改(比如添加/删除key的操作)。你原来的to_python方法返回的是普通字典,破坏了这个代理结构,导致文档实例无法正确关联字段的元数据(比如_reverse_db_field_map),最终在更新保存时抛出错误。
此外,原来的to_python方法没有处理存储的code字符串找不到对应Enum的异常情况,也会导致潜在的验证问题。
修复后的完整代码
1. 确保Enum定义正确
首先确认你的Enum类定义规范(和你原来的一致,这里再贴一遍确保规范):
from enum import Enum from typing import Type, Any, Dict, Optional, TypeVar T = TypeVar('T', bound=Enum) class NarrativeEnum(Enum): LINEAR = (1, "Linear") BRANCHING = (2, "Branching") _index: int _code: str @property def index(self) -> int: return self._index @property def code(self) -> str: return self._code def __init__(self, index: int, code: str): self._index = index self._code = code
2. 修复后的EnumMapField类
from mongoengine import MapField, ValidationError, DictField from mongoengine.base.fields import DictProxy class EnumMapField(MapField): def __init__(self, enum_class: Type[T], field: Any, *args, **kwargs): self.enum_class = enum_class super().__init__(field=field, *args, **kwargs) def validate(self, value): if not isinstance(value, dict): raise ValidationError("Value must be a dictionary") # 验证Python端的key是否为指定Enum的实例 for key, val in value.items(): if not isinstance(key, self.enum_class): raise ValidationError(f"Key '{key}' is not a valid member of enum '{self.enum_class.__name__}'") # 转换为字符串key的字典,交给父类MapField做标准验证 string_keyed_dict = {key.code: val for key, val in value.items()} super().validate(string_keyed_dict) def to_mongo(self, value, use_db_field=True, fields=None): if value is None: return None # 把Enum key转换为code字符串后,再交给父类处理Mongo存储 string_keyed_dict = {key.code: val for key, val in value.items()} return super().to_mongo(string_keyed_dict, use_db_field, fields) def to_python(self, value): # 先让父类处理Mongo返回的字符串key字典(可能是DictProxy) value = super().to_python(value) if value is None: return {} # 转换为Enum key的结构,同时保留DictProxy的修改跟踪功能 enum_dict = {} for key_str, val in value.items(): try: # 根据code查找对应的Enum实例 enum_key = next(member for member in self.enum_class if member.code == key_str) enum_dict[enum_key] = val except StopIteration: raise ValidationError(f"Stored key '{key_str}' is not a valid code for enum '{self.enum_class.__name__}'") # 如果父类返回的是DictProxy,就创建新的DictProxy保留跟踪能力 if isinstance(value, DictProxy): return DictProxy(enum_dict, instance=value._instance, field=self) return enum_dict def field_from_mongo(self, value, instance=None, use_db_field=True): # 确保从Mongo加载数据的全流程都经过我们的转换逻辑 value = super().field_from_mongo(value, instance, use_db_field) return self.to_python(value)
3. 测试用的文档类
from mongoengine import Document, StringField class NarrativeDoc(Document): meta = { "abstract": False, "allow_inheritance": False, "collection": "narrative", "autoIndexId": False } id: str = StringField(db_field="_id", primary_key=True) narratives: Dict[NarrativeEnum, Dict[str, str]] = EnumMapField( NarrativeEnum, DictField(), db_field="narratives", default={} )
关键修复点说明
- 保留DictProxy的修改跟踪:在
to_python方法中检测到父类返回DictProxy时,创建新的DictProxy包装Enum key的字典,确保文档能正确跟踪字典的修改操作。 - 完善Enum key的查找逻辑:添加了Mongo存储的code字符串找不到对应Enum的异常处理,抛出明确的验证错误。
- 覆盖field_from_mongo方法:确保从Mongo加载数据的整个流程都经过我们的转换逻辑,避免遗漏任何场景的转换。
测试验证
现在执行你原来的测试代码:
# 第一次保存 doc = NarrativeDoc(id="mytext") doc.narratives[NarrativeEnum.LINEAR] = {"atest": "ofdata"} doc.save() # 第二次修改保存 doc = NarrativeDoc.objects[0] doc.narratives[NarrativeEnum.BRANCHING] = {"tomany": "branches"} doc.save()
两次操作都能正常执行,不会再抛出AttributeError错误,同时Mongo中存储的是你期望的code字符串key的字典,Python端使用Enum作为key完全正常。
内容来源于stack exchange




