メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
FastAPI の非同期リクエスト処理が効果を発揮するのは、その下のデータベース呼び出しも非同期である場合だけです。SQLAlchemy 2.0 は型付きの ORM スタイルと非同期エンジンでこれをサポートしますが、MySQL に対して両者をつなぐには、最初の試みで間違えやすいセットアップの手順がいくつかあります。

非同期ドライバの選択

SQLAlchemy 2.0 は、2 つの非同期 MySQL ドライバ、asyncmy と aiomysql のダイアレクトを同梱しています。どちらか一方が公式の推奨として文書化されているわけではありません。asyncmy は Cython で書かれたコンパイル済みのドライバで、現行の Python バージョン向けにビルド済みの wheel を提供しています。このガイドでそれを使うのはそのためです。
sqlalchemy[asyncio] ではなく素の sqlalchemy をインストールするのが、最初の本当の罠です。SQLAlchemy の非同期 ORM 層は greenlet ライブラリで非同期コードと同期コードをつないでおり、それは基本パッケージの依存関係ではありません。[asyncio] エクストラを省略すると、非同期エンジンに初めて触れたときに次のエラーが出ます。
対処は pip install greenlet か、より良いのは、常に [asyncio] エクストラ付きで SQLAlchemy をインストールして自動的に取り込まれるようにすることです。 2 番目の罠は cryptography です。MySQL 8 と VillageSQL は、新しいアカウントのデフォルトを caching_sha2_password 認証プラグインにしています。これは、あるアカウントがあるクライアントライブラリから初めて認証するときにパスワードを送るために、RSA 鍵交換を必要とします。asyncmy は RSA の実装を同梱していないため、この完全なハンドシェイクを行う必要がある接続は次のエラーで失敗します。
アカウントが一度完全な認証を終えると(mysql CLI を含む任意のクライアントから)、サーバーはその結果をキャッシュし、以降の接続は RSA のステップを省く高速なパスを使います。このエラーが、そのアカウントに最後に何が接続したかによって現れたり消えたりするように見えるのはそのためです。cryptography を最初からインストールしておけば、そのキャッシュへの依存が完全になくなり、これが確実な対処です。

データベースとユーザーの作成

root として接続するのではなく、専用のデータベースとアプリケーションユーザーを作成します。
ユーザーのスコープは localhost ではなく 127.0.0.1 です。このガイドの接続文字列は TCP で 127.0.0.1 に接続するため、権限付与はそのホストに一致しなければなりません。Unix ソケット接続は、アカウントの IP ベースの権限付与に関係なく、代わりに localhost に対して照合されます。

プロジェクトのレイアウト

requirements.txt:

非同期エンジンとセッション

app/database.py は、非同期エンジン、セッションファクトリ、およびすべてのモデルが使う宣言的ベースを作成します。
expire_on_commit=False は FastAPI にとって重要です。デフォルトの expire_on_commit=True では、コミットされたオブジェクトのすべての属性が古いものとして印を付けられ、その属性の次の読み取りが、それを更新するための新しい SELECT を発行します。await の外から引き起こされると、その更新は次のエラーで失敗します。
そして、セッションが完全に閉じた後は、同じ属性の読み取りが代わりに次のエラーで失敗します。
expire_on_commit=False は、すでに読み込まれた値をコミット後もオブジェクト上で使える状態に保ち、どちらも回避します。 get_session() はジェネレータ関数であり、これにより FastAPI の Depends() は、リクエストが成功しても例外を送出しても、終了後にセッションを自動的に閉じられます。

モデル

app/models.py は、従来の Column のみのスタイルではなく、SQLAlchemy 2.0 の型付き宣言的スタイルである Mapped と mapped_column を使って Post テーブルを定義します。
server_default=func.now() はタイムスタンプのデフォルトを MySQL 自身に委ねるため、アプリケーションの外から挿入された行でも正しく設定されます。

Alembic のセットアップ

非同期テンプレートで Alembic を初期化します。これにより、非同期エンジン向けにすでに配線された env.py が生成されます。
これはプロジェクトディレクトリの中から実行してください。Alembic は、init に渡されたパスに関係なく、alembic.ini を現在の作業ディレクトリに書き込みます。alembic/ スクリプトフォルダ自体は指定されたパスに作成されますが、誤ったディレクトリから実行すると、予期しない場所に alembic.ini が取り残されます。 alembic.ini を実際のデータベースに向けます。
次に、autogenerate がモデルを認識できるように、alembic/env.py を編集してモデルをインポートします。
マイグレーションを生成して実行します。
autogenerate のステップは新しいテーブルを検出し、マイグレーションを書きました。
VillageSQL に対する適用は成功しました。
実際のサーバーに対して直接確認します。

Pydantic スキーマ

app/schemas.py は、ORM モデルとは別に、リクエストとレスポンスの形を定義します。
from_attributes=True により、PostRead は辞書ではなく Post ORM インスタンスから直接レスポンスを構築できます。

FastAPI のルート

app/main.py は Depends() を通じてセッションを各ルートに配線するため、すべてのリクエストは 1 つのグローバルな接続を共有するのではなく、それぞれ独自のセッションを得ます。
リクエスト間で共有されるグローバルなセッションは、並行負荷のもとで、やがて同じ接続に順序の乱れたクエリが交互に届く事態を招きます。Depends() を通じてリクエストごとに作成されるセッションはそれを回避します。各リクエストが独自のセッションと、プールから取り出した独自の接続を得るためです。

実行と確認

アプリを起動します。
空のテーブルは空のリストを返します。
投稿を作成します。
HTTP 201 ステータスとともに返ります。もう一度一覧すると、新しい行が表示されます。
データベースを直接クエリすると、API のレスポンスだけでなく、書き込みが実際に届いたことを確認できます。

よくある質問

非同期エンジンには、同期エンジンとは異なる接続文字列が必要なのはなぜですか?

接続文字列のドライバ名は、どの DBAPI を読み込むかを SQLAlchemy に伝えます。同期のセットアップは mysql+pymysql:// または mysql+mysqldb:// を使います。非同期のセットアップには asyncio 上に構築されたドライバが必要なため、文字列は代わりに mysql+asyncmy:// を使います。同期ドライバの名前を create_async_engine で使うと即座に失敗します。そのドライバには、SQLAlchemy が呼び出せる非同期インターフェースがないためです。

expire_on_commit=False は必要ですか?

厳密には必要ではありませんが、省略すると、コミットされたオブジェクトのすべての属性アクセスが、それを再読み込みするための新しいデータベースラウンドトリップを引き起こし、そのラウンドトリップは、有効な非同期コンテキストの中で起こる場合にしか機能しません。属性がどこで読まれるかに応じて、これは MissingGreenlet として、またはセッションが閉じた後は DetachedInstanceError として現れます。Web アプリケーションにとって安全なデフォルトは expire_on_commit=False です。

Alembic の同期的な autogenerate ワークフローは引き続き使えますか?

はい。非同期テンプレートの env.py でも、revision --autogenerate と upgrade は通常のコマンドラインから実行できます。非同期の部分が影響するのは、env.py がスキーマを調べて変更するために自身の接続を開く方法だけです。Alembic の CLI コマンド自体については何も変わりません。

トラブルシューティング

関連項目