この記事は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をフォローして、記事を追ってみてください。
https://sqlmodel.tiangolo.com/
SQLModelは、Pydantic とSQLAlchemy のいいとこ取りをしたPython ORMライブラリです。FastAPIの作者(tiangolo)が開発しており、以下の特徴があります:
PythonのORMではまだまだSQLAlchemyが主流ですが、FastAPIの作者が作っていること、またSQLAlchemyのラッパーであり移行が簡単なので、今からWatchしておくといいかと思います。
実はFastAPIドキュメントのSQLの項目も、既にSQLModelでの実装に書き換わっています。
https://fastapi.tiangolo.com/tutorial/sql-databases/
ちなみに筆者の環境では、シンプルな案件なら割とSQLModelを初手で選んでいます。
一部、極めて複雑なクエリが必要な場合などはSQLAlchemy生書きを選択することもありますが、大抵のユースケースはこれで足ります。
以下のようなLLMでの会話アプリケーションなどを想定して解説します。
ChatSession (LLM会話セッション) └─ Message (会話履歴)「DBのモデル定義とAPIのスキーマ定義、二重管理するのだるすぎない?」
FastAPIを使っているなら、誰もが一度は思うはず。
SQLAlchemyのモデルを書いて、Pydanticのスキーマも書いて……マッピングして……。
SQLModelなら一発です。
まずPythonでBackendを書くとき、Pydanticはほぼ必須になってきています。
データはスキーマで保護しないとカオスになりやすいです。Typescriptの世界でも最近はZod などスキーマの重要性が認識されてきています。
Pydanticとほぼ同じ内容をDBモデルに書くのはDRY(Don't Repeat Yourself)の原則から反するだけでなく、メンテナンス性の悪化やスキーマによる保護の効果を著しく減らすことになります。
SQLModelなら、DBモデル自体がPydanticモデルだから、バリデーションもガッツリ効くし、エディタの補完も爆速。PydanticとSQLAlchemyの融合でかつFastAPIとの相性もめっちゃいいので使わない手はないです。
「DB保存用」と「API返却用」で、似たようなクラスを2回書く必要がありました。
# 1. DB用の定義 (SQLAlchemy)classUserDB(Base): __tablename__="users"id= Column(Integer, primary_key=True) name= Column(String, nullable=False) age= Column(Integer)# 2. API用の定義 (Pydantic)classUserSchema(BaseModel):id:int name:str age:int「これ、name の定義変えたら両方修正するの? 正気?」
ってなりません? DRY原則どこいった?
1つのクラスで両方の役割を果たします。
from sqlmodelimport Field, SQLModel# これだけで、DBテーブル定義 兼 APIスキーマ定義classUser(SQLModel, table=True):id:int|None= Field(default=None, primary_key=True) name:str age:intこれが革命。table=True をつければDBモデルになり、外せばただのPydanticモデルとして振る舞います。
この「継承元を変えなくていい」「二重管理しなくていい」が死ぬほどデカいです。
現代のPythonなら非同期(Async) 一択。
SQLiteで非同期通信するためにaiosqlite も入れておきます。
pipinstall sqlmodel aiosqlite見えるよな? でもこれ、DBのテーブル定義なんだぜ。
LLMチャットアプリを想定して、「セッション(親)」と「メッセージ(子)」を定義します。
from datetimeimport datetime, timezonefrom sqlmodelimport Field, SQLModel, Relationship# 親:チャットセッションclassChatSession(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")# 子:メッセージclassMessage(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)を設定。str ->VARCHAR etc)。list["Message"] という型ヒントで定義可能。お決まりのセットアップです。
from sqlmodel.ext.asyncio.sessionimport AsyncSessionfrom sqlalchemy.ext.asyncioimport create_async_engine# SQLiteの非同期接続DATABASE_URL="sqlite+aiosqlite:///./database.db"# echo=Trueで発行されたSQLがログに出る(開発中は必須)engine= create_async_engine(DATABASE_URL, echo=True)# DB初期化(テーブル作成)asyncdefinit_db():asyncwith engine.begin()as conn:await conn.run_sync(SQLModel.metadata.create_all)SQLAlchemyの「あの長い呪文」はもういりません。
Pydanticモデルをインスタンス化してadd するだけ。バリデーションエラーがあればここで落ちます。
asyncdefcreate_session(title:str):asyncwith AsyncSession(engine)as session:# インスタンス化(ここで型チェックが走る!) new_session= ChatSession(title=title) session.add(new_session)await session.commit()await session.refresh(new_session)# IDが入った状態を取得return new_sessionselect 文もモダンで読みやすいです。
from sqlmodelimport selectasyncdefget_session(session_id:int):asyncwith AsyncSession(engine)as session: statement= select(ChatSession).where(ChatSession.id== session_id) result=await session.exec(statement)return result.first()SQLModel(SQLAlchemy)の便利なところ。
「親(セッション)を作るときに、子(初期プロンプト)もまとめて保存」ができます。
asyncdefcreate_session_with_prompt(title:str, initial_prompt:str):asyncwith 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ここがSQLModelを使う最大の理由です。
DBモデルをそのままレスポンスモデルとして使えます。
from typingimport Annotatedfrom fastapiimport FastAPI, Dependsapp= FastAPI()# 依存性注入用asyncdefget_session():asyncwith AsyncSession(engine)as session:yield session# ここがモダン!型定義と依存関係をセットにする(SessionDepパターン)SessionDep= Annotated[AsyncSession, Depends(get_session)]@app.post("/sessions/", response_model=ChatSession)asyncdefcreate_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、でもモデルの定義やバリデーションもできる優れモノです。
DBモデル =Pydanticスキーマこの快適さは戻れません。ガードレールのない素のSQLや、型定義が曖昧なORMで消耗してる人がいればぜひ検討してほしいです。SQLModel +Pydantic で、堅牢かつ爆速な開発体験を手に入れましょう。

趣味はAIモデル開発Livetoon CTO | 元医者、現AIエンジニア | 医療LLM専門家 | 東大病院AIチーム所属 | FastAPIとRAGのオタク | 最高峰アジア人画像生成モデル: longisland3XL | Livetoon TTS: 最高クラスの音声合成AI