使用SQLAlchemy Versioning时无法定位Name的问题排查
我有一个已有的Flask-SQLAlchemy项目,尝试添加SQLAlchemy官方示例中的版本控制功能。定义了如下两个模型(已剔除无关代码):
class DynamicForm(Versioned, db.Model, BaseModel): name = db.Column(db.String(100), nullable=False, unique=True) fields = db.relationship('DynamicField', back_populates='form') class DynamicField(Versioned, db.Model, BaseModel): name = db.Column(db.String(100), nullable=False) form_id = db.Column(db.Integer(), db.ForeignKey('dynamic_form.id')) form = db.relationship('DynamicForm', back_populates='fields')
导入这些模型时出现如下错误:
sqlalchemy.exc.InvalidRequestError: When initializing mapper mapped class DynamicForm->dynamic_form, expression 'DynamicField' failed to locate a name ('DynamicField'). If this is a class name, consider adding this relationship() to the <class 'api.inventory.dynamic_form.models.DynamicForm'> class after both dependent classes have been defined.
不继承Versioned时这些模型可以正常工作,因此问题与_history_mapper有关(完整回溯见文末)。我找到一个临时解决方法:在db.relationship(...)中使用DynamicField前先声明它(比如用backref并先定义DynamicField),但这个方法存在两个问题:
- 已有项目需要大量重构
- 并非所有场景都能在声明关联前定义所有被引用类
请问该错误的原因是什么?
完整回溯信息
env/lib/python3.8/site-packages/sqlalchemy/orm/clsregistry.py:393: in _resolve_name rval = d[token] env/lib/python3.8/site-packages/sqlalchemy/util/_collections.py:746: in __missing__ self[key] = val = self.creator(key) env/lib/python3.8/site-packages/sqlalchemy/orm/clsregistry.py:372: in _access_cls return self.fallback[key] E KeyError: 'DynamicField' The above exception was the direct cause of the following exception: tests/unit_tests/test_versioning.py:4: in <module> from api.inventory.base import BaseModel api/inventory/__init__.py:2: in <module> from .dynamic_form import * api/inventory/dynamic_form/__init__.py:1: in <module> from .models import * api/inventory/dynamic_form/models.py:18: in <module> class DynamicForm(Versioned, db.Model, BaseModel): env/lib/python3.8/site-packages/flask_sqlalchemy/model.py:67: in __init__ super(NameMetaMixin, cls).__init__(name, bases, d) env/lib/python3.8/site-packages/flask_sqlalchemy/model.py:121: in __init__ super(BindMetaMixin, cls).__init__(name, bases, d) env/lib/python3.8/site-packages/sqlalchemy/orm/decl_api.py:72: in __init__ _as_declarative(reg, cls, dict_) env/lib/python3.8/site-packages/sqlalchemy/orm/decl_base.py:126: in _as_declarative return _MapperConfig.setup_mapping(registry, cls, dict_, None, {}) env/lib/python3.8/site-packages/sqlalchemy/orm/decl_base.py:177: in setup_mapping return cfg_cls(registry, cls_, dict_, table, mapper_kw) env/lib/python3.8/site-packages/sqlalchemy/orm/decl_base.py:326: in __init__ self._early_mapping(mapper_kw) env/lib/python3.8/site-packages/sqlalchemy/orm/decl_base.py:209: in _early_mapping self.map(mapper_kw) env/lib/python3.8/site-packages/sqlalchemy/orm/decl_base.py:1004: in map mapper_cls(self.cls, self.local_table, **self.mapper_args), api/versioned_history/history_meta.py:184: in map_ _history_mapper(mp) api/versioned_history/history_meta.py:37: in _history_mapper for prop in local_mapper.iterate_properties: env/lib/python3.8/site-packages/sqlalchemy/orm/mapper.py:2031: in iterate_properties self._check_configure() env/lib/python3.8/site-packages/sqlalchemy/orm/mapper.py:1900: in _check_configure _configure_registries({self.registry}, cascade=True) env/lib/python3.8/site-packages/sqlalchemy/orm/mapper.py:3418: in _configure_registries _do_configure_registries(registries, cascade) env/lib/python3.8/site-packages/sqlalchemy/orm/mapper.py:3457: in _do_configure_registries mapper._post_configure_properties() env/lib/python3.8/site-packages/sqlalchemy/orm/mapper.py:1917: in _post_configure_properties prop.init() env/lib/python3.8/site-packages/sqlalchemy/orm/interfaces.py:231: in init self.do_init() env/lib/python3.8/site-packages/sqlalchemy/orm/relationships.py:2150: in do_init self._process_dependent_arguments() env/lib/python3.8/site-packages/sqlalchemy/orm/relationships.py:2245: in _process_dependent_arguments self.target = self.entity.persist_selectable env/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py:1113: in __get__ obj.__dict__[self.__name__] = result = self.fget(obj) env/lib/python3.8/site-packages/sqlalchemy/orm/relationships.py:2112: in entity argument = self._clsregistry_resolve_name(self.argument)() env/lib/python3.8/site-packages/sqlalchemy/orm/clsregistry.py:397: in _resolve_name self._raise_for_name(name, err) env/lib/python3.8/site-packages/sqlalchemy/orm/clsregistry.py:375: in _raise_for_name util.raise_( env/lib/python3.8/site-packages/sqlalchemy/util/compat.py:207: in raise_ raise exception E sqlalchemy.exc.InvalidRequestError: When initializing mapper mapped class DynamicForm->dynamic_form, expression 'DynamicField' failed to locate a name ('DynamicField'). If this is a class name, consider adding this relationship() to the <class 'api.inventory.dynamic_form.models.DynamicForm'> class after both dependent classes have been defined.
错误原因解析
正常情况下,SQLAlchemy会延迟解析关系中用字符串指定的类名(比如'DynamicField'),直到所有模型类都完成定义后,才会统一处理关系的初始化。
但你使用的Versioned版本控制示例中,_history_mapper函数在模型类初始化的阶段,就主动访问了local_mapper.iterate_properties,这个操作会强制触发SQLAlchemy对当前模型mapper的立即配置。当处理DynamicForm的fields关系时,此时DynamicField类还未被定义(因为它在DynamicForm之后声明),SQLAlchemy无法找到对应的类,因此抛出了找不到DynamicField的错误。
从回溯信息也能验证这一点:在DynamicForm类初始化过程中,Versioned的map_方法调用了_history_mapper,遍历属性时触发了mapper的配置流程,进而触发了关系的初始化,而此时依赖的DynamicField还未完成定义,最终导致解析失败。
内容的提问来源于stack exchange,提问作者noslenkwah

