SQLAlchemyとは?Python ORMの基本と使い方を入門から実践まで解説
SQLAlchemy(エスキューエル・アルケミー)は、PythonでデータベースをPythonらしく操作するためのライブラリで、Pythonエコシステムで最も広く使われているORMのひとつです。SQL文を直接書かなくても、Pythonのクラスとデータベースのテーブルを対応づけてデータを読み書きできます。この記事では、現行の最新の安定メジャーバージョンであるSQLAlchemy 2.0系を前提に、基礎となるEngine・Session・Modelの考え方から、モデル定義、CRUD、リレーション、JOIN、よくあるエラー対処までを、動作確認済みのサンプルコードとあわせて解説します。
まとめ:SQLAlchemyの要点を先に押さえる
細かい説明に入る前に、この記事の結論を先にまとめます。
- SQLAlchemyとは:PythonのORM兼データベースツールキット。SQLを書かずにPythonオブジェクトでDBを操作でき、必要なら生SQLに近い記述(Core)も使える二層構造が特徴。
- 覚えるべき3要素:接続を司るEngine、操作の単位となるSession、テーブルを表すModel(モデル)。この3つの関係を理解すれば全体像がつかめる。
- バージョンは2.0系が現役:旧来の
declarative_base()やsession.query()ではなく、DeclarativeBase・Mapped[]・mapped_column()・select()を使うのが現在の標準。型注釈と相性が良く、エディタ補完も効く。 - 基本の流れ:Engineを作る → モデルを定義する → Sessionを開く → CRUD(追加・取得・更新・削除)を行う →
commit()で確定。
以下では、この要点を順番に、実際に動くコードとともに見ていきます。
SQLAlchemyとは何か:PythonのORM/データベースツールキット
SQLAlchemyは2006年から開発が続く歴史あるライブラリで、PostgreSQL・MySQL・SQLite・Oracle・SQL Serverなど主要なリレーショナルデータベースに対応しています。最大の特徴は、ORM(Object Relational Mapper)として動作する点です。ORMとは、プログラムのオブジェクトとデータベースのテーブルを対応づける仕組みで、これを使うとSQL文を書く代わりにPythonの文法でデータを扱えます。たとえばuser.nameのように属性へアクセスするだけで、対応する行のデータを読み書きできます。
ORMを使うメリット
ORMを使う利点は主に3つあります。第一に、データベースの種類が変わっても同じPythonコードで操作できるため、SQLiteで開発しPostgreSQLで本番運用する、といった切り替えが容易です。第二に、文字列でSQLを組み立てないため、SQLインジェクションのリスクを減らせます。第三に、テーブル構造をPythonのクラスとして表現できるので、コードの可読性と保守性が高まります。
CoreとORMの二層構造
SQLAlchemyを理解するうえで重要なのが、CoreとORMという二層構造です。Coreは「SQLをPythonの式で表現する」低レベルのツールキットで、ORMはそのCoreの上に構築された高レベルのオブジェクト指向APIです。普段はORMを使えば十分ですが、複雑な集計や細かなチューニングが必要な場面ではCoreの記述に降りられる柔軟さが、SQLAlchemyが業務システムでも選ばれる理由です。SQLAlchemy 2.0では、ORMでもCoreと同じselect()構文を使うように統一され、両者の境界はより自然になりました。
インストールと環境構築
SQLAlchemy本体はpipで簡単に導入できます。使用するデータベースに応じて、別途ドライバを入れる点に注意してください。
pip install SQLAlchemy
# PostgreSQLを使う場合(推奨ドライバ psycopg / psycopg3)
pip install "psycopg[binary]"
# MySQLを使う場合
pip install PyMySQL
SQLiteはPython標準のため追加ドライバ不要で、ローカル開発やお試しに最適です。PostgreSQL接続用ドライバの選び方についてはPsycopg3とは何か – Python用PostgreSQLデータベースアダプターの概要と主要な特徴もあわせて参照してください。インストール後はバージョンを確認しておきましょう。
python -c "import sqlalchemy; print(sqlalchemy.__version__)"
# 例) 2.0.51
本記事のコードは、本稿執筆時点の最新安定版であるSQLAlchemy 2.0.51で動作を確認しています。2.1系は執筆時点ではベータ(プレリリース)段階のため、実務では2.0系の利用が無難です。最新の対応状況やバージョンは、公式サイト(sqlalchemy.org)で確認してください。
基本の3要素:Engine・Session・Model
SQLAlchemyの操作は、Engine・Session・Modelという3つの登場人物で成り立っています。それぞれの役割を整理します。
| 要素 | 役割 | 主なAPI | 生成タイミング |
|---|---|---|---|
| Engine | DBへの接続・接続プール管理 | create_engine() | アプリ起動時に1つ |
| Session | DBとの仲介・トランザクション管理 | sessionmaker() | 処理・リクエストごと |
| Model | テーブル構造をクラスで表現 | DeclarativeBase | 定義は1回 |
Engineと接続URI
Engineはcreate_engine()で作成し、接続先を「データベースURI」という文字列で指定します。URIは「方言(DBの種類)+ドライバ://ユーザー:パスワード@ホスト:ポート/DB名」の形式です。Engineは接続そのものではなく接続を管理するファクトリで、アプリ起動時に一度だけ作って使い回すのが定石です。
from sqlalchemy import create_engine
# SQLite(ファイル)
engine = create_engine("sqlite:///example.db", echo=True)
# PostgreSQL(psycopg3)
# engine = create_engine("postgresql+psycopg://user:pass@localhost:5432/mydb")
# MySQL(PyMySQL)
# engine = create_engine("mysql+pymysql://user:pass@localhost/mydb")
echo=Trueを渡すと、実行されるSQLが標準出力に表示され、学習やデバッグに役立ちます。本番では通常Falseにします。
Sessionとトランザクション
Sessionは、オブジェクトの追加・変更・取得を一手に引き受ける窓口で、トランザクションの単位でもあります。sessionmaker()でセッションを作る「工場」を用意し、with構文で開閉するのが安全です。withを抜けるときに自動でクローズされ、接続リークを防げます。
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(engine)
with Session() as session:
# ここでDB操作を行う
session.commit() # 変更を確定。例外時はrollbackされる
モデル(テーブル)定義:2.0スタイルのスキーマ設計
モデルは、データベースのテーブル構造をPythonのクラスで表したものです。SQLAlchemy 2.0では、DeclarativeBaseを継承したベースクラスを用意し、各カラムをMapped[型]の型注釈とmapped_column()で宣言します。1.x系で使われていたdeclarative_base()関数やColumnでも動きますが、型チェックやエディタ補完が効く新スタイルが推奨です。
from typing import Optional
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users" # テーブル名
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50)) # NOT NULL
email: Mapped[Optional[str]] = mapped_column(String(255), unique=True) # NULL許可・一意
ポイントは2つあります。第一に、テーブル名は__tablename__で指定します。第二に、カラムの型はMapped[int](→INTEGER)やMapped[str](→VARCHAR)のようにPythonの型から自動で決まり、Optional[str]にするとNULL許可になります。主キーはprimary_key=True、一意制約はunique=True、デフォルト値はdefault=(アプリ側)やserver_default=(DB側)で指定します。定義したテーブルはBase.metadata.create_all(engine)でまとめて作成できます。
なお、運用が始まってからのテーブル変更(カラム追加など)は、手作業ではなくマイグレーションツールで管理するのが安全です。SQLAlchemy公式のマイグレーションツールについてはAlembicとは何か:データベースマイグレーションツールの基本概要と用途で解説しています。
CRUD操作:データの追加・取得・更新・削除
CRUD(Create/Read/Update/Delete)は最も基本的な操作です。SQLAlchemy 2.0では、取得にselect()とsession.scalars()(またはsession.execute())を使うのが標準で、1.x系のsession.query()は旧スタイルとして扱われます。
from sqlalchemy import select
with Session() as session:
# Create(追加)
user = User(name="Taro", email="[email protected]")
session.add(user)
session.commit()
# Read(取得)— 2.0スタイル
taro = session.scalars(
select(User).where(User.name == "Taro")
).first()
all_users = session.scalars(select(User)).all()
# Update(更新)— 取得して属性を変えるだけ
taro.name = "Taro Yamada"
session.commit()
# Delete(削除)
session.delete(taro)
session.commit()
変更はcommit()するまでDBに反映されません。SQLAlchemyは取得済みオブジェクトの変更を自動追跡するため、更新は属性を書き換えてcommit()するだけで済みます。複数件をまとめて入れるときはsession.add_all([...])が便利です。
リレーション:テーブル間の関係を定義する
実際のアプリでは、ユーザーと投稿のように複数テーブルを関連づけて扱います。SQLAlchemyではForeignKey(DB上の外部キー制約)とrelationship()(Pythonオブジェクト同士の関連)を組み合わせて表現します。下は「1人のユーザーが複数の投稿を持つ」一対多の例です。
from typing import List
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
posts: Mapped[List["Post"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(100))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="posts")
これでuser.postsからそのユーザーの投稿一覧へ、post.userから投稿者へと、双方向にたどれます。双方向の関連づけにはback_populatesを両側に明示する方法と、片側だけで済むbackrefがあります。大規模開発では意図が明確なback_populatesが推奨です。cascade="all, delete-orphan"を付けると、親(ユーザー)を削除したときに子(投稿)も連動して削除されます。多対多の関係は、間に中間テーブルを用意しrelationship()のsecondary引数で指定します。下は「投稿」と「タグ」を多対多で結ぶ例です。
from sqlalchemy import Column, Table, ForeignKey
# 中間テーブル(関連だけを持つので Model クラスにはしない)
post_tag = Table(
"post_tag",
Base.metadata,
Column("post_id", ForeignKey("posts.id"), primary_key=True),
Column("tag_id", ForeignKey("tags.id"), primary_key=True),
)
class Tag(Base):
__tablename__ = "tags"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
posts: Mapped[List["Post"]] = relationship(
secondary=post_tag, back_populates="tags"
)
# Post 側にも secondary=post_tag の relationship を追加すれば双方向にたどれる
# posts.tags / tag.posts で相互参照できる
JOINとロード戦略:複数テーブルを効率よく取得する
複数テーブルをまたぐ取得には、明示的なJOINと、リレーション経由の自動取得の2通りがあります。明示的なJOINはselect(...).join(...)で記述します。
from sqlalchemy import select, func
with Session() as session:
# ユーザー名ごとの投稿数を集計
rows = session.execute(
select(User.name, func.count(Post.id))
.join(Post)
.group_by(User.id)
).all()
# 例) [('Taro', 2)]
リレーションを使うと、JOINを書かずに関連データへアクセスできますが、ループ内で関連先に触れるたびにクエリが発行される「N+1問題」に注意が必要です。これを避けるには、関連データを先読みするeager loadingを使います。
| ロード戦略 | 指定 | 取得方法 | 向いている場面 |
|---|---|---|---|
| 遅延(既定) | lazy=”select” | アクセス時に都度 | 関連を使わない時 |
| JOIN先読み | joinedload() | 1クエリでJOIN | 1対1・少数の関連 |
| サブクエリ先読み | selectinload() | 別クエリでまとめて | 一対多の大量取得 |
from sqlalchemy.orm import joinedload
stmt = select(User).options(joinedload(User.posts))
users = session.scalars(stmt).unique().all() # N+1を回避
実践:最小構成の動くサンプル
ここまでの要素を1つにまとめた、そのまま動かせる最小構成のサンプルです(SQLAlchemy 2.0.51で動作確認済み)。SQLiteのインメモリDBを使うため、追加のセットアップは不要です。
from typing import List, Optional
from sqlalchemy import create_engine, String, ForeignKey, select, func
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker,
)
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
email: Mapped[Optional[str]] = mapped_column(String(255), unique=True)
posts: Mapped[List["Post"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(100))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="posts")
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(engine)
with Session() as session:
taro = User(name="Taro", email="[email protected]")
taro.posts = [Post(title="はじめての投稿"), Post(title="2本目")]
session.add(taro)
session.commit()
with Session() as session:
user = session.scalars(select(User).where(User.name == "Taro")).first()
print(user.name, [p.title for p in user.posts])
# Taro ['はじめての投稿', '2本目']
このコードは、テーブル作成 → ユーザーと投稿の追加 → 取得までを一気通貫で行います。FastAPIなどのフレームワークと組み合わせた実践的なモデル設計はFastAPIとSQLAlchemyでのモデルの構築方法とベストプラクティスで詳しく扱っています。
初心者がつまずきやすいエラーと対処法
SQLAlchemyでよく遭遇する代表的なエラーと対処の方向性をまとめます。
| エラー | 主な原因 | 対処の方向 |
|---|---|---|
| OperationalError | 接続URI誤り・DB未起動 | URI・サーバ稼働を確認 |
| IntegrityError | 一意制約/NOT NULL違反 | 事前チェック・制約見直し |
| InvalidRequestError | セッション多重利用 | 処理ごとにSession生成 |
エラーへの基本対策は、DB操作をtry / exceptで囲み、例外時はrollback()でトランザクションを戻すことです。with構文でセッションを開いていれば、例外発生時のクローズ漏れも防げます。エラーメッセージは英語でも要点(どのカラム・どの制約か)が明示されているため、まずは落ち着いて全文を読むことが解決の近道です。
よくある質問(FAQ)
SQLAlchemyの読み方は?
「エスキューエル・アルケミー」と読みます。Alchemyは「錬金術」を意味し、ロゴにも錬金術のモチーフが使われています。
Django ORMとの違いは何ですか?
Django ORMはDjangoフレームワークに統合された専用ORMで手軽さが魅力です。一方SQLAlchemyはフレームワーク非依存で、FlaskやFastAPIなど任意の環境で使え、生SQLに近いCoreまで降りられる柔軟性が強みです。複雑なクエリやチューニングが求められるプロジェクトで特に力を発揮します。
1.x系のコードはそのまま動きますか?
多くは動きますが、declarative_base()やsession.query()は旧スタイルです。新規開発ではDeclarativeBase・Mapped[]・select()を使う2.0スタイルを推奨します。型チェックやエディタ補完の恩恵が大きく、将来的な移行コストも下げられます。
SQLAlchemyとSQLModelはどう使い分けますか?
SQLModelはSQLAlchemyとPydanticを組み合わせ、FastAPI向けに使いやすくしたライブラリです。FastAPIでAPIの入出力バリデーションとDBモデルを兼ねたい場合はSQLModelが便利で、純粋なDB操作の自由度や成熟度を重視するならSQLAlchemyが適します。詳しくはSQLModelとは何か:基本的な説明とその特徴を参照してください。