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 и параметрами и делает код прямолинейным.