データベースをPythonのオブジェクトとして扱えるのがORMです。
本記事ではSQLAlchemyを使って「モデルの定義」から「CRUDの基本操作」までを一気通貫で体験します。
初心者の方でも手を動かしながら理解できるよう、最小構成と実行例を丁寧に解説します。
SQLAlchemyとは(ORMの基礎)
ORMの仕組みとメリット
ORM(Object-Relational Mapping)はテーブルをクラスに、行をインスタンスに対応付けて扱う仕組みです。
これによりアプリケーションからはuser.name
のように属性へアクセスするだけで、裏側では適切なSQLが発行されます。
SQLの細かな文法に気を取られず、アプリ本来のロジックに集中できるのが大きな利点です。
またORMは保守性と移植性の向上にも寄与します。
モデルクラスやリレーションを中心に設計を統一でき、DB固有の違いはドライバやダイアレクトに吸収されます。
アプリが大きくなってもコードの見通しを保ちやすいことが実務でも評価されています。
SQLAlchemyの特徴と構成
SQLAlchemyはPythonで定番のORMで、「Core」と「ORM」という2層構造を持っています。
ORMはクラス中心の操作を、CoreはSQL式を組み立てる低レイヤを提供します。
必要に応じてORMとCoreを使い分けられる柔軟さが特長です。
主な構成要素は次の通りです。
- ORMレイヤ: Declarative(モデル定義)、Session(トランザクション管理)、Query API(取得・更新)
- Coreレイヤ: SQL Expression Language(型安全なSQL組み立て)
- Dialect: SQLite、PostgreSQL、MySQLなど各DB向けアダプタ
SQLAlchemy 2系では型ヒントを活用した記述が推奨され、モダンなPythonに馴染む書き方になっています。
1系のスタイルとは異なる点があるため、新規学習では2系スタイルを前提にしましょう。
初心者が押さえる用語(モデル/セッション/クエリ)
学び始めで最初に理解しておくと楽になる3つの用語があります。
モデル、セッション、クエリの関係を意識すると全体像がつかみやすいです。
モデルはテーブルを表すPythonクラスで、カラムをクラス属性として宣言します。
セッションはDBとの対話を担う単位で、トランザクション境界を作ります。
クエリはデータ取得のための問い合わせで、ORMではSQLに相当する操作を記述します。
下表は用語の超要約です。
用語 | 役割 | 例 |
---|---|---|
モデル(Model) | テーブルの設計図をクラスで表現 | class User(Base): ... |
セッション(Session) | DB接続とトランザクションの単位 | with Session(engine) as s: ... |
クエリ(Query) | 取得や絞り込みの指示 | select(User).where(User.id==1) |
実際の処理は「セッション内でモデルを使ってクエリする」流れになります。
基本形はwith Session(engine) as session: ...
です。
準備とインストール(Python×SQLAlchemy)
必要環境(Python/SQLite)
最小構成ではPythonとSQLiteだけで始められます。
Python 3.8以上とSQLiteは標準で利用できるため追加セットアップが少ないのが魅力です。
Windows、macOS、LinuxいずれもOKです。
バージョン確認は次の通りです。
コマンドの結果で環境を確認してから進みましょう。
python --version
python -c "import sqlite3; print('sqlite3 version:', sqlite3.sqlite_version)"
Python 3.11.6
sqlite3 version: 3.42.0
SQLAlchemyのインストール(pip)
インストールはpipで完了します。
2系を明示して入れるとサンプルと表記が一致します。
python -m pip install "SQLAlchemy>=2.0"
Successfully installed SQLAlchemy-2.0.x ...
特別なドライバは不要で、SQLiteであれば追加の依存はありません。
他DBを使う時はpsycopg(PostgreSQL)やPyMySQL(MySQL)などを別途導入します。
最小プロジェクト構成
最初は小さく始めるのが理解の近道です。
以下の3ファイル構成で進めます。
orm-tutorial/
├─ database.py # EngineとBaseの定義
├─ models.py # モデル定義(Userなど)
└─ crud_demo.py # CRUDの実行スクリプト(実行例つき)
ファイル | 役割 |
---|---|
database.py | エンジン作成とDeclarative Baseの用意 |
models.py | テーブルに対応するモデルクラスを定義 |
crud_demo.py | セッションを開いてCRUDを実行し、動作を確認 |
躓きやすいのはファイルの役割分担です。
最初はこの最小構成を真似てから、必要に応じて分割や拡張を行うと安全です。
モデル定義とテーブル作成(SQLAlchemy)
Declarative Baseの用意
まずはORMの土台となるBaseクラスを用意します。
全てのモデルはこのBaseを継承します。
加えてSQLiteに接続するエンジンを作成します。
# database.py
# SQLAlchemy 2系スタイルでのBaseとEngine定義
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase
DB_URL = "sqlite:///./orm_tutorial.db" # カレントディレクトリにDBファイルを作成
class Base(DeclarativeBase):
"""全モデルの共通基底クラス"""
pass
# echo=Trueで発行SQLをログ出力(学習時は可視化が便利)
engine = create_engine(DB_URL, echo=True, future=True)
echo=Trueを有効にすると生成されたSQLが見えるため、ORMの挙動理解に役立ちます。
学習時だけ有効化し、実運用では無効化するのがおすすめです。
モデルクラスの定義(Column/型)
次にテーブルに対応するモデルクラスを宣言します。
属性に型と制約を与えることで、テーブル設計をコードで表現します。
# models.py
# Userテーブルを表すモデルを定義(最小限のカラム構成)
from datetime import datetime
from sqlalchemy import String, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from database import Base # 同一ディレクトリ想定
class User(Base):
__tablename__ = "users"
# 主キー。自動採番(INT AUTOINCREMENT相当)
id: Mapped[int] = mapped_column(primary_key=True)
# 必須項目。最大50文字
name: Mapped[str] = mapped_column(String(50), nullable=False)
# 一意制約とインデックスを付与
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
# 作成日時。Python側で挿入時に自動設定
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
def __repr__(self) -> str:
return f"User(id={self.id}, name={self.name!r}, email={self.email!r})"
型ヒント(Mapped[T])とmapped_columnの組み合わせが2系の基本です。
これによりエディタ補完が効き、静的解析にも強くなります。
エンジン作成と接続(SQLite)
エンジンはDBとの接続やドライバ管理を担います。
SQLiteではURLにファイルパスを指定するだけで使い始められます。
# 接続テストの例(任意)
# 既存のdatabase.pyのengineを流用して簡単な生SQLを実行
from database import engine
with engine.begin() as conn:
result = conn.exec_driver_sql("SELECT 1")
print("Connection OK:", result.scalar())
Connection OK: 1
ORMを使っていても低レイヤのCore APIに降りられるのがSQLAlchemyの強みです。
必要に応じて併用できます。
テーブル作成(create_all)
モデル定義が揃ったらテーブルを作成します。
Base.metadata.create_allがマイグレーションの初期手段として有効です。
# init_db.py
# モデルに基づいてテーブルを作成するワンショットスクリプト
from database import engine
from models import Base
Base.metadata.create_all(bind=engine)
print("Tables created!")
# echo=Trueのため一部のSQLログが出力されます(例)
BEGIN (implicit)
PRAGMA main.table_info("users")
PRAGMA temp.table_info("users")
CREATE TABLE users (
id INTEGER NOT NULL,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE (email)
)
COMMIT
Tables created!
create_allは「ないテーブルだけ作る」ため、繰り返し実行しても基本的に安全です。
スキーマ変更の管理は将来的にAlembicの導入を検討してください。
CRUDの基本操作(ORM)
セッションの作成と終了(Session)
実データの操作はセッションを通じて行います。
with文でスコープを区切り、自動的にクローズするのが安全です。
# セッション基本形(例)
from sqlalchemy.orm import Session
from database import engine
with Session(engine) as session:
# session.add(...) / session.execute(...) などを記述
# 必要に応じてsession.commit()
pass # このブロックを出ると自動的にセッションが閉じます
コミットしない限り変更は確定されない点に注意してください。
エラー時はsession.rollback()
で元に戻せます。
Create(追加とコミット)
新規レコードはモデルのインスタンスを作りsession.addで登録、session.commitで確定します。
コミット後は主キーなどが自動採番されます。
Read(取得/フィルタ/並び替え)
取得はselect(User)を使い、session.scalarsでモデルの配列として受け取るのが2系の基本です。
whereで絞り込み、order_byで並び替えます。
Update(更新とコミット)
取得したインスタンスの属性を書き換えてcommitすれば更新されます。
commit後にrefreshで最新状態を反映すると確実です。
Delete(削除とコミット)
削除はsession.delete(obj)で対象を登録し、commitで確定します。
参照整合性には注意しましょう。
以下はCRUDをひと通り実行する最小サンプルです。
1ファイルで完結するため、そのまま実行できます。
# crud_demo.py
# 1ファイルでモデル定義からCRUD、例外処理までを実演します。
from __future__ import annotations
import os
from datetime import datetime
from sqlalchemy import String, DateTime, select, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
DB_FILE = "orm_tutorial.db"
DB_URL = f"sqlite:///./{DB_FILE}"
# 学習用に毎回クリーンな状態にする(既存DBを削除)
if os.path.exists(DB_FILE):
os.remove(DB_FILE)
# Declarative Baseの用意
class Base(DeclarativeBase):
pass
# モデル定義(Userテーブル)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True) # 主キー(INT, AUTOINCREMENT)
name: Mapped[str] = mapped_column(String(50), nullable=False) # 必須の文字列
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) # 一意制約つき
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) # デフォルト現在時刻
def __repr__(self) -> str:
return f"User(id={self.id}, name={self.name!r}, email={self.email!r})"
# Engine作成。学習ではecho=TrueでSQLログを見るのも有効ですが、出力を見やすくするためFalseにしています。
engine = create_engine(DB_URL, echo=False, future=True)
# テーブル作成
Base.metadata.create_all(bind=engine)
# セッションを開いてCRUDを順番に実行
with Session(engine) as session:
# === Create: レコード追加 ===
alice = User(name="Alice", email="alice@example.com")
bob = User(name="Bob", email="bob@example.com")
charlie = User(name="Charlie", email="charlie@example.com")
# まとめて追加してコミット
session.add_all([alice, bob, charlie])
session.commit() # ここでidが採番される
print("After insert:", [alice, bob, charlie])
# === Read: 全件と絞り込み ===
all_users = session.scalars(select(User).order_by(User.id)).all()
print("All users:", all_users)
# 名前に'a'を含むユーザ( SQLiteのLIKEはASCIIでは大文字小文字を区別しないことが多い )
users_with_a = session.scalars(
select(User).where(User.name.like("%a%")).order_by(User.created_at.desc())
).all()
print("Name contains 'a':", users_with_a)
# 個別取得(getは主キー検索に最適)
u2 = session.get(User, 2)
print("Get id=2:", u2)
# === Update: 値の変更とコミット ===
u2.email = "bobby@example.com" # 値を変更
session.commit() # 永続化
session.refresh(u2) # DB上の最新値でオブジェクトを更新
print("Updated user id=2:", u2)
# === Delete: 削除とコミット ===
to_delete = session.get(User, 3)
session.delete(to_delete) # 削除を登録
session.commit() # 確定
remaining = session.scalars(select(User).order_by(User.id)).all()
print("Deleted id=3. Remaining:", remaining)
# === 例外処理: 一意制約違反を誘発してロールバック ===
try:
# すでにalice@example.comが存在するため一意制約違反になる
session.add(User(name="Eve", email="alice@example.com"))
session.commit()
except Exception as e:
session.rollback() # 直前のトランザクションを取り消す
print("Rollback due to error:", type(e).__name__)
After insert: [User(id=1, name='Alice', email='alice@example.com'), User(id=2, name='Bob', email='bob@example.com'), User(id=3, name='Charlie', email='charlie@example.com')]
All users: [User(id=1, name='Alice', email='alice@example.com'), User(id=2, name='Bob', email='bob@example.com'), User(id=3, name='Charlie', email='charlie@example.com')]
Name contains 'a': [User(id=3, name='Charlie', email='charlie@example.com'), User(id=1, name='Alice', email='alice@example.com')]
Get id=2: User(id=2, name='Bob', email='bob@example.com')
Updated user id=2: User(id=2, name='Bob', email='bobby@example.com')
Deleted id=3. Remaining: [User(id=1, name='Alice', email='alice@example.com'), User(id=2, name='Bob', email='bobby@example.com')]
Rollback due to error: IntegrityError
セッションはトランザクションの境界です。
複数の操作を1つのトランザクションにまとめたい時は、コミットのタイミングを意識してください。
例外処理とロールバック
DB操作では一意制約違反や外部キー制約違反などの例外に備える必要があります。
典型的にはtry/except
でIntegrityErrorやSQLAlchemyErrorを捕捉し、session.rollback()で直前の変更を元に戻します。
# 例外処理のテンプレート(部分例)
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session
from database import engine
from models import User
with Session(engine) as session:
try:
session.add(User(name="Duplicate", email="alice@example.com"))
session.commit()
except IntegrityError as ie:
session.rollback()
print("Unique constraint violated:", ie.orig) # DBドライバ由来の例外メッセージ
except SQLAlchemyError as se:
session.rollback()
print("Database error:", se)
except Exception as e:
session.rollback()
print("Unexpected error:", e)
例外が起きたら必ずrollbackという習慣をつけると、トランザクションが中途半端な状態で残る事故を防げます。
commitに成功するまではDBの状態は確定していない点を意識して設計しましょう。
まとめ
本記事では、SQLAlchemyを使ったORMの基本を、モデル定義からテーブル作成、そしてCRUDと例外処理まで一通り解説しました。
モデルはクラス、行はインスタンス、セッションはトランザクションの単位という関係を押さえると、「オブジェクトとしてDBを扱う」感覚が自然に身につきます。
小さなスクリプトで動作を確認したら、プロジェクト構成を整え、必要に応じてAlembicなどのマイグレーションツールや他DBドライバの導入に進むと良いでしょう。
まずは本稿のサンプルを手元で実行し、ORMの手触りを体験することから始めてください。