SQLAlchemy 2.0 #1 全体像 — Core と ORM、そして 2.0 スタイル

Python でデータベースを扱うコードは、どこへ行っても結局 SQLAlchemy に行き当たります。FastAPI のプロジェクトでも、データパイプラインでも、古い Flask アプリでもそうです。問題は、ネット上に 1.x 時代のコードと 2.0 スタイルのコードが混在していて、検索しながら学ぶと互換性のない文法がごちゃ混ぜになることです。このシリーズは 2.0 スタイルに統一して SQLAlchemy を基礎から整理する全 7 回です。モダン Python 実践 #3 で FastAPI 連携を概要として扱ったのに対し、このシリーズは SQLAlchemy そのものを深く掘り下げます。

SQLAlchemy は 2 層です — Core と ORM #

SQLAlchemy を学び始めるとき、最初につかむべき絵はこれです。SQLAlchemy はひとつのライブラリではなく、2 層のスタックです。

  • Core: データベース接続(エンジン、コネクションプール)、トランザクション、そして SQL を Python の式として組み立てる SQL Expression Language まで。「SQL を Python で書く道具」です。
  • ORM: Core の上に載る層で、テーブルの行を Python オブジェクトにマッピングします。オブジェクトを作って変更すると、ORM が変更を追跡して必要な SQL を代わりに作ってくれます。

ORM を使う場合も、接続とトランザクションは Core のものをそのまま使い、ORM のクエリも内部では Core の式に変換されます。だからこのシリーズも同じ順序で進みます。次回で Core(エンジン、トランザクション)を固めてから、第 3 回で ORM に上がります。Core を飛ばして ORM だけ学ぶと、「なぜコミットされていないのか」「なぜコネクションが枯渇したのか」といった問題で必ず行き詰まります。

どちらの層を主に使うかは用途で分かれます。ドメインオブジェクトが明確なアプリケーション(ユーザー、注文、投稿)は ORM が合いますし、大量集計やレポートのように行がオブジェクトである必要のない処理は Core のほうが単純で高速です。両者は排他的ではなく、ひとつのプロジェクトで混ぜて使うのが普通です。

1.x と 2.0 — 何がなぜ変わったのか #

SQLAlchemy 2.0(2023 年リリース)は、長年積み重なっていた 2 つの問題を整理しました。

  1. クエリの書き方が複数ありました。 1.x では ORM は session.query(User)、Core は select() と、クエリの方式が別々でした。2.0 は select() ひとつに統一しました。同じ文法が Core でも ORM でも動きます。
  2. 暗黙の動作が多すぎました。 自動コミットや暗黙のコネクションのような「よしなにやってくれる」動作は便利に見えて、デバッグを難しくしていました。2.0 はトランザクションの開始とコミットをコードに明示するスタイルに変えました。

さらに 2.0 は型ヒントを正式サポートしています。モデルを Mapped[int]Mapped[str] と宣言すれば、IDE の補完と mypy のチェックがクエリ結果まで通ります。これだけでも 2.0 スタイルに移る理由として十分です。

実務で 1.x スタイルのコードに出会ったときの見分け方は簡単です。session.query(...) が見えたら 1.x スタイル(2.0 でも動きはしますがレガシー)、select(...)session.execute()session.scalars() に渡していれば 2.0 スタイルです。このシリーズのコードはすべて後者です。

インストールと最初の接続 #

インストールは uv なら 1 行です。パッケージ管理が初めてなら Python パッケージングシリーズを先にどうぞ。

インストール
uv add sqlalchemy

DB サーバーなしで始めるために SQLite を使います。SQLite のドライバーは Python に同梱されているので追加インストールは不要です。接続の起点はエンジン(Engine) です。

main.py
from sqlalchemy import create_engine

engine = create_engine("sqlite:///app.db", echo=True)
  • 第 1 引数は接続 URL です。方言+ドライバー://ユーザー:パスワード@ホスト/DB名 の形式で、SQLite はファイルパスだけを書きます。PostgreSQL なら postgresql+psycopg2://user:pw@localhost/mydb のようになります。
  • echo=True は実行される SQL をすべてログに出します。学習中はオンにしておくことをおすすめします。ORM が自分のコードをどんな SQL に変えるのかを目で確認するのが、いちばん速い学習法です。
  • create_engine はこの時点では接続しません。エンジンは「接続の方法とコネクションプール」を持つオブジェクトで、実際の接続は最初のクエリのときに起きます。

最初のクエリ — text() で SQL を直接実行 #

抽象化を学ぶ前に、いちばん低い層から確認します。text() は SQL 文字列をそのまま実行する道具です。

main.py
from sqlalchemy import text

with engine.connect() as conn:
    result = conn.execute(text("SELECT 'hello' AS greeting"))
    print(result.all())  # [('hello',)]
  • engine.connect() はプールからコネクションを借り、with ブロックが終わると返却します。
  • conn.execute() の戻り値は Result オブジェクトです。.all().first().scalar() などのメソッドで行を取り出します。行はタプルのようにも、カラム名でもアクセスできます。

パラメータは必ずバインドで渡します。文字列フォーマットで SQL を組み立てるのは SQL インジェクションへの近道です。

main.py
with engine.connect() as conn:
    result = conn.execute(
        text("SELECT :name AS name, :age AS age"),
        {"name": "佐藤", "age": 30},
    )
    row = result.first()
    print(row.name, row.age)  # 佐藤 30

text() だけでも DB の作業はすべて可能です。それでも SQLAlchemy を使う理由は、この上の層が SQL 文字列組み立てのミスをコンパイル時に近いところまで前倒しし、DB 方言の差を吸収し、オブジェクトマッピングを自動化してくれるからです。シリーズを通して層をひとつずつ上がっていきます。

このシリーズの地図 #

テーマ中心となる問い
1(この記事)全体像Core と ORM は何で、2.0 は何が違うのか
2エンジンとトランザクションコネクションプールと commit はどう動くのか
3ORM モデル定義テーブルを Python クラスでどう宣言するのか
4セッションORM は変更をどう追跡し、いつ SQL を発行するのか
5リレーション1:N、N:M と N+1 問題をどう扱うのか
6クエリ応用結合、集計、サブクエリ、大量処理
7Alembic と実戦構成スキーマ変更をどう管理し、非同期はどうするのか

まとめ #

  • SQLAlchemy は Core(接続、トランザクション、SQL 式)と ORM(オブジェクトマッピング)の 2 層構造です。ORM も Core の上で動きます。
  • 2.0 スタイルは select() にクエリを統一し、トランザクションを明示的にし、型ヒントを正式サポートします。session.query() が見えたらレガシースタイルです。
  • 接続の起点は create_engine() で、エンジンは接続方法とプールを管理するだけで、生成時点では接続しません。
  • text() とパラメータバインドで SQL を直接実行できます。文字列フォーマットで SQL を組み立ててはいけません。
  • 次回はエンジンとコネクションプール、そして 2.0 の明示的なトランザクションパターン 2 つを扱います。
X