2025, Sep 23 04:01

Ссылки Beanie в FastAPI: не создавайте Link вручную

Почему в Beanie и FastAPI нельзя вручную создавать Link и как правильно связывать документы. Минимальный пример и решение с автоконвертацией в DBRef.

Ссылки Beanie в FastAPI: почему ручной Link(...) не работает и что делать вместо этого

При связывании документов в Beanie легко поддаться искушению и создать объекты Link вручную. Итог — цепочка непонятных ошибок типов. Решение куда проще, чем кажется: добавляйте сам документ, а не обёртку Link. Ниже — минимальный пример, который воспроизводит проблему и показывает корректный способ задать связь.

Постановка задачи: документы Beanie и маршрут FastAPI

Модели описывают два документа Beanie. Один ссылается на другой через набор ссылок. Точка входа API пытается прикрепить элемент-ссылку.

# models.py
from beanie import Document, Link

class Alpha(Document):
    prime: int
    note: str

class Beta(Document):
    score: float
    alpha_refs: set[Link[Alpha]] = set()
# main.py
from fastapi import FastAPI, HTTPException, status
from beanie import PydanticObjectId, Link
from .models import Alpha, Beta

app = FastAPI()

@app.post("/beta/{beta_id}/attach/{alpha_id}")
async def attach_alpha(beta_id: PydanticObjectId, alpha_id: PydanticObjectId):
    beta_obj = await Beta.get(beta_id)
    if not beta_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

    alpha_obj = await Alpha.get(alpha_id)
    if not alpha_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

    # попытка, которая вызывает ошибки
    beta_obj.alpha_refs.add(Link(alpha_obj))
    await beta_obj.save()
    return beta_obj

Ручная сборка Link приводит к сбоям. Первая попытка ругается на отсутствующий параметр.

Параметр 'document_class' не заполнен

Заполнение параметра вызывает новую ошибку типов.

Ожидался тип 'DBRef', вместо этого получен 'Alpha'

Переход к передаче id всё равно не соответствует ожидаемому типу.

Ожидался тип 'DBRef', вместо этого получен 'PydanticObjectId | None'

Что происходит на самом деле

Проблема в попытке вручную создать объекты Link. Хотя поле аннотировано как set[Link[Alpha]], самостоятельно инстанцировать Link не нужно. Beanie принимает добавленный «сырой» документ и при сохранении преобразует его во внутренний DBRef. Когда вы подсовываете собственный Link, это нарушает контракт и приводит к несоответствиям типов и параметров, показанным выше.

Решение: поручите Beanie преобразование ссылок

Добавьте сам объект документа в поле ссылок и сохраните. Beanie выполнит преобразование в DBRef при записи.

# main.py (исправленная ключевая строка)
from fastapi import FastAPI, HTTPException, status
from beanie import PydanticObjectId
from .models import Alpha, Beta

app = FastAPI()

@app.post("/beta/{beta_id}/attach/{alpha_id}")
async def attach_alpha(beta_id: PydanticObjectId, alpha_id: PydanticObjectId):
    beta_obj = await Beta.get(beta_id)
    if not beta_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

    alpha_obj = await Alpha.get(alpha_id)
    if not alpha_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

    beta_obj.alpha_refs.add(alpha_obj)
    await beta_obj.save()
    return beta_obj

Этого небольшого изменения достаточно, чтобы избавиться от всех промежуточных проблем с типами: ODM сделает правильное преобразование во время сохранения.

Почему это важно

Понимание того, как Beanie работает с ссылками, делает слой данных предсказуемым, а обработчики API — лаконичными. Неверное использование Link порождает загадочные сообщения о DBRef и document_class, отвлекая от сути. Доверяя встроенному преобразованию Beanie, вы сохраняете ясность намерений модели и снижаете риск несовпадений типов.

Итоги

Связывая документы Beanie в обработчиках FastAPI, не конструируйте Link вручную. Добавляйте в поле ссылок экземпляр документа и сохраняйте родительский объект. Это соответствует задумке Beanie, избавляет от ошибок с DBRef и параметрами и делает код прямолинейным.

Статья основана на вопросе на StackOverflow от Ambitions и ответе пользователя Pokemon.