FastAPIのsqlalchemy.exc.MissingGreenletを直す:非同期設定の確認手順

green snake
Photo by Pixabay on Pexels.com

FastAPIでSQLAlchemyの非同期機能を使うと、次の例外が発生することがあります。

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

MissingGreenletは、SQLAlchemyの非同期処理が想定していない実行経路で、データベース入出力を始めようとしたことを示す例外です。

async defの中に同期コードがあるという理由だけで原因を決めず、接続URL、エンジン、セッション、クエリの呼び出し方が同じ方式にそろっているかを順番に確認します。

MissingGreenletが示す状態

同期処理は、データベース操作の完了を待ってから次の処理へ進む書き方です。

非同期処理は、待ち時間を含む操作をawaitで明示し、非同期対応のエンジンとセッションを通して実行する書き方です。

SQLAlchemyの非同期機能では、非同期ドライバへの入出力が所定の実行経路から呼ばれる必要があります。

例外文にあるgreenlet_spawn has not been calledは、その経路の外で入出力が要求された可能性を示しています。

最初に確認する5項目

  1. スタックトレースから、例外の直前に実行されたデータベース操作を特定します。
  2. PostgreSQLの接続URLが、この例で使う非同期ドライバのpostgresql+asyncpg://になっているか確認します。
  3. エンジンがcreate_async_engine()で作られているか確認します。
  4. 依存関係から渡されるセッションがAsyncSessionか確認します。
  5. execute()commit()refresh()など、非同期で実行する操作にawaitが付いているか確認します。

同期構成を採用する場合は、エンジン、セッション、呼び出し方をすべて同期方式にそろえます。

この記事では、元の構成に合わせてPostgreSQLとasyncpgを使う非同期方式に統一します。

非同期エンジンとセッションをそろえる

方式が混在した構成

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(bind=engine)

async def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

この例では、非同期の依存関数から同期エンジンと同期セッションを渡しているため、後続の処理がどちらの方式を前提にしているのか分かりにくくなります。

この混在だけでMissingGreenletと断定することはできませんが、非同期用の呼び出しと組み合わせる前に解消すべき状態です。

非同期方式に統一した構成

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

接続URL、エンジン、セッションを非同期方式にそろえると、ルート関数やサービス関数も同じ前提で書けます。

データベース操作にawaitを付ける

AsyncSessionを受け取る関数では、非同期のデータベース操作をawaitします。

同期セッションの呼び出し方が残った例

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

AsyncSessionに合わせた例

async def create_user(db: AsyncSession, user_data):
    new_user = User(**user_data)
    db.add(new_user)
    await db.commit()
    await db.refresh(new_user)
    return new_user

add()は元の例と同じくそのまま呼び、commit()refresh()にはawaitを付けます。

Dependsが返すセッション型を確認する

FastAPIの依存性注入で使うDepends(get_db)自体がMissingGreenletの原因になるわけではありません。

確認すべきなのは、get_dbが返すセッション型と、ルート関数の呼び出し方が一致しているかどうかです。

from fastapi import Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

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

AsyncSessionを使うルート関数はasync defで定義し、クエリ実行をawaitします。

通常のdefの本文にawaitを書くコードはPythonの構文として成立しないため、MissingGreenletの修正前例にはなりません。

PostgreSQLの非同期ドライバを設定する

この記事の構成では、PostgreSQLの非同期ドライバとしてasyncpgを使います。

psycopg2との使い分けは、psycopg2とasyncpgの違いも参照してください。

pip install asyncpg

インストール後は、接続URLにpostgresql+asyncpg://を指定します。

SQLALCHEMY_DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"

設定の対応表

非同期方式で確認する設定と呼び出し方
確認箇所 混在している例 非同期方式の例
接続URL postgresql:// postgresql+asyncpg://
エンジン create_engine() create_async_engine()
セッション Session AsyncSession
コミット db.commit() await db.commit()
クエリ実行 db.execute(...) await db.execute(...)
ルート関数 defawaitを書く async defで定義する

切り分けの進め方

MissingGreenletが出たら、最初にスタックトレースで入出力が始まった行を確認します。

次に、接続URL、エンジン、セッション、依存関係、各データベース操作をたどり、同期方式と非同期方式が混在していないかを調べます。

この記事の非同期構成では、create_async_engine()AsyncSessionpostgresql+asyncpg://、必要なawaitを一組としてそろえることが解決の基準です。

投稿者 greeden

コメントを残す

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

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