green snake
Photo by Pixabay on Pexels.com

FastAPIで発生する sqlalchemy.exc.MissingGreenlet の原因と解決方法

FastAPIでSQLAlchemyを使用する際に、以下のようなエラーが発生することがあります。

sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here. Was IO attempted in an unexpected place?

このエラーは 非同期処理(async)と同期処理(sync)が適切に扱われていない 場合に発生します。本記事では、考えられる原因とその解決方法を、修正前修正後のコード付きで解説します。


原因と解決策

原因1: 非同期関数 (async def) 内で同期エンジン (create_engine()) を使用している

FastAPIでは、非同期処理を適切に行うために、非同期エンジン(create_async_engine())を使用する必要があります
同期エンジン(create_engine())を使ってしまうと、非同期関数内でSQLAlchemyの同期メソッドが動作せず、MissingGreenlet エラーが発生します。

修正前

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# 同期エンジンを使用している
SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

async def get_db():
    db = SessionLocal()
    try:
        yield db  # 非同期関数内で同期エンジンを使用
    finally:
        db.close()

修正後

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

# 非同期エンジンを使用する
SQLALCHEMY_DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)

async def get_db():
    async with AsyncSessionLocal() as db:
        yield db  # 非同期関数内で非同期エンジンを使用

原因2: 非同期関数 (async def) で同期セッション (SessionLocal) を使用している

async def 関数内では、同期的な SessionLocal.commit().execute() を直接実行できません
SQLAlchemy 2.0 では、非同期処理のために AsyncSession を使用する必要があります。

修正前

async def create_user(db, user_data):
    new_user = User(**user_data)
    db.add(new_user)
    db.commit()  # ここでエラー発生
    db.refresh(new_user)
    return new_user

修正後

async def create_user(db: AsyncSession, user_data):
    new_user = User(**user_data)
    db.add(new_user)
    await db.commit()  # 非同期処理には await を付ける
    await db.refresh(new_user)
    return new_user

原因3: Depends(get_db) を非同期関数 (async def) に適用している

FastAPIでは、依存関係としてデータベースセッションを Depends(get_db) で渡すことが多いですが、
非同期関数 (async def) の中で同期セッションを使うと MissingGreenlet エラーが発生します

修正前

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session

app = FastAPI()

async def get_users(db: Session = Depends(get_db)):  # Sessionは同期用
    return db.query(User).all()  # エラー発生

修正後

from sqlalchemy.ext.asyncio import AsyncSession

async def get_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User))  # 非同期用のクエリ実行
    return result.scalars().all()

原因4: FastAPIのルート関数 (@app.get) を同期 (def) にしているのに await を使っている

FastAPIでは、async def を使うべき場面で def を使ってしまうと、
内部の await 呼び出しが適切に処理されず、エラーになります。

修正前

@app.get("/users")
def get_users(db: AsyncSession = Depends(get_db)):  # def なのに async 対応の DB を使っている
    result = await db.execute(select(User))  # ここでエラー発生
    return result.scalars().all()

修正後

@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_db)):  # async def にする
    result = await db.execute(select(User))
    return result.scalars().all()

原因5: asyncpg を使っていない

非同期エンジンを使用する場合、PostgreSQL用のドライバとして asyncpg をインストールする必要があります

修正前

# postgresql:// のままでは同期接続
SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname"

修正後

# postgresql+asyncpg:// に変更
SQLALCHEMY_DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"

必要なパッケージをインストール:

pip install asyncpg

まとめ

原因 修正前 修正後
非同期関数で同期エンジンを使用 create_engine() を使用 create_async_engine() に変更
async def 内で同期セッションを使用 db.commit() await db.commit()
Depends(get_db) のミスマッチ Session = Depends(get_db) AsyncSession = Depends(get_db)
FastAPIのルート関数が同期関数 def get_users() async def get_users()
asyncpg を使っていない postgresql:// postgresql+asyncpg://

結論

このエラーは、SQLAlchemyの非同期機能を正しく使えていない場合に発生します
非同期エンジン (create_async_engine) を使用し、非同期セッション (AsyncSession) を適切に扱い、
クエリ実行時には await を忘れないようにすることで解決できます。

これらのポイントを押さえて、FastAPIとSQLAlchemyを正しく組み合わせて使いましょう!

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)