プログラミングを頑張る土木系専攻大学院生のブログ

主にプログラミングについて開発備忘録的な形で投稿しています。

SQLModelの記述方法~1対多と多対多のテーブルの関連付け方法~

はじめに~SQLModelとは

FastAPIでよく使われるSQLのライブラリの中でも、比較的簡単に記述できるSQLModelについてご紹介します。

SQLModelは、FastAPIの作者であるtiangolo氏が開発した、SQLAlchemyとPydanticの良いとこ取りをしたPython用ORMライブラリです。

今回使用するライブラリ

  • FastAPI
  • SQLModel
  • その他: typing, datetime(標準ライブラリ)

必要に応じて、uvicornsqlalchemyもプロジェクトに追加してください。


SQLModelの特徴

  • SQLAlchemyベース
    → SQLAlchemyのORM機能やDB接続機能をそのまま活用可能。

  • Pydanticと統合
    → 型ヒントとバリデーションがPydanticの書き方で可能。APIとDBモデルの共通化に優れる。

  • シンプルな記述
    → 最小限のコードでテーブル設計ができ、FastAPIとの親和性が高い。


モデル定義例

from sqlmodel import SQLModel, Field

class Ingredient(SQLModel, table=True):
    id: int = Field(default=None, primary_key=True)
    name: str
    reading: str
    type: str

こんな方におすすめ

  • FastAPIでAPIとDBモデルを一元管理したい方
  • 型安全・バリデーションを重視したい方
  • SQLAlchemyの複雑な記法が苦手な方

注意点

  • 複雑なDB設計には不向き。あくまで「シンプルな用途向け」
  • 既存のSQLAlchemyプロジェクトへの導入は慎重に検討が必要

まとめ

SQLModelは、FastAPIでAPIとDBを型安全に、簡潔に扱いたい方に非常に便利なライブラリです。 新規にFastAPI + SQLiteなどで構築する際には、SQLModelを採用するのがおすすめです。


今回作成するモデル構成

各テーブルの説明

食材テーブル (ingredients)

  • id: 主キー
  • name: 食材名
  • reading: 読み方
  • type: 種類(選択肢形式)

レシピテーブル (recipes)

  • id: 主キー
  • name: レシピ名
  • url: YouTubeのURL
  • thumbnail: サムネイル画像URL
  • notes: 備考
  • created_at: 作成日時
  • updated_at: 更新日時

中間テーブル(レシピ × 食材)recipe_ingredients

  • recipe_id: レシピID(外部キー)
  • ingredient_id: 食材ID(外部キー)

カテゴリテーブル (categories)

  • id: 主キー
  • name: カテゴリ名(例: パン・麺類、主菜、副菜 等)

実際に記述していく

ステップ1:まずは関係性はとりあえず無視して個別にテーブルを定義

from sqlmodel import SQLModel, Field
from typing import Optional

class Ingredient(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    reading: str = Field(max_length=20, description="食材の読み")
    type: str

class Category(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

class Recipe(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(max_length=30, unique=True)
    url: str = Field(max_length=255, unique=True)
    thumbnail: str = Field(max_length=255, unique=True, description="サムネイル画像のURL")
    notes: str | None = Field(default=None, nullable=True)
    created_at: str | None = Field(default=None)
    updated_at: str | None = Field(default=None)

Fieldで使う主な引数

  • default: デフォルト値
  • default_factory: デフォルト値を生成する関数
  • primary_key: 主キー指定
  • nullable: NULL許容
  • unique: 一意制約
  • index: インデックス作成
  • foreign_key: 外部キー指定("テーブル名.カラム名")
  • max_length, min_length: 文字列長制約
  • description, title: 説明・タイトル
  • gt, ge, lt, le: 数値の制約(不等号)

ステップ2 1対多および多対多の関係性を記述

多対多の関係(レシピ×食材)

・多対多の関係の場合、中間テーブルを記述する ・個別のテーブルにRelationshipでlink_modelを指定

中間テーブル

class RecipeIngredientLink(SQLModel, table=True):
    recipe_id: int = Field(default=None, foreign_key="recipe.id", primary_key=True)
    ingredient_id: int = Field(default=None, foreign_key="ingredient.id", primary_key=True)

Relationshipでlink_modelを指定 例:

recipes: List["Recipe"] = Relationship(back_populates="ingredients", link_model=RecipeIngredientLink) # 多対多の関係

リレーション定義

class Ingredient(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    reading: str = Field(max_length=20, description="食材の読み")
    type: str

    recipes: List["Recipe"] = Relationship(back_populates="ingredients", link_model=RecipeIngredientLink)

class Recipe(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(max_length=30, unique=True)
    url: str = Field(max_length=255, unique=True)
    thumbnail: str = Field(max_length=255, unique=True, description="サムネイル画像のURL")
    notes: str | None = Field(default=None, nullable=True)
    created_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
    updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, onupdate=datetime.utcnow)

    ingredients: List[Ingredient] = Relationship(back_populates="recipes", link_model=RecipeIngredientLink)

1対多の関係(カテゴリ×レシピ)

1対多の関係はそのまま記述できる

  • 子側(多側)に外部キー(category_id)を持たせる
  • 親側(1側)にRelationshipでリストを持たせる
class Category(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

    recipes: List["Recipe"] = Relationship(back_populates="category")

class Recipe(SQLModel, table=True):
    # (前略)
    category_id: Optional[int] = Field(default=None, foreign_key="category.id")
    category: Optional[Category] = Relationship(back_populates="recipes")

最終的な完成形

from sqlmodel import SQLModel, Field, Relationship
from typing import List, Optional
from datetime import datetime

class RecipeIngredientLink(SQLModel, table=True):
    recipe_id: int = Field(default=None, foreign_key="recipe.id", primary_key=True)
    ingredient_id: int = Field(default=None, foreign_key="ingredient.id", primary_key=True)

class Ingredient(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    reading: str = Field(max_length=20, description="食材の読み")
    type: str

    recipes: List["Recipe"] = Relationship(back_populates="ingredients", link_model=RecipeIngredientLink)

class Category(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

    recipes: List["Recipe"] = Relationship(back_populates="category")

class Recipe(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(max_length=30, unique=True)
    url: str = Field(max_length=255, unique=True)
    thumbnail: str = Field(max_length=255, unique=True, description="サムネイル画像のURL")
    notes: str | None = Field(default=None, nullable=True)
    created_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
    updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, onupdate=datetime.utcnow)

    ingredients: List[Ingredient] = Relationship(back_populates="recipes", link_model=RecipeIngredientLink)
    category_id: Optional[int] = Field(default=None, foreign_key="category.id")
    category: Optional[Category] = Relationship(back_populates="recipes")

おわりに

本記事では、FastAPIと親和性の高いSQLModelを用いて、1対多・多対多のテーブル設計をどのように記述するかを実例とともに解説しました。

初学者でも手軽に実装でき、型安全かつ拡張性のあるDB設計が可能です。ぜひ、実際のプロジェクトで活用してみてください。