full-stack-fastapi-templateの構成調査
最終的には1つの記事にまとめる予定
full-stack-fastapi-template の0.7.1時点での利用方法やFastAPI(バックエンド)周りで利用されている技術と構成全体についてを解明しながら、学習する目的のスクラップになります。
学習対象としては、以下を中心に実施予定です。
- full-stack-fastapi-template の構成解析
- Alembic
- traefik
我流からの脱却を目標にしています。
Alembicでモデルを追加したときのトラブル
Docker Compose 初回起動時にリビジョン追加によるエラーが発生する問題と解決策
問題の概要
Docker Compose で構築されたコンテナを初回起動する際に、新しく追加された Alembic のリビジョンがマウントされていないため、エラーが発生する事象があります。
Dockerコンテナに関連するマウントの問題です。
問題の発生時系列
-
docker compose up
でコンテナ環境を構築。 - 構築後に
models.py
を変更。 - Alembic を使ってリビジョンファイルを作成:
alembic revision --autogenerate -m "create tables"
- リビジョンファイルを作成した状態で、Docker コンテナを
docker compose stop
で停止した後、再起動を行う。 -
docker compose up
を実行しようとすると、Docker コンテナ内にリビジョンファイルが存在しないために、Alembic の実行時にエラーが発生します。
問題の原因
- Alembic のリビジョン状態が DB テーブル上に保存されているため、Docker コンテナ内にリビジョンファイルが存在しないと、実行不能になります。
- Docker Compose の構成上、
backend
ディレクトリがマウントされていない為、ローカルで作成したリビジョンファイルがコンテナ内に送られていない。
解決策
-
リビジョンファイル作成:
ローカル環境で新しく Alembic リビジョンファイルを作成する。 -
Docker コンテナ内のリビジョン状態をリセット:
docker compose down --volumes
これにより DB テーブル上の Alembic リビジョン情報をリセット。
-
Docker コンテナを再構築:
ローカルの最新リビジョンを Docker コンテナに反映するために、キャッシュをクリアして構築:docker compose build --no-cache docker compose up
-
Docker Compose Watch の利用:
Docker Compose Watch を搭載して、ローカルの変更が Docker コンテナに反映されるように設定する。
確認作業
少し雑ですが、概要のみ記載します。
-
backend/app/alembic/versions
内容確認:
リビジョンファイルのリストを確認:- 1a31ce608336_add_cascade_delete_relationships.py - 9c0a54914c78_add_max_length_for_string_varchar_.py - d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py - e2412789c190_initialize_models.py
-
prestart
ログの確認:docker compose logs prestart
ログの内容から Alembic の実行状況を確認。
-
DB 内容の確認:
Adminer や psql を使用し、alembic_version
テーブルの状態を確認。
alembic_versionが存在するリビジョンファイルのリストと一致している場合は、正常に起動するはずです。
リビジョンファイルの追加手順
全体を通した注意点ですが、docker compose watch
でコンテナを起動した場合は、ローカルでのファイル変更をコンテナ上に反映することが出来ません。(バインドマウントはしていないです。)
コンテナの仕組みについては、イマイチわからない...という方は、ローカルで開発した作業内容を反映するためには、docker compose watch
でコンテナを起動した状態でソースコードの変更をする。 と覚えておいてください。
とりあえず、ローカル環境とコンテナ環境がずれないように注意してください。ということです。
実際には、ファイル変更後にdocker compose watch
を実行しても差分を判断してコンテナに反映してくれるようです。ただ、ローカル環境下で、alembic upgrade head
をした場合、コンテナには最新のリビジョンファイルがないのに、DB上のrevision_versionは最新(コンテナに存在しないリビジョン)状態になります。そのため、docker compose up
に失敗します。慣れていないとトラブルシューティングに躓くと思います。
1. プロジェクト構成変更内容
まずは、docker compose watch
でコンテナを立ち上げてください。ローカルで開発している内容をコンテナに反映する必要があります。
以下の変更を行い、Alembic を利用してマイグレーションを作成し適用します。
Order
テーブル
2. モデルの追加:Order
クラスを追加し、既存の Item
クラスを外部キーで参照するリレーションシップを構築します。
models.py
)
2.1. 変更後のモデルコード(import uuid
from sqlmodel import Field, Relationship, SQLModel
class Item(ItemBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
title: str = Field(max_length=255)
owner_id: uuid.UUID = Field(
foreign_key="user.id", nullable=False, ondelete="CASCADE"
)
owner: User | None = Relationship(back_populates="items")
orders: list["Order"] = Relationship(back_populates="item")
# ここを追加
class Order(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
item_id: uuid.UUID = Field(foreign_key="item.id", nullable=False)
quantity: int = Field(default=1, ge=1)
item: Item | None = Relationship(back_populates="orders")
3. Alembic のマイグレーション手順
3.1. マイグレーションファイルの作成
まずはコンテナを立ち上げる必要があります。
docker compose watch
で立ち上げます。バックエンドの更新内容をコンテナに反映する必要があります。
新しいモデルを基にマイグレーションファイルを生成します。
alembic revision --autogenerate -m "Add Order table with relationship to Item"
3.2. マイグレーションファイルの確認
生成されたマイグレーションファイルを確認し、リレーションシップや外部キー制約が正しく定義されていることを確認します。
以下のようなファイルが作成されます。Revision IDなどは自動で割り当てられます。
"""Add Order table With Item Relation
Revision ID: bbadec34fb2b
Revises: 1a31ce608336
Create Date: 2024-12-29 22:28:00.190985
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision = 'bbadec34fb2b'
down_revision = '1a31ce608336'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('order',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('item_id', sa.UUID(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('order')
# ### end Alembic commands ###
3.3. マイグレーションの適用
データベースに変更を適用します。
alembic upgrade head
4. API の実装
新しい Order
テーブルを操作するためのエンドポイントを実装します。
<workdir>/backend/app/api/routes/orders.py
)
4.1. エンドポイントコード(CRUD構成になるようにAPIを作成しています。
また、本来はCurrentUserを利用することで、ユーザのチェックなどが出来ますがサンプルコードですので割愛します。
import uuid
from typing import Any
from fastapi import APIRouter, HTTPException
from sqlmodel import select
from app.api.deps import CurrentUser, SessionDep
from app.models import Item, Message, Order
router = APIRouter()
@router.get("/", response_model=list[Order])
def read_orders(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
statement = select(Order).offset(skip).limit(limit)
orders = session.exec(statement).all()
return orders
@router.get("/{id}", response_model=Order)
def read_order(session: SessionDep, id: uuid.UUID) -> Any:
order = session.get(Order, id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
return order
@router.post("/", response_model=Order)
def create_order(*, session: SessionDep, item_id: uuid.UUID, quantity: int) -> Any:
item = session.get(Item, item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
order = Order(item_id=item_id, quantity=quantity)
session.add(order)
session.commit()
session.refresh(order)
return order
@router.put("/{id}", response_model=Order)
def update_order(*, session: SessionDep, id: uuid.UUID, quantity: int) -> Any:
order = session.get(Order, id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
order.quantity = quantity
session.add(order)
session.commit()
session.refresh(order)
return order
@router.delete("/{id}")
def delete_order(session: SessionDep, id: uuid.UUID) -> Message:
order = session.get(Order, id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
session.delete(order)
session.commit()
return Message(message="Order deleted successfully")
main.py
)
4.2. ルーターの追加(from app.api.routes import items, login, orders, users, utils
api_router.include_router(orders.router, prefix="/orders", tags=["orders"])
5. 動作確認
-
内容の更新
dockerを立ち上げる際にdocker compose watch
を利用していると更新内容が反映されています。
docker compose up
で立ち上げたので一度落としてdocker compose watch
などを実行すると、コンテナの立ち上げに失敗するはずです。コンテナのボリュームマウントの問題です。別のスクラップでまとめています。 -
SwaggerUI を使用して以下の手順をテストします。
-
/items/
エンドポイントでItem
を作成します。 -
/orders/
エンドポイントでOrder
を作成します。- 作成時に
Item
の ID が必要です。
- 作成時に
-
/orders/{order_id}
エンドポイントでOrder
を取得します。
-
6. 学びのメモ
6.1. リレーションシップの双方向定義
Item
と Order
の間にリレーションシップを定義し、back_populates
を使用して双方向のリンクを構築しました。型ヒントには from __future__ import annotations
を利用することで、未定義のクラスを参照可能にしました。