🏎

FastAPIでREST(その3)

2023/04/19に公開

前回まで

https://zenn.dev/robon/articles/bb17fd07739519

customerで1 schema = 1 modelの実装ができました。

仕上げ

productsの追加

customersと同様に実装できました。
schema単位のモジュール化も問題ありませんでしたし、こちらの方が凝集度が高く、拡張も容易な気がします。同じような設計思想で開発されているところもありました。

https://github.com/zhanymkanov/fastapi-best-practices

https://github.com/Netflix/dispatch

ordersの追加

注文は明細がOne to Manyの関係になりますので、SQLAlchemyのドキュメントを読みながら実装していきます。

src/orders/models.py
from datetime import date
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from src.database import Base


class OrderHeader(Base):
    __tablename__ = "order_header"

    orderId: Mapped[int] = mapped_column(
        name="order_id", primary_key=True)
    customerId: Mapped[int] = mapped_column(name="customer_id")
    orderDate: Mapped[date] = mapped_column(name="order_date")
    details: Mapped[list["OrderDetail"]] = relationship(
        cascade="all, delete-orphan", lazy="joined")

    def __repr__(self) -> str:
        return f"OrderHeader(orderId={self.orderId!r}, " \
            f"customerId={self.customerId!r}, orderDate={self.orderDate!r})"


class OrderDetail(Base):
    __tablename__ = "order_detail"

    orderId: Mapped[int] = mapped_column(
        ForeignKey("order_header.order_id"),
        name="order_id", primary_key=True)
    rowNumber: Mapped[int] = mapped_column(
        name="row_number", primary_key=True)
    productId: Mapped[int] = mapped_column(name="product_id")
    quantity: Mapped[int]
    pricePerUnit: Mapped[int] = mapped_column(name="price_per_unit")

    def __repr__(self) -> str:
        return f"OrderDetail(orderId={self.orderId!r}, " \
            f"rowNumber={self.rowNumber!r}, productId={self.productId!r})" \
            f"quantity={self.quantity!r}, pricePerUnit={self.pricePerUnit!r})"

schemaは、自然な延長で実装しましたが、参照関係のため、Detailが上になってます。

src/orders/schemas.py
from datetime import date
from pydantic import BaseModel


class OrderDetail(BaseModel):
    orderId: int | None
    rowNumber: int
    productId: int
    quantity: int
    pricePerUnit: int

    class Config:
        orm_mode = True


class OrderHeader(BaseModel):
    orderId: int
    customerId: int
    orderDate: date
    details: list[OrderDetail] = []

    class Config:
        orm_mode = True

serviceも自然な延長でできましたが、deleteやupdateの前処理のselectが無駄というか勿体ないというか気になるのは、ギリギリのコンピュータリソースで実装してきたロートルだからなんでしょうかね。

src/orders/service.py
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select

from . import models, schemas


async def get_order(db: AsyncSession, orderId: int) -> models.OrderHeader:
    result = await (db.execute(select(models.OrderHeader).filter(
        models.OrderHeader.orderId == orderId)))
    order = result.first()
    if order is None:
        raise HTTPException(status_code=404, detail="Customer not found")
    return order[0]


async def create_order(db: AsyncSession, order: schemas.OrderHeader):
    db_details = []
    for detail in order.details:
        db_details.append(models.OrderDetail(
            orderId=order.orderId,
            rowNumber=detail.rowNumber,
            productId=detail.productId,
            quantity=detail.quantity,
            pricePerUnit=detail.pricePerUnit,
        ))
    db_order = models.OrderHeader(
        orderId=order.orderId,
        customerId=order.customerId,
        orderDate=order.orderDate,
        details=db_details)
    db.add(db_order)
    await db.commit()
    await db.refresh(db_order)
    return db_order


async def update_order(
        db: AsyncSession, orderId: int, order: schemas.OrderHeader
):
    original = await get_order(db, orderId)
    original.customerId = order.customerId
    original.orderDate = order.orderDate
    original.details.clear()
    for detail in order.details:
        original.details.append(models.OrderDetail(
            orderId=orderId,
            rowNumber=detail.rowNumber,
            productId=detail.productId,
            quantity=detail.quantity,
            pricePerUnit=detail.pricePerUnit,
        ))
    db.add(original)
    await db.commit()
    await db.refresh(original)
    return original


async def delete_order(db: AsyncSession, orderId: int):
    original = await get_order(db, orderId)
    await db.delete(original)
    await db.commit()

ちなみにupdate_orderのログはこんな感じです。

2023-04-16 03:02:46,661 INFO sqlalchemy.engine.Engine ROLLBACK
2023-04-16 03:02:52,277 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-04-16 03:02:52,278 INFO sqlalchemy.engine.Engine SELECT order_header.order_id, order_header.customer_id, order_header.order_date, order_detail_1.order_id AS order_id_1, order_detail_1.row_number, order_detail_1.product_id, order_detail_1.quantity, order_detail_1.price_per_unit 
FROM order_header LEFT OUTER JOIN order_detail AS order_detail_1 ON order_header.order_id = order_detail_1.order_id 
WHERE order_header.order_id = $1::INTEGER
2023-04-16 03:02:52,278 INFO sqlalchemy.engine.Engine [cached since 5.624s ago] (1,)
2023-04-16 03:02:52,286 INFO sqlalchemy.engine.Engine UPDATE order_header SET customer_id=$1::INTEGER WHERE order_header.order_id = $2::INTEGER
2023-04-16 03:02:52,286 INFO sqlalchemy.engine.Engine [generated in 0.00047s] (2, 1)
2023-04-16 03:02:52,291 INFO sqlalchemy.engine.Engine UPDATE order_detail SET product_id=$1::INTEGER, quantity=$2::INTEGER, price_per_unit=$3::INTEGER WHERE order_detail.order_id = $4::INTEGER AND order_detail.row_number = $5::INTEGER
2023-04-16 03:02:52,291 INFO sqlalchemy.engine.Engine [generated in 0.00014s] (2, 3, 2000, 1, 1)
2023-04-16 03:02:52,296 INFO sqlalchemy.engine.Engine COMMIT
2023-04-16 03:02:52,300 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-04-16 03:02:52,300 INFO sqlalchemy.engine.Engine SELECT order_header.order_id, order_header.customer_id, order_header.order_date, order_detail_1.order_id AS order_id_1, order_detail_1.row_number, order_detail_1.product_id, order_detail_1.quantity, order_detail_1.price_per_unit 
FROM order_header LEFT OUTER JOIN order_detail AS order_detail_1 ON order_header.order_id = order_detail_1.order_id 
WHERE order_header.order_id = $1::INTEGER
2023-04-16 03:02:52,300 INFO sqlalchemy.engine.Engine [cached since 22.93s ago] (1,)
INFO:     127.0.0.1:50652 - "PUT /orders/1 HTTP/1.1" 200 OK

ということで、NestJSのTypeORMよりは、FastAPIのSQLAlchemyの方が使いやすかったような気がします。

最後に

今回のソースコードは、以下のリポジトリで公開しています。

https://github.com/take0a/fastapi-sample

GitHubで編集を提案
株式会社ROBONの技術ブログ

Discussion