You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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={}
    )

关键修复点说明

  1. 保留DictProxy的修改跟踪:在to_python方法中检测到父类返回DictProxy时,创建新的DictProxy包装Enum key的字典,确保文档能正确跟踪字典的修改操作。
  2. 完善Enum key的查找逻辑:添加了Mongo存储的code字符串找不到对应Enum的异常处理,抛出明确的验证错误。
  3. 覆盖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

火山引擎 最新活动