Как правильно возвращать в модели список из сущностей, принадлежащих связанной модели?
Делаю небольшой проект на Fastapi (мой первый на нем проект), python 3.10, с использованием sqlalchemy, sqlmodel. Есть 2 связанные между собой сущности (сделал их для наглядности). 1 - магазин, 2 - товары. Товар привязывается к магазину (это условно, для примера). При получении данных о магазине хочу возвращать список товаров, которые там содержатся.
Update: добавил тестовый проект на Гитхаб для большей наглядности.
models
product.py
from sqlmodel import SQLModel, Field, Relationship
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from app.models.shop import Shop
class ProductBase(SQLModel):
"""Сущность: продукт (товар)"""
name: Optional[str] = None
shop_id: int = Field(default=None, foreign_key="shop.id")
class Product(ProductBase, table=True):
__tablename__ = "product"
id: Optional[int] = Field(default=None, primary_key=True, nullable=False)
shop: Optional["Shop"] = Relationship(back_populates="products")
class ProductRead(ProductBase):
id: int
class ProductCreate(ProductBase):
pass
shop.py
from sqlmodel import SQLModel, Field, Relationship
from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING:
from app.models.product import Product, ProductRead
class ShopBase(SQLModel):
"""Сущность: магазин, в котором содержатся продукты (товары)"""
name: Optional[str] = None
class Shop(ShopBase, table=True):
__tablename__ = "shop"
id: Optional[int] = Field(default=None, primary_key=True, nullable=False)
products: List["Product"] = Relationship(back_populates="shop")
class ShopRead(ShopBase):
id: int
class ShopGet(ShopRead):
products: List["ProductRead"] = []
is_main: bool = True
class ShopCreate(ShopBase):
pass
repositories
product.py
from typing import Optional
from sqlalchemy.future import select
from app.models.product import Product, ProductCreate
from app.repositories.base import BaseRepository
class ProductRepository(BaseRepository):
async def create(self, product: ProductCreate) -> Product:
db_product = Product.from_orm(product)
self.session.add(db_product)
await self.session.commit()
await self.session.refresh(db_product)
return db_product
async def get_by_name(self, name: str) -> Optional[Product]:
query = select(Product).where(Product.name == name)
result = await self.session.execute(query)
return result.scalar_one_or_none()
async def get_by_id(self, product_id: int) -> Optional[Product]:
result = await self.session.get(Product, int(product_id))
return result
shop.py
from typing import Optional
from sqlalchemy.future import select
from app.models.shop import Shop, ShopCreate, ShopGet
from app.repositories.base import BaseRepository
class ShopRepository(BaseRepository):
async def create(self, shop: ShopCreate) -> Shop:
db_shop = Shop.from_orm(shop)
self.session.add(db_shop)
await self.session.commit()
await self.session.refresh(db_shop)
return db_shop
async def get_by_name(self, name: str) -> Optional[Shop]:
query = select(Shop).where(Shop.name == name)
result = await self.session.execute(query)
return result.scalar_one_or_none()
async def get_by_id(self, shop_id: int) -> Optional[ShopGet]:
result = await self.session.get(Shop, int(shop_id))
return result
endpoints
shop.py
from fastapi import APIRouter, Depends, Query
from app.repositories.shop import ShopRepository
from app.models.shop import Shop, ShopCreate, ShopGet
from app.endpoints.depends import get_shop_repository
router = APIRouter()
@router.get("/get_by_name", response_model=ShopGet)
async def get_by_name(
name: str = Query(description="Название магазина"),
shop: ShopRepository = Depends(get_shop_repository)):
return await shop.get_by_name(name=name)
@router.get("/get_by_id", response_model=ShopGet)
async def get_by_id(
shop_id: int = Query(description="ID магазина"),
shop: ShopRepository = Depends(get_shop_repository)):
return await shop.get_by_id(shop_id=shop_id)
@router.post("/create", response_model=Shop)
async def create(
name: str = Query(description="Название магазина"),
shop: ShopRepository = Depends(get_shop_repository)):
return await shop.create(shop=ShopCreate(name=name))
коннект к базе данных
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
from app import settings
async_engine = create_async_engine(settings.db_async_connection_str, echo=True, future=True)
async def get_async_session() -> AsyncSession:
async_session = sessionmaker(
bind=async_engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
Использовал Relationship, информацию брал из документации
Первая проблема, с которой столкнулся, при разворачивании сваггера - ошибка
TypeError: issubclass() arg 1 must be a class
Выяснил, что это из-за такого импорта (использовал его по документации, чтобы не было конфликтов при взаимном импорте):
if TYPE_CHECKING:
from app.models.shop import Shop
Гуглением обнаружил, что уже с такой проблемой сталиквались, хотя ни одно из предложенных решений не помогло. При этом, если напрямую вызывать метод, все работает (т.е. падает только сваггер).
И вторая проблема (основная) - то что эндпоинт получение shop по id всегда возвращает пустой список products (хотя я их надобавлял несколько, привязанных именно к этому shop_id). Мне необходимо, чтобы в модели shop возвращался список из привязанных к нему products.
Аналогичный вопрос на английском