FastAPIの作者が作った「SQLModel」が革命的すぎるので、全Python使いに教えたい
この記事はLivetoon Tech Advent Calendar 2025の11日目の記事です。
本日はCTOの私がよく使ってるSQLModelについてお話します。
宣伝
今回のアドベントカレンダーでは、LivetoonのAIキャラクターアプリのkaiwaに関わるエンジニアが、アプリの話からLLM・合成音声・インフラ監視・GPU・OSSまで、幅広くアドベントカレンダーとして書いて行く予定です。
是非、publicationをフォローして、記事を追ってみてください。
SQLModelとは
SQLModelは、 Pydantic と SQLAlchemy のいいとこ取りをしたPython ORMライブラリです。FastAPIの作者(tiangolo)が開発しており、以下の特徴があります:
- Pydanticの書き心地: バリデーション・型安全性をそのまま活用
- SQLAlchemyの互換性: 既存のSQLAlchemy知識がそのまま使える
- 1つのクラスで完結: Pydanticモデルとテーブル定義を統一
- FastAPIとの親和性: シームレスな統合
PythonのORMではまだまだSQLAlchemyが主流ですが、FastAPIの作者が作っていること、またSQLAlchemyのラッパーであり移行が簡単なので、今からWatchしておくといいかと思います。
実はFastAPIドキュメントのSQLの項目も、既にSQLModelでの実装に書き換わっています。
ちなみに筆者の環境では、シンプルな案件なら割とSQLModelを初手で選んでいます。
一部、極めて複雑なクエリが必要な場合などはSQLAlchemy生書きを選択することもありますが、大抵のユースケースはこれで足ります。
想定するユースケース
以下のようなLLMでの会話アプリケーションなどを想定して解説します。
ChatSession (LLM会話セッション)
└─ Message (会話履歴)
- ChatSession: LLMとの会話セッション
- Message: 会話の各メッセージ(user/assistant)
モダンPythonでいこう
「DBのモデル定義とAPIのスキーマ定義、二重管理するのだるすぎない?」
FastAPIを使っているなら、誰もが一度は思うはず。
SQLAlchemyのモデルを書いて、Pydanticのスキーマも書いて……マッピングして……。
SQLModelなら一発です。
まずPythonでBackendを書くとき、Pydanticはほぼ必須になってきています。
データはスキーマで保護しないとカオスになりやすいです。Typescriptの世界でも最近は Zod などスキーマの重要性が認識されてきています。
Pydanticとほぼ同じ内容をDBモデルに書くのはDRY(Don't Repeat Yourself)の原則から反するだけでなく、メンテナンス性の悪化やスキーマによる保護の効果を著しく減らすことになります。
SQLModelなら、DBモデル自体がPydanticモデルだから、バリデーションもガッツリ効くし、エディタの補完も爆速。PydanticとSQLAlchemyの融合でかつFastAPIとの相性もめっちゃいいので使わない手はないです。
1. なぜSQLModelなのか?
従来のやり方 (SQLAlchemy + Pydantic)
「DB保存用」と「API返却用」で、似たようなクラスを2回書く必要がありました。
# 1. DB用の定義 (SQLAlchemy)
class UserDB(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
age = Column(Integer)
# 2. API用の定義 (Pydantic)
class UserSchema(BaseModel):
id: int
name: str
age: int
「これ、name の定義変えたら両方修正するの? 正気?」
ってなりません? DRY原則どこいった?
SQLModelのやり方
1つのクラスで両方の役割を果たします。
from sqlmodel import Field, SQLModel
# これだけで、DBテーブル定義 兼 APIスキーマ定義
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
age: int
これが革命。
table=True をつければDBモデルになり、外せばただのPydanticモデルとして振る舞います。
この「継承元を変えなくていい」「二重管理しなくていい」が死ぬほどデカいです。
2. インストール
現代のPythonなら 非同期(Async) 一択。
SQLiteで非同期通信するために aiosqlite も入れておきます。
pip install sqlmodel aiosqlite
3. テーブル定義:これがPydanticに見えますか?
見えるよな? でもこれ、DBのテーブル定義なんだぜ。
LLMチャットアプリを想定して、「セッション(親)」と「メッセージ(子)」を定義します。
from datetime import datetime, timezone
from sqlmodel import Field, SQLModel, Relationship
# 親:チャットセッション
class ChatSession(SQLModel, table=True):
__tablename__ = "chat_sessions"
id: int | None = Field(default=None, primary_key=True)
title: str = Field(index=True)
model_name: str = Field(default="gpt-5.1")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# リレーション:このセッションに紐づくメッセージ一覧
messages: list["Message"] = Relationship(back_populates="session")
# 子:メッセージ
class Message(SQLModel, table=True):
__tablename__ = "messages"
id: int | None = Field(default=None, primary_key=True)
content: str
role: str # user or assistant
# 外部キー
session_id: int | None = Field(default=None, foreign_key="chat_sessions.id")
# 親への参照
session: ChatSession | None = Relationship(back_populates="messages")
ここが最高:
-
SQLModelを継承するだけでPydanticの機能が全部使えます。 -
FieldでDBのカラム制約(PK, Index, Foreign Key)を設定。 - 型ヒントがそのままDBの型になる(
str->VARCHARetc)。 - リレーションも
list["Message"]という型ヒントで定義可能。
4. DB接続と初期化
お決まりのセットアップです。
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
# SQLiteの非同期接続
DATABASE_URL = "sqlite+aiosqlite:///./database.db"
# echo=Trueで発行されたSQLがログに出る(開発中は必須)
engine = create_async_engine(DATABASE_URL, echo=True)
# DB初期化(テーブル作成)
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
5. CRUD操作:超直感的
SQLAlchemyの「あの長い呪文」はもういりません。
Create
Pydanticモデルをインスタンス化して add するだけ。バリデーションエラーがあればここで落ちます。
async def create_session(title: str):
async with AsyncSession(engine) as session:
# インスタンス化(ここで型チェックが走る!)
new_session = ChatSession(title=title)
session.add(new_session)
await session.commit()
await session.refresh(new_session) # IDが入った状態を取得
return new_session
Read
select 文もモダンで読みやすいです。
from sqlmodel import select
async def get_session(session_id: int):
async with AsyncSession(engine) as session:
statement = select(ChatSession).where(ChatSession.id == session_id)
result = await session.exec(statement)
return result.first()
6. 【上級編】セッション作成と同時にメッセージも突っ込む
SQLModel(SQLAlchemy)の便利なところ。
「親(セッション)を作るときに、子(初期プロンプト)もまとめて保存」ができます。
async def create_session_with_prompt(title: str, initial_prompt: str):
async with AsyncSession(engine) as session:
# 1. 子(メッセージ)を作る
# ID指定不要!
initial_msg = Message(content=initial_prompt, role="user")
# 2. 親を作りつつ、子を持たせる(リストに突っ込むだけ!)
new_session = ChatSession(
title=title,
messages=[initial_msg]
)
session.add(new_session)
await session.commit()
# ↑ これだけで sessionsテーブル と messagesテーブル 両方にINSERTが走る!
await session.refresh(new_session)
return new_session
7. FastAPIとの統合:これが真骨頂
ここがSQLModelを使う最大の理由です。
DBモデルをそのままレスポンスモデルとして使えます。
from typing import Annotated
from fastapi import FastAPI, Depends
app = FastAPI()
# 依存性注入用
async def get_session():
async with AsyncSession(engine) as session:
yield session
# ここがモダン!型定義と依存関係をセットにする(SessionDepパターン)
SessionDep = Annotated[AsyncSession, Depends(get_session)]
@app.post("/sessions/", response_model=ChatSession)
async def create_chat_session(
title: str,
session: SessionDep # ← 記述がスッキリ!
):
chat_session = ChatSession(title=title)
session.add(chat_session)
await session.commit()
await session.refresh(chat_session)
return chat_session
# ↑ ここ!DBモデルをそのまま返しても、FastAPIがPydanticモデルとして処理してくれる!
response_model=ChatSession と書くだけで、Swagger UIのドキュメントも自動生成されるし、不要なフィールドの除外もPydanticの設定で制御できます。
使い勝手はPydantic、でもモデルの定義やバリデーションもできる優れモノです。
まとめ:これからSQLModelがスタンダードになるかも?
-
二重管理の解消:
DBモデル=Pydanticスキーマこの快適さは戻れません。 - 型安全性: バリデーションと補完が開発速度をブーストさせます。
- 可読性: コードがスッキリして、何をやっているか一目瞭然。
- 便利: 親子関係の同時保存など、高度なDB操作も直感的。
ガードレールのない素のSQLや、型定義が曖昧なORMで消耗してる人がいればぜひ検討してほしいです。
SQLModel + Pydantic で、堅牢かつ爆速な開発体験を手に入れましょう。
Discussion