1
/
5

FastAPIでREST(その3)

前回まで

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

仕上げ

productsの追加

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


GitHub - zhanymkanov/fastapi-best-practices: FastAPI Best Practices and Conventions we used at our startup
FastAPI Best Practices and Conventions we used at our startup - zhanymkanov/fastapi-best-practices
https://github.com/zhanymkanov/fastapi-best-practices


GitHub - Netflix/dispatch: All of the ad-hoc things you're doing to manage incidents today, done for you, and much more!
All of the ad-hoc things you're doing to manage incidents today, done for you, and much more! - Netflix/dispatch
https://github.com/Netflix/dispatch


ordersの追加

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


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が上になってます。


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が無駄というか勿体ないというか気になるのは、ギリギリのコンピュータリソースで実装してきたロートルだからなんでしょうかね。


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の方が使いやすかったような気がします。

最後に

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


GitHub - take0a/fastapi-sample: REST API Sample with FastAPI
REST API Sample with FastAPI. Contribute to take0a/fastapi-sample development by creating an account on GitHub.
https://github.com/take0a/fastapi-sample


Invitation from 株式会社ROBON
If this story triggered your interest, have a chat with the team?
株式会社ROBON's job postings

Weekly ranking

Show other rankings
Like 荒木 岳夫's Story
Let 荒木 岳夫's company know you're interested in their content