You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI+SQLModel中User与Signatory关联报InvalidRequestError求助

问题:FastAPI+SQLModel多模型关联引发InvalidRequestError

使用FastAPI结合SQLModel开发项目,Signatory模型通过外键关联User模型,运行时触发SQLAlchemy的InvalidRequestError,提示无法解析Signatory类。

相关代码

user_model.py

from datetime import datetime
from typing import List, Optional

from sqlmodel import Field, SQLModel, Relationship
from app.models.signatory_model import Signatory
from app.models.document_model import Document


class UserBase(SQLModel):
    email: str = Field(index=True, unique=True)
    is_active: bool = True
    is_superuser: bool = False


class User(UserBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    first_name: str
    last_name: str
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: datetime = Field(default_factory=datetime.now)

    user_as_signatory: Optional["Signatory"] = Relationship(back_populates="user")
    signatories: List["Signatory"] = Relationship(back_populates="creator")
    documents: List["Document"] = Relationship(back_populates="owner")

signatory_model.py

from datetime import datetime
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import Enum
from sqlmodel import Field, Relationship, SQLModel

from app.models.document_model import Document, DocumentSignatoryLink

if TYPE_CHECKING:
    from .user_model import User
    

class SignatoryBase(SQLModel, table=True):
    id: int = Field(default=None, primary_key=True)
    first_name: str
    last_name: str
    email: str
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: datetime = Field(default_factory=datetime.now)


class Signatory(SignatoryBase):
    user_id: Optional[int] = Field(default=None, foreign_key="user.id")
    creator_id: int = Field(default=None, foreign_key="user.id")

    documents: List["Document"] = Relationship(
        back_populates="signatories",
        link_model=DocumentSignatoryLink,
    )
    user: Optional["User"] = Relationship(back_populates="user_as_signatory")
    creator: "User" = Relationship(back_populates="signatories")

报错信息

backend_1   | Traceback (most recent call last):
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 515, in _resolve_name
backend_1   |     rval = d[token]
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/util/_collections.py", line 344, in __missing__
backend_1   |     self[key] = val = self.creator(key)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 483, in _access_cls
backend_1   |     return self.fallback[key]
backend_1   | KeyError: 'Signatory'
backend_1   | 
backend_1   | The above exception was the direct cause of the following exception:
backend_1   | 
backend_1   | Traceback (most recent call last):
backend_1   |   File "/app/app/initial_data.py", line 23, in <module>
backend_1   |     main()
backend_1   |   File "/app/app/initial_data.py", line 18, in main
backend_1   |     init()
backend_1   |   File "/app/app/initial_data.py", line 13, in init
backend_1   |     init_db(session)
backend_1   |   File "/app/app/core/db.py", line 31, in init_db
backend_1   |     user = session.exec(user_query).first()
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlmodel/orm/session.py", line 68, in exec
backend_1   |     results = super().execute(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2306, in execute
backend_1   |     return self._execute_internal(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2191, in _execute_internal
backend_1   |     result: Result[Any] = compile_state_cls.orm_execute_statement(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
backend_1   |     result = conn.execute(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1421, in execute
backend_1   |     return meth(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 514, in _execute_on_connection
backend_1   |     return connection._execute_clauseelement(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1635, in _execute_clauseelement
backend_1   |     compiled_sql, extracted_params, cache_hit = elem._compile_w_cache(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 702, in _compile_w_cache
backend_1   |     compiled_sql = self._compiler(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 315, in _compiler
backend_1   |     return dialect.statement_compiler(dialect, self, **kw)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 1424, in __init__
backend_1   |     Compiled.__init__(self, dialect, statement, **kwargs)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 865, in __init__
backend_1   |     self.string = self.process(self.statement, **compile_kwargs)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 910, in process
backend_1   |     return obj._compiler_dispatch(self, **kwargs)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/visitors.py", line 141, in _compiler_dispatch
backend_1   |     return meth(self, **kw)  # type: ignore  # noqa: E501
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/compiler.py", line 4683, in visit_select
backend_1   |     compile_state = select_stmt._compile_state_factory(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/sql/base.py", line 684, in create_for_statement
backend_1   |     return klass.create_for_statement(statement, compiler, **kw)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 1091, in create_for_statement
backend_1   |     _QueryEntity.to_compile_state(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 2546, in to_compile_state
backend_1   |     _MapperEntity(
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 2626, in __init__
backend_1   |     entity._post_inspect
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 1252, in __get__
backend_1   |     obj.__dict__[self.__name__] = result = self.fget(obj)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 2711, in _post_inspect
backend_1   |     self._check_configure()
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 2388, in _check_configure
backend_1   |     _configure_registries({self.registry}, cascade=True)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 4203, in _configure_registries
backend_1   |     _do_configure_registries(registries, cascade)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 4244, in _do_configure_registries
backend_1   |     mapper._post_configure_properties()
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 2405, in _post_configure_properties
backend_1   |     prop.init()
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/interfaces.py", line 579, in init
backend_1   |     self.do_init()
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/relationships.py", line 1642, in do_init
backend_1   |     self._setup_entity()
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/relationships.py", line 1856, in _setup_entity
backend_1   |     self._clsregistry_resolve_name(argument)(),
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 519, in _resolve_name
backend_1   |     self._raise_for_name(name, err)
backend_1   |   File "/usr/local/lib/python3.10/site-packages/sqlalchemy/orm/clsregistry.py", line 500, in _raise_for_name
backend_1   |     raise exc.InvalidRequestError(
backend_1   | sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[User(user)], expression 'Signatory' failed to locate a name ('Signatory'). If this is a class name, consider adding this relationship() to the <class 'app.models.user_model.User'> class after both dependent classes have been defined.
esign_backend_1 exited with code 1

问题根源

  1. 循环导入:user_model.py直接导入Signatory类,导致加载User模型时,Signatory类尚未完成初始化,SQLAlchemy无法解析该类。
  2. 错误的表模型定义:SignatoryBase设置了table=True,导致SQLModel尝试为基类和子类分别创建表,引发模型解析混乱。
  3. 模型注册时机:应用启动时可能未确保所有关联模型被加载,导致SQLAlchemy元数据中缺少Signatory的注册信息。

修复方案

1. 修复循环导入

修改user_model.py的导入逻辑,仅在类型检查阶段导入关联模型,运行时使用字符串引用:

from datetime import datetime
from typing import List, Optional, TYPE_CHECKING

from sqlmodel import Field, SQLModel, Relationship

# 仅类型检查时导入,避免运行时循环依赖
if TYPE_CHECKING:
    from app.models.signatory_model import Signatory
    from app.models.document_model import Document


class UserBase(SQLModel):
    email: str = Field(index=True, unique=True)
    is_active: bool = True
    is_superuser: bool = False


class User(UserBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    first_name: str
    last_name: str
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: datetime = Field(default_factory=datetime.now)

    # 保持字符串引用不变
    user_as_signatory: Optional["Signatory"] = Relationship(back_populates="user")
    signatories: List["Signatory"] = Relationship(back_populates="creator")
    documents: List["Document"] = Relationship(back_populates="owner")

2. 修正表模型定义

将SignatoryBase改为非表基类,仅Signatory作为实际映射数据库的表模型:

from datetime import datetime
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import Enum
from sqlmodel import Field, Relationship, SQLModel

from app.models.document_model import Document, DocumentSignatoryLink

if TYPE_CHECKING:
    from .user_model import User
    

# 移除table=True,作为数据结构基类
class SignatoryBase(SQLModel):
    id: int = Field(default=None, primary_key=True)
    first_name: str
    last_name: str
    email: str
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: datetime = Field(default_factory=datetime.now)


# 子类设置table=True,作为实际表模型
class Signatory(SignatoryBase, table=True):
    user_id: Optional[int] = Field(default=None, foreign_key="user.id")
    creator_id: int = Field(default=None, foreign_key="user.id")

    documents: List["Document"] = Relationship(
        back_populates="signatories",
        link_model=DocumentSignatoryLink,
    )
    user: Optional["User"] = Relationship(back_populates="user_as_signatory")
    creator: "User" = Relationship(back_populates="signatories")

3. 确保模型全量加载

在FastAPI应用的入口文件(如main.py)中,显式导入所有模型类,确保SQLAlchemy元数据中注册所有模型:

from fastapi import FastAPI
from app.core.db import engine
from app.models import user_model, signatory_model, document_model  # 导入所有模型
from sqlmodel import SQLModel

# 创建数据库表(如果需要)
SQLModel.metadata.create_all(engine)

app = FastAPI()

# ... 其他路由和逻辑

修复说明

  • 循环导入修复:TYPE_CHECKING是Python的常量,仅在类型检查时为True,运行时不会执行该分支的导入,彻底避免循环依赖问题。
  • 表模型修正:基类仅用于共享字段定义,不应映射数据库表,子类作为实际表模型,确保SQLModel只创建一张signatory表。
  • 模型加载:显式导入所有模型可以确保SQLAlchemy在应用启动时将所有模型注册到元数据中,避免关联解析失败。

内容的提问来源于stack exchange,提问作者E.Mohammed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 13:57:03