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項目
- スタックトレースから、例外の直前に実行されたデータベース操作を特定します。
- PostgreSQLの接続URLが、この例で使う非同期ドライバの
postgresql+asyncpg://になっているか確認します。 - エンジンが
create_async_engine()で作られているか確認します。 - 依存関係から渡されるセッションが
AsyncSessionか確認します。 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(...) |
| ルート関数 | defにawaitを書く |
async defで定義する |
切り分けの進め方
MissingGreenletが出たら、最初にスタックトレースで入出力が始まった行を確認します。
次に、接続URL、エンジン、セッション、依存関係、各データベース操作をたどり、同期方式と非同期方式が混在していないかを調べます。
この記事の非同期構成では、create_async_engine()、AsyncSession、postgresql+asyncpg://、必要なawaitを一組としてそろえることが解決の基準です。

