带有 OrderingList 的 sqlalchemy 对象 - Session.commit() 方法失败

sqlalchemy Object with OrderingList - Session.commit() method fails

具有 sqlalchemy 类型属性的模型 OrderingList 在数据库会话提交步骤中失败(因为数据库我使用的是 PostgreSQL 13):

from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from database.base_class import Base
from sqlalchemy.ext.orderinglist import ordering_list


class ContentType(Enum):
    SCENE_HEADING = 'scene_heading'
    ACTION = 'action'
    CHARACTER = 'character'
    PARENTHETICAL = 'parenthetical'
    DIALOGUE = 'dialogue'
    SHOT = 'shot'
    TRANSITION = 'transition'
    TEXT = 'text'


class EditorElement(Base):
    """
    Class that represents the Table of Editor Elements.
    """
    id = Column(Integer, primary_key=True, autoincrement=True, index=True)
    content = Column(String, nullable=True)
    content_type = Column(ContentType, nullable=True)

    screenplay_id = Column(Integer, ForeignKey('screenplays.id', ondelete="CASCADE"), nullable=False)

    screenplay = relationship("Screenplay", back_populates="elements")


class Screenplay(Base):
    """
    Class that represents the screenplay table in the database.
    """
    id = Column(Integer, primary_key=True, autoincrement=True, index=True)
    name = Column(String, index=True, nullable=False)
    elements = relationship("EditorElement", back_populates="screenplay", order_by="EditorElement.content_type",
                            collection_class=ordering_list('content_type'))

我正在使用 FastAPI 框架,在创建 Screenplay 的 CRUD 文件中,我有以下 class:

class CRUDScreenplay(CRUDBase[models.Screenplay, schemas.ScreenplayBase, schemas.ScreenplayBase]):

    @staticmethod
    def create(db: Session, *, obj_in: schemas.ScreenplayCreate) -> models.Screenplay:
        db_obj = models.Screenplay()
        db_obj.name = obj_in['name']

        for element in obj_in.get('elements'):
            e = models.EditorElement(
                content=element.content,
                content_type=element.content_type,
                screenplay_id=db_obj.id
            )
            db_obj.elements.append(e)

        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj

使用 PyCharm 调试器我能够检查错误发生的时间,它似乎出现在 Session.commit() 之后。这是commit()方法

之前的对象elements

这里是 commit() 方法之后的 elements 对象。

控制台报错:

INFO:     127.0.0.1:52649 - "POST /api/v1/screenplays/ HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\sql\sqltypes.py", line 1649, in _object_value_for_elem
    return self._object_lookup[elem]
KeyError: 'scene_heading'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 372, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\fastapi\applications.py", line 261, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\middleware\cors.py", line 84, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 21, in __call__
    raise e
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\routing.py", line 656, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\routing.py", line 259, in handle
    await self.app(scope, receive, send)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\routing.py", line 61, in app
    response = await func(request)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\fastapi\routing.py", line 235, in app
    response_data = await serialize_response(
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\fastapi\routing.py", line 130, in serialize_response
    value, errors_ = await run_in_threadpool(
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\starlette\concurrency.py", line 39, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\anyio\to_thread.py", line 28, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(func, *args, cancellable=cancellable,
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\anyio\_backends\_asyncio.py", line 818, in run_sync_in_worker_thread
    return await future
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\anyio\_backends\_asyncio.py", line 754, in run
    result = context.run(func, *args)
  File "pydantic\fields.py", line 854, in pydantic.fields.ModelField.validate
  File "pydantic\fields.py", line 1071, in pydantic.fields.ModelField._validate_singleton
  File "pydantic\fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
  File "pydantic\class_validators.py", line 313, in pydantic.class_validators._generic_validator_basic.lambda12
  File "pydantic\main.py", line 678, in pydantic.main.BaseModel.validate
  File "pydantic\main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic\main.py", line 1001, in pydantic.main.validate_model
  File "pydantic\utils.py", line 409, in pydantic.utils.GetterDict.get
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\orm\attributes.py", line 481, in __get__
    return self.impl.get(state, dict_)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\orm\attributes.py", line 941, in get
    value = self._fire_loader_callables(state, key, passive)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\orm\attributes.py", line 977, in _fire_loader_callables
    return self.callable_(state, passive)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\orm\strategies.py", line 911, in _load_for_state
    return self._emit_lazyload(
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\orm\strategies.py", line 1051, in _emit_lazyload
    result = result.unique().scalars().all()
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\engine\result.py", line 1371, in all
    return self._allrows()
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\engine\result.py", line 401, in _allrows
    rows = self._fetchall_impl()
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\engine\result.py", line 1284, in _fetchall_impl
    return self._real_result._fetchall_impl()
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\engine\result.py", line 1696, in _fetchall_impl
    return list(self.iterator)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\orm\loading.py", line 147, in chunks
    fetch = cursor._raw_all_rows()
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\engine\result.py", line 393, in _raw_all_rows
    return [make_row(row) for row in rows]
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\engine\result.py", line 393, in <listcomp>
    return [make_row(row) for row in rows]
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\sql\sqltypes.py", line 1768, in process
    value = self._object_value_for_elem(value)
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\sql\sqltypes.py", line 1651, in _object_value_for_elem
    util.raise_(
  File "C:\Users\userh\Playground\screenplay-writer\backend\.venv\lib\site-packages\sqlalchemy\util\compat.py", line 207, in raise_
    raise exception
LookupError: 'scene_heading' is not among the defined enum values. Enum name: None. Possible values: None

然而,数据已在数据库中成功创建,但由于此错误,FastAPI 没有return任何内容。

GitHub 中有一些相关问题,但我无法弄清楚那里发生了什么:

  1. Issue 1
  2. Issue 2
  3. Issue 3
  4. Issue 4

更新

Alembic(通常与 FastAPI 一起使用)或 SLQAlchemy 创建了大写的 PostgreSQL ENUM 类型。

我更改了 ContentType(enum.Enum) 的定义,因为它在官方文档 LINK 的示例中。感谢@fchancel 指出:

class ContentType(str, enum.Enum):
    HEADING = 'HEADING'
    ACTION = 'ACTION'
    CHARACTER = 'CHARACTER'
    PARENTHETICAL = 'PARENTHETICAL'
    DIALOGUE = 'DIALOGUE'
    SHOT = 'SHOT'
    TRANSITION = 'TRANSITION'
    TEXT = 'TEXT'

所以,现在我的代码可以工作了

似乎在创建元素时,content_type 部分有问题,因为它找不到 heading_scene。要理解这一点,我们需要了解您如何调用您的函数以及 obj_in 包含什么。

但是,错误的来源可能只是您的class ContentType(Enum),要正常工作应该是class ContentType(str, Enum)