SQLModel: SQLAlchemyとPydanticの二重管理を解消する「2-in-1」ソリューション

Python tutorial - IT technology blog
Python tutorial - IT technology blog

「モデルの二重定義」という悪夢

FastAPIとSQLAlchemyを使ってAPIを構築したことがあるなら、同じデータ構造を2回定義しなければならない煩わしさを経験したことがあるはずです。

まず、データベースにマッピングするためのSQLAlchemyクラスを作成します。その直後、入力データのバリデーションや出力JSONのフォーマットのために、ほとんど同じ内容のクラスをPydanticで作成しなければなりません。

# SQLAlchemyモデル(データベース用)
class UserDB(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String, index=True)
    email = Column(String, unique=True)

# Pydanticモデル(API用)
class UserOut(BaseModel):
    id: int
    username: str
    email: str

1つや2つのテーブルなら問題ありませんが、プロジェクトがスケールして50〜100個のテーブルになると、同期を維持することは非常に困難な作業になります。DB側でStringからTextにデータ型を変更したのにPydantic側の更新を忘れると、システムは即座にユーザーへ500エラーを返してしまいます。

なぜこれほど苦労するのか?

問題は、それぞれの設計思想の違いにあります。SQLAlchemyはオブジェクトをSQL文に変換することに特化しています。一方でPydanticは、データのパースや型変換、JSONの扱いに優れています。これら2つのライブラリは、本来お互いを理解するように設計されていません。

私が以前参加した実際のプロジェクトでは, データベースの30個のフィールドをリファクタリングするだけで午後いっぱいかかってしまいました。その時間のほとんどは、スキーマの不一致(schema mismatch)を避けるために、対応するPydanticクラスを探して修正することに費やされました。そこで必要になるのが「統合」です。

一般的な応急処置

SQLModelが登場する前、開発者は通常、次のいずれかの方法を選択していました:

  • 手動でのコピー&ペースト: 最初は速いですが、メンテナンスにおいては「時限爆弾」となります。
  • 変換ライブラリ(pydantic-sqlalchemyなど)の使用: それなりに動作しますが、Pydantic v2の新機能や複雑なリレーションシップ(relationship)に対応できないことがよくあります。

どちらの方法も根本的な解決にはなりません。同じエンティティに対して、2つの異なる「信頼できる唯一の情報源(Single Source of Truth)」を管理し続けているからです。

SQLModel:1つのクラスで2つの役割

SQLModelは、FastAPIの作者であるTiangolo氏によって生み出されました。このライブラリはSQLAlchemyとPydanticの両方を直接継承しています。その目的は明確です。1つのPythonクラスを、データベースモデルとデータ検証スキーマの両方として機能させることです。

クイックインストール

パッケージを1つインストールするだけです。SQLiteで素早くテストしたい場合は、複雑なドライバーを別途インストールする必要もありません。

pip install sqlmodel

「ハイブリッド」モデルの定義方法

魔法の鍵はtable=Trueという引数にあります。これにより、SQLModelはこのクラスをデータベースにマッピングする必要があることを認識します。

from typing import Optional
from sqlmodel import Field, SQLModel, create_engine, Session, select

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None

これでHeroは2つの役割を担います。FastAPIのリクエストボディからデータを受け取るために使用できると同時に、変換なしでそのままデータベースに保存することも可能です。

安全なデータ操作

実戦から得た教訓は、常にコンテキストマネージャ(withブロック)内でSessionを使用することです。これにより、処理が終わるとすぐに接続が閉じられ、コネクションリークによるデータベースのハングアップを防ぐことができます。

def create_hero():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
    
    with Session(engine) as session:
        session.add(hero_1)
        session.commit()
        session.refresh(hero_1) # DBから自動生成されたIDを取得
        print("新規ヒーロー:", hero_1)

実践的なモデルの階層化テクニック

データベースの全情報をそのままAPIに公開してはいけません。hashed_passwordのような機密情報は隠す必要があります。SQLModelは、これを解決するために非常に柔軟な継承をサポートしています。

私はセキュリティを確保するために、通常コードを以下の3つのレイヤーで構成します:

  1. Base: 誰にでも公開される共通フィールドを保持。
  2. Create: 登録時のみ使用するフィールド(パスワードなど)を保持。
  3. Table: 実際にDBにマッピングされるクラス。プライベートなフィールドを追加。
class HeroBase(SQLModel):
    name: str
    age: Optional[int] = None

class HeroCreate(HeroBase):
    password: str # クライアントからデータを受け取る時のみ使用

class Hero(HeroBase, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    hashed_password: str # DBに保存、APIには返さない

今すぐSQLModelを使うべき理由

もし新しいFastAPIプロジェクトを始めるなら、SQLModelを選んでください。ボイラープレートコードを最大50%削減できます。さらに重要なのは、VS Codeなどのエディタでのコード補完(IntelliSense)が非常にスムーズに動作することです。IDEが警告してくれないようなフィールド名のタイポに悩まされることはもうありません。

SQLModelは単にコードを短くするだけではありません。コードをよりクリーンで読みやすく、そして何より長期的なメンテナンスを容易にします。まずは小さなモジュールから試してみてください。その違いをすぐに実感できるはずです。

Share: