🤸
【SQLAlchemy】Modelの循環インポートを撲滅する
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.py
がpost.py
をインポートし、同時にpost.py
がuser.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")
これで循環参照がなくなったはずです!
Discussion