🤸

【SQLAlchemy】Modelの循環インポートを撲滅する

2024/09/18に公開

FastAPIとSQLAlchemyにおける循環インポート問題の解決策

循環参照が発生した

FastAPIとSQLAlchemyを用いたWebアプリケーション開発において、以下のようなエラーメッセージに遭遇することがあります。

ImportError: cannot import name 'User' from partially initialized module 'app.models.user' (most likely due to a circular import)

このエラーは「循環インポート問題」として知られており、モデル間の相互参照によって引き起こされます。本記事では、この問題の原因と効果的な解決策について解説します。

TYPE_CHECKINGを使って解決

これらの手法を適切に適用することで、FastAPIとSQLAlchemyを使用したプロジェクトにおける循環インポート問題を解決し、型チェックの利点を維持しつつ、実行時のパフォーマンスを確保することが可能となります。

発生した状況

循環インポート問題は、二つ以上のモジュールが互いに依存関係を持つ際に発生します。典型的な例として、ユーザー(User)と投稿(Post)モデルの関係性を考えてみましょう。

# user.py
from sqlalchemy.orm import relationship
from app.models.post import Post

class User(Base):
    # ...
    posts = relationship("Post", back_populates="user")

# post.py
from sqlalchemy.orm import relationship
from app.models.user import User

class Post(Base):
    # ...
    user = relationship("User", back_populates="posts")

この構造では、user.pypost.pyをインポートし、同時にpost.pyuser.pyをインポートしているため、Pythonのインタープリタが適切な初期化順序を決定できなくなります。

解決した方法

まず、基底クラスを定義します:

UserとPostモデルを以下のように実装します:

# user.py
from __future__ import annotations
from typing import TYPE_CHECKING, List
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base

if TYPE_CHECKING:
    from app.models.post import Post

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True)
    posts: Mapped[List["Post"]] = relationship("Post", back_populates="user")

# post.py
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base

if TYPE_CHECKING:
    from app.models.user import User

class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(100))
    content: Mapped[str] = mapped_column(Text)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    user: Mapped["User"] = relationship("User", back_populates="posts")

これで循環参照がなくなったはずです!

株式会社Xronotech

Discussion