0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【FastAPI】Route・UseCase・Repositoryの役割を整理する

0
Posted at

FastAPIをClean Architecture寄りに分割していると、

Route
↓
UseCase
↓
Repository
↓
Database

という構成がよく登場します。

自分が実装していて混乱したのは、**「それぞれ何を担当するのか」「どのオブジェクトを渡せばよいのか」**でした。

RouteはHTTPの入口

Routeでは、HTTPに関する処理を担当します。

例えば、

@router.get(
    "/help/{slug}",
    response_model=HelpDetail,
)
async def read_help(
    slug: str,
    db: AsyncSession = Depends(get_db),
):
    repository = HelpRepositoryImpl(db)
    use_case = HelpUseCase(repository)

    return await use_case.get_help(slug)

ここでは、

  • URLやHTTPメソッド
  • Path / Query Parameter
  • Dependency Injection
  • Response Model

などを扱います。

一方、具体的なSQLをRouteへ直接書くと責務が増えてしまうため、DB処理はRepositoryへ任せます。

UseCaseは「アプリケーションとして何をするか」

UseCaseでは、

slugからヘルプ詳細を取得する

のような、アプリケーションの処理を表現します。

class HelpUseCase:
    def __init__(
        self,
        help_repo: HelpRepository,
    ):
        self.help_repo = help_repo

    async def get_help(
        self,
        slug: str,
    ):
        help_item = await (
            self.help_repo.get_help_by_slug(slug)
        )

        return help_item

UseCase自身がSQLAlchemyのクエリを書くのではなく、必要なデータをRepositoryへ依頼します。

RepositoryはDBアクセスを担当する

Repositoryでは、SQLAlchemyを使った具体的な取得処理を書きます。

class HelpRepositoryImpl:
    def __init__(
        self,
        session: AsyncSession,
    ):
        self.session = session

    async def get_help_by_slug(
        self,
        slug: str,
    ):
        stmt = select(Help).where(
            Help.slug == slug
        )

        return (
            await self.session.scalars(stmt)
        ).one_or_none()

つまり、

UseCase
→ 「slugからHelpが欲しい」

Repository
→ 「SQLAlchemyでどう取得するか」

という役割分担です。

Session object has no attribute ...が出た理由

今回つまずいたのが、UseCaseへSessionを直接渡していたケースです。

# NG
use_case = HelpUseCase(db)

UseCaseは、

self.help_repo.get_help_by_slug(slug)

を呼びます。

しかしself.help_repoに入っているのはRepositoryではなくAsyncSessionです。

結果として、

AsyncSession.get_help_by_slug(...)

を呼ぼうとしてしまいます。

当然Sessionにはそのメソッドがないため、AttributeErrorになります。

正しくは、

repository = HelpRepositoryImpl(db)
use_case = HelpUseCase(repository)

とします。

Repository Interfaceは何のため?

Pythonでは、具象クラスにメソッドが存在すれば、Interfaceに書かれていなくても技術的には呼び出せます。

それでもInterfaceを用意するのは、

UseCaseがRepositoryに何を要求しているか

という契約を明確にするためです。

class HelpRepository(ABC):

    @abstractmethod
    async def get_help_by_slug(
        self,
        slug: str,
    ):
        ...

これにより、UseCaseが具体的なSQLAlchemy実装ではなく、Repositoryという抽象へ依存できます。

まとめ

役割は次のように分けると理解しやすくなります。

Route
→ HTTPを扱う

UseCase
→ アプリケーションの処理を表現する

Repository
→ DB操作を抽象化する

RepositoryImpl
→ SQLAlchemyで実際にDBへアクセスする

AsyncSession
→ RepositoryImplがDBアクセスに使う

特に重要なのは、SessionとRepositoryは別物ということです。

この境界が分かると、FastAPIのレイヤー構成やDIのコードがかなり読みやすくなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?