Closed21

【チーム開発】FastAPI + PostgreSQL + Docker によるWebAPI開発で詰まった所

s_ms_m

SQLModelを使用したモデル設計

class ScriptBase(SQLModel):
    title: str = Field(index=True)
    content: str
    timer: Optional[int] = Field(default=None)
    timer_id: Optional[PyObjectId] = Field(default=None)

class Script(ScriptBase, table=True):
    id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    created_at: datetime = Field(default_factory=datetime.now, index=True)
    updated_at: datetime = Field(default_factory=datetime.now, sa_column_kwargs={'onupdate': datetime.now}, index=True)

class ScriptPublic(ScriptBase):
    id: uuid.UUID
    created_at: datetime
    updated_at: datetime

class ScriptCreate(ScriptBase):
    pass

class ScriptUpdate(ScriptBase):
    title: Optional[str] = None
    content: Optional[str] = None
    timer: Optional[int] = None
    timer_id: Optional[PyObjectId] = None

共通する項目をBaseModelとして、継承する
参考:
https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/#multiple-hero-schemas

s_ms_m

SQLModelでUUID

import uuid

class Script(ScriptBase, table=True):
    id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)

SQLModelでUUIDを使用する場合、default_factoryに関数を渡す。
ここで渡された関数が、DBにアクセスする前に呼ばれる。
参考:
https://sqlmodel.tiangolo.com/advanced/uuid/

s_ms_m

SQLModelでupdated_at

class Script(ScriptBase, table=True):
    id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    created_at: datetime = Field(default_factory=datetime.now, index=True)
    updated_at: datetime = Field(default_factory=datetime.now, sa_column_kwargs={'onupdate': datetime.now}, index=True)

sa_column_kwargsに上記のように渡すと良いそう。
ドキュメントに記載は見つけられなかった。
参考:
https://qiita.com/itoi10/items/fbf2d60413e2f316d253
https://zenn.dev/alcnaka/articles/e16eed5c8e291e

s_ms_m

MongoDBをDockerで構築

mongodb:
    image: mongo:7.0.15
    container_name: mongodb
    ports:
      - "27017:27017"
    volumes:
      - mongodb-store:/data/db
      - ./mongodb/configdb:/data/configdb
    environment:
      - TZ=Asia/Tokyo
    env_file:
      - ./mongodb/.env
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=password
MONGO_INITDB_DATABASE=mongo

dockerで構築し、手元で触ってみる
参考:
https://qiita.com/r1wtn/items/c18e14375bbaa564e289
https://qiita.com/yoshiaki1973/items/e5dab5b88f106a4026b2

s_ms_m

MongoDBとFastAPIと接続

from motor.motor_asyncio import AsyncIOMotorClient

client = AsyncIOMotorClient(MONGO_URL)
db = client.genkou

PyObjectId = Annotated[str, BeforeValidator(str)]

MongoDBはBSONとしてデータが格納されるが、FastAPIはJSONをエンコード、デコードする。
JSONに直接エンコードできないようなデータに対して、BSONはサポートしており、データが保存される前に文字列に変換する。
参考:
https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/#creating-the-application
https://docs.pydantic.dev/latest/api/functional_validators/#pydantic.functional_validators.BeforeValidator

s_ms_m

PydanticでMongoDBのモデルを設計

class TimerInterval(BaseModel):
    timer: int = Field(gt=0)
    intervals: list['TimerInterval'] = []

class TimerBase(BaseModel):
    timer_interval: TimerInterval

class TimerCreate(TimerBase):
    pass

class TimerPublic(TimerBase):
    id: PyObjectId = Field(alias="_id")
    model_config = ConfigDict(
        populate_by_name=True
    )

class TimerUpdate(TimerBase):
    timer_interval: Optional[TimerInterval] = None

Field(gt=0)で正の整数に限定する。
MongoDBでは_idを使用するので、Field(alias="_id")populate_by_name=Trueでエイリアスを設定。
また、Field(...)kwargsで追加の設定をしない限り必要ないとのこと。
参考:
https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/#database-models
https://zenn.dev/gdtypk/articles/pydantic-merit-and-thoughts#field-を使った一般的なバリデーション
https://docs.pydantic.dev/latest/concepts/json_schema/#generating-json-schema
https://stackoverflow.com/questions/60661687/what-is-the-purpose-of-using-field-as-a-default-value-in-pydantic-schemas
https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name

s_ms_m

awaitとasync

async def create_timer(timer: TimerCreate):
    timer_data = timer.model_dump()
    new_timer = await timer_collection.insert_one(timer_data)
    created_timer = await timer_collection.find_one({"_id": new_timer.inserted_id})

    return created_timer

awaitを使用する際は、その外側の関数にasyncが必要。
参考:
https://github.com/andrewsayre/pysmartthings/issues/20

s_ms_m

マイグレーションツールAlembicを導入

alembic init {マイグレーション環境名}

マイグレーション環境を作成

sqlalchemy.url = postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)

alembic.inisqlalchemy.urlを編集する。$()は環境変数名(.envに書くだけではダメ)

from sqlmodel import SQLModel
from genkou.models import Script
###省略###
target_metadata = SQLModel.metadata
###省略###
def run_migrations_online() -> None:
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    url = config.get_main_option("sqlalchemy.url") # 追記

    with connectable.connect() as connection:
        context.configure(
            url=url, # 追記
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

env.pyを編集

alembic revision --autogenerate -m "create tables"

マイグレーションファイルを自動生成

alembic upgrade head

DBにマイグレーションファイルの内容を反映
参考:
https://alembic.sqlalchemy.org/en/latest/tutorial.html#creating-an-environment
https://zenn.dev/shimakaze_soft/articles/4c0784d9a87751#環境のセットアップ
https://zenn.dev/mook_jp/books/sqlmodel-tutorial/viewer/alembic

s_ms_m

SQLテーブルを変更した際に、マイグレーションができなくなった

FastAPIの方で、SQLModelの定義を変更し、マイグレーションを使用としたところ、エラーが発生。
DBにデータがあることが原因と考え、SupabaseのGUIからテーブルを削除
やり直すが、状況は変わらず。
参考より、downgradeするも、状況は変わらず、downgrade baseはできるが、revisionはできない。
そこで、revisions/配下にあるファイルを削除し、やり直したところ、成功。
(原因はわからなかったが、メンターの方によると、テーブルを削除する前に、downgradeをするといけたかもしれないらしい。)
参考:
https://zenn.dev/ryo_t/scraps/e95b7f4856094f

s_ms_m

Renderでデプロイ

FROM python:3.13.0

# To avoid buffering of stdout and stderr
# ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Tokyo

WORKDIR /usr/src/app/

COPY ./requirements.txt /usr/src/app/requirements.txt
RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt

COPY ./app/ /usr/src/app/

CMD ["fastapi", "run", "genkou/app.py", "--port", "80"]

開発でdocker-composeを使用する場合でも、デプロイのことを考えると、 ソースファイルのCOPYなどはDockerfileにあらかじめ書いておくと良いと思った
参考:
https://fastapi.tiangolo.com/deployment/docker/
https://qiita.com/yuuki-h/items/9f594c046a6e676eb8f8

s_ms_m

Pythonのパッケージライブラリのバージョン確認

pip list

--versionで確認することが多いのですが、fastapiではCLIのバージョンが表示されてしまい、確認の方法がわからなかったので、pip listで確認しました。
参考:
https://note.nkmk.me/python-package-version/

このスクラップは22日前にクローズされました