VillageSQL is a drop-in replacement for MySQL with extensions.
All examples on this page work on VillageSQL. Install Now →
非同期ドライバの選択
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 が生成されます。
init に渡されたパスに関係なく、alembic.ini を現在の作業ディレクトリに書き込みます。alembic/ スクリプトフォルダ自体は指定されたパスに作成されますが、誤ったディレクトリから実行すると、予期しない場所に alembic.ini が取り残されます。
alembic.ini を実際のデータベースに向けます。
alembic/env.py を編集してモデルをインポートします。
Pydantic スキーマ
app/schemas.py は、ORM モデルとは別に、リクエストとレスポンスの形を定義します。
from_attributes=True により、PostRead は辞書ではなく Post ORM インスタンスから直接レスポンスを構築できます。
FastAPI のルート
app/main.py は Depends() を通じてセッションを各ルートに配線するため、すべてのリクエストは 1 つのグローバルな接続を共有するのではなく、それぞれ独自のセッションを得ます。
Depends() を通じてリクエストごとに作成されるセッションはそれを回避します。各リクエストが独自のセッションと、プールから取り出した独自の接続を得るためです。
実行と確認
アプリを起動します。よくある質問
非同期エンジンには、同期エンジンとは異なる接続文字列が必要なのはなぜですか?
接続文字列のドライバ名は、どの 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 コマンド自体については何も変わりません。
トラブルシューティング
関連項目
- ORM での MySQL の使用: Django、Rails、および Prisma — 同じ文字セットと厳密モードの設定が SQLAlchemy にも当てはまります
- MySQL の接続プーリング — 並行する FastAPI リクエストのもとでの非同期エンジンのプールサイズの調整