💽

FastAPIの作者が作った「SQLModel」が革命的すぎるので、全Python使いに教えたい

に公開

この記事はLivetoon Tech Advent Calendar 2025の11日目の記事です。
https://adventar.org/calendars/12157

本日はCTOの私がよく使ってるSQLModelについてお話します。

宣伝

https://kai0.onelink.me/Hogh/AdventCalendar2025

今回のアドベントカレンダーでは、LivetoonのAIキャラクターアプリのkaiwaに関わるエンジニアが、アプリの話からLLM・合成音声・インフラ監視・GPU・OSSまで、幅広くアドベントカレンダーとして書いて行く予定です。
是非、publicationをフォローして、記事を追ってみてください。

SQLModelとは

https://sqlmodel.tiangolo.com/

SQLModelは、 PydanticSQLAlchemy のいいとこ取りをしたPython ORMライブラリです。FastAPIの作者(tiangolo)が開発しており、以下の特徴があります:

  • Pydanticの書き心地: バリデーション・型安全性をそのまま活用
  • SQLAlchemyの互換性: 既存のSQLAlchemy知識がそのまま使える
  • 1つのクラスで完結: Pydanticモデルとテーブル定義を統一
  • FastAPIとの親和性: シームレスな統合

PythonのORMではまだまだSQLAlchemyが主流ですが、FastAPIの作者が作っていること、またSQLAlchemyのラッパーであり移行が簡単なので、今からWatchしておくといいかと思います。

実はFastAPIドキュメントのSQLの項目も、既にSQLModelでの実装に書き換わっています。
https://fastapi.tiangolo.com/tutorial/sql-databases/

ちなみに筆者の環境では、シンプルな案件なら割と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 -> VARCHAR etc)。
  • リレーションも 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