最終更新:

ORMの仕組み — プログラムのメモと、DBの1行を結ぶ


オブジェクトと、テーブルの行を結ぶ

プログラムのメモUsername = "ひよこ"Sessionへ追加対応の規則を定義DBの1行usersid | name1 | ひよこINSERTとcommitで保存
本文のSQLAlchemyとSQLiteの例です。クラスと列の対応を定義し、SQLで保存します。add、flush、commitは異なる段階です。
ひよこ ひよこ
ORMなら、SQLを知らなくてもいいの?
ペンギン先生 ペンギン先生
オブジェクトとリレーショナルDBの対応を助ける道具だよ。よく使う保存や検索を書きやすくするけれど、裏ではSQLやトランザクションが動く。まず、1行を保存して読む例を見よう。
ひよこ ひよこ
クラスとテーブルが、そのまま1対1になる?
ペンギン先生 ペンギン先生
この例ではUserをusers、nameを列へ対応させる。ただし、継承や複数テーブル等の対応もあり、すべてのORMでクラス1個=テーブル1個が唯一のルールではないよ。
ひよこ ひよこ
session.addと書いた瞬間に保存は確定?
ペンギン先生 ペンギン先生
SQLAlchemyではSessionへ追加した段階と、flushでDBへ変更を送る段階、commitで確定する段階を分けるよ。エラーならrollbackが必要になることもあるんだ。
ひよこ ひよこ
ユーザーの投稿も、自動で全部読める?
ペンギン先生 ペンギン先生
関連の設定や取得戦略によるよ。必要なときに読むlazy loadingと、先にまとめて読むeager loading等がある。何を読むか、SQLが何回出るかを確認するんだ。
ひよこ ひよこ
N+1問題って、どういう増え方?
ペンギン先生 ペンギン先生
ユーザー一覧を1回で読み、そのN人の投稿を1人ずつ読むとN回増える例だよ。合計N+1回。ただし、関連・既に読み込んだ値・キャッシュ等で変わるので、まず発行されたSQLを見るんだ。
ひよこ ひよこ
先にまとめて読めば、必ず2回のSQL?
ペンギン先生 ペンギン先生
JOINで1回にする方式やINでまとめる方式等がある。件数や関係によって複数回になる場合もあるし、JOINで行数が増えることもある。常に2回や常に速いとは決めないよ。
ひよこ ひよこ
Active RecordとData Mapperは、製品ごとに固定?
ペンギン先生 ペンギン先生
モデル自身に保存等の操作を持たせるか、保存の対応を分けるかという設計の違いだね。TypeORMは両方に対応している。小規模ならこちら、大規模ならあちらと規模だけで決める必要もないよ。
ひよこ ひよこ
ORMで書けない処理は、どうすればいい?
ペンギン先生 ペンギン先生
集計や大量更新等では、用意された一括操作やSQLを検討できる。型が付いていても、入力の許可や生SQLの安全な渡し方は別の確認だよ。生成SQLと実行計画を見て、用途に合う方法を選ぼう。

まずは、1行を保存して読み戻す

Python 3の新しい練習用環境へSQLAlchemy 2.0系を追加します。既存プロジェクトとは別の仮想環境を用意してください。

python -m venv .venv

以下のpythonは、WindowsのPowerShellなら.\.venv\Scripts\python.exe、macOS/Linuxなら.venv/bin/pythonに置き換えます。

python -m pip install "SQLAlchemy>=2.0,<2.1"
python orm-demo.py

orm-demo.pyへ次を保存します。DBはメモリ内で、既存のDBへ接続しません。

from sqlalchemy import create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]

engine = create_engine("sqlite://", echo=True)
Base.metadata.create_all(engine)
with Session(engine) as session:
    session.add(User(name="ひよこ"))
    session.commit()
    user = session.scalars(select(User)).one()
    print(user.id, user.name)

SQL等のログの後で1 ひよこと表示されます。echo=TrueのログでCREATE TABLE、INSERT、SELECT等を見てみましょう。オブジェクトで書いても、SQLはなくなりません。

対応と保存の段階を見る

この例ではUserクラスがusersテーブル、nameが列、保存されたUserが1行に対応します。他の対応方法もあるため、すべてのクラスが必ず1テーブルになるとは限りません。

SQLAlchemyのaddはSessionへ追加する操作です。flushは変更をDBへ送り、commitはトランザクションを確定します。問い合わせ前に自動でflushされることもあります。エラー時のrollbackやSessionの終了を含め、確定する場所を意識しましょう。

もう少し詳しく:関連の取得とN+1

一覧を1回で読み、各N件の関連を1回ずつ読むなら、合計N+1回のSELECTになります。JOINで取る方式、selectinload等でINを使ってまとめる方式などがあります。件数による分割や複合キー、行数の増加等の条件があるので、「eager loadingなら必ず2回」で覚えません。

TypeORMはActive RecordとData Mapperの両方を使えます。設計パターンを製品名と1対1で固定せず、モデルの責任・保存を分ける必要・チームの理解で選びます。生SQLや一括操作でも、値はパラメーターで渡し、許す操作と対象を確かめます。

🐧 ペンギン先生のまとめ:「ORM」って出てきたら「プログラムのオブジェクトと、DBのテーブルを結ぶ道具」と思えばだいたいOK!

SQLの基本はSQL入門、道具の比較はPrismaとDrizzleへ進めます。

参考資料