Claude CodeでSQLAlchemyのモデルを実装する手順
SQLAlchemy 2.0のDeclarativeスタイルで、Claude CodeにMapped型注釈付きのORMモデルを書かせる手順です。列・外部キー・リレーションシップの実装パターンと、生成コードのチェックポイントをまとめます。
SQLAlchemyはPythonの代表的なO/Rマッパー(ORM)兼SQLツールキットです。2.0系では、DeclarativeBaseを継承した基底クラスと、Mapped型注釈・mapped_column()の組み合わせでモデルを定義するスタイルが標準になりました。Claude Codeにモデルを書かせる際は、このスタイルであることと外部キー・リレーションシップの向きを指示に含めておくと、生成後の手直しが減ります。本記事では列定義からリレーションシップの実装、指示に含めておく情報までをコード例つきでまとめます。
Claude CodeでSQLAlchemyのモデルを書く前提
SQLAlchemyは2.0系で書き方が大きく変わり、DeclarativeBase・Mapped・mapped_column()を使う書き方が標準です。Claude Codeにモデルを書かせるときも、この2.0スタイルを明示しないと、1.x系のColumn()直書きスタイルが混ざることがあります。前提は次の2つです。
- Python 3.9以上(
Mapped[Optional[...]]のような型注釈を使うため、型ヒントの基本文法を理解している前提です) pip install SQLAlchemyでインストールしたSQLAlchemy 2.0系(本記事のコード例は2.0.54、2026年9月15日リリースのドキュメントに基づいています)
pip install SQLAlchemyCLAUDE.mdに「SQLAlchemy 2.0のMapped/mapped_columnスタイルで書く。Column()の直接インスタンス化は使わない」のような1行を書いておくと、モデルファイルを新規生成するたびに同じスタイルを指定し直さずに済みます。CLAUDE.mdの実用的な書き方はClaude CodeのCLAUDE.mdを実用に引き上げる10のパターンにまとめています。
ステップ1: DeclarativeBaseとMapped/mapped_columnでモデルを定義する
モデル定義の起点は、DeclarativeBaseを継承した基底クラスです。この基底クラスを1つ作り、すべてのモデルクラスがそれを継承する構成がSQLAlchemy公式チュートリアルの標準形です。
from typing import List, Optional
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30))
fullname: Mapped[Optional[str]]
def __repr__(self) -> str:
return f"User(id={self.id!r}, name={self.name!r})"各属性はMapped[...]という型注釈とmapped_column()の組み合わせで宣言します。列の型はPythonの型から自動的に決まり、intはINTEGER、strはVARCHARに対応します。Optional[str]のようにOptionalで包むと、その列はNULLを許可する設定になります。Mapped[str]のようにOptionalを付けなければ、NOT NULL制約が付きます。
String(30)のようにmapped_column()側に型オブジェクトを渡すと、文字列長などの詳細な制約を指定できます。指定しなければ、Pythonの型から推測される既定の型がそのまま使われます。主キーにする列にはmapped_column(primary_key=True)を指定します。1つのモデルには、主キー列が最低1つ必要です。
ステップ2: 外部キーとリレーションシップを実装する
複数のモデル間の関係は、ForeignKeyとrelationship()の組み合わせで表現します。方向性によって書き方が変わるため、まず1対多から見ます。
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(primary_key=True)
email_address: Mapped[str]
user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
user: Mapped["User"] = relationship(back_populates="addresses")
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30))
addresses: Mapped[List["Address"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)外部キーは「多」側のテーブルに置きます。上の例ではAddress.user_idがuser_account.idを参照し、User.addressesがMapped[List["Address"]]というコレクション型のリレーションシップになります。双方向にたどれるようにするには、relationship()のback_populates引数に相手側の属性名を指定します。片方だけにback_populatesを書くと、マッピング時にエラーになります。
cascade="all, delete-orphan"を親側のrelationship()に指定すると、親レコードを削除したときに子レコードも連動して削除されます。コレクションから子オブジェクトを取り除いたとき、その子レコードが自動的に削除される点がdelete-orphanの役割です。
一方が単一の相手しか持たない場合は、relationship()の型注釈を非コレクション型にするだけで、SQLAlchemy側が1対1として扱います。
class UserProfile(Base):
__tablename__ = "user_profile"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
bio: Mapped[Optional[str]]
user: Mapped["User"] = relationship(back_populates="profile")
class User(Base):
# ...(前述の属性に続けて)
profile: Mapped["UserProfile"] = relationship(back_populates="user")1対1はSQLAlchemyがMapped注釈から読み取る規約であって、データベース側の一意制約を自動で作るわけではありません。同じ親を参照する行が複数登録されるのを防ぎたい場合は、user_id列にunique=Trueを別途指定します。
双方が複数件を持ち合う多対多は、間に中間テーブルを1つ挟みます。中間テーブルはmapped_column()ではなく、Core側のTableとColumnで定義する点がほかのパターンと異なります。
from sqlalchemy import Column, Table
post_tags = Table(
"post_tags",
Base.metadata,
Column("post_id", ForeignKey("post.id"), primary_key=True),
Column("tag_id", ForeignKey("tag.id"), primary_key=True),
)
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(primary_key=True)
tags: Mapped[List["Tag"]] = relationship(secondary=post_tags, back_populates="posts")
class Tag(Base):
__tablename__ = "tag"
id: Mapped[int] = mapped_column(primary_key=True)
posts: Mapped[List["Post"]] = relationship(secondary=post_tags, back_populates="tags")中間テーブルの2列について、公式ドキュメントは「主キー制約またはユニーク制約にしておくことを推奨する」としています。必須ではありませんが、アプリケーション側の不備で同じ組み合わせが重複登録されるのを、テーブル側で防げます。
ステップ3: 重複する列定義をAnnotatedでまとめる
id: Mapped[int] = mapped_column(primary_key=True)のような列定義は、モデルが増えるほど同じ記述が繰り返されます。typing.Annotatedで型とmapped_column()をセットにした別名を作ると、モデル本体からは型注釈だけで済むようになります。
import datetime
from typing import Annotated
from sqlalchemy import func
from sqlalchemy.orm import mapped_column
intpk = Annotated[int, mapped_column(primary_key=True)]
timestamp = Annotated[
datetime.datetime,
mapped_column(nullable=False, server_default=func.CURRENT_TIMESTAMP()),
]
class Post(Base):
__tablename__ = "post"
id: Mapped[intpk]
created_at: Mapped[timestamp]Claude Codeに複数のモデルファイルをまとめて書かせるときは、こうした共通の型エイリアスを先に1ファイル(db/types.pyなど)にまとめて渡しておくと、モデルごとに主キーやタイムスタンプの定義がぶれません。
Claude Codeへの指示に含めておく情報
プロンプトだけで「SQLAlchemyのモデルを書いて」と頼むと、バージョンやスタイルの前提が省略されたまま実装が進みます。次の3点は、モデルを新規に書かせるときの指示に含めておくと手戻りが減ります。
- SQLAlchemyのバージョン系列: 「2.0のMapped/mapped_columnスタイルで」と明示します。1.x系の
Column()直書きスタイルは今も動きますが、型チェッカーとの相性やAnnotatedとの組み合わせは2.0系のスタイルが前提です - テーブル名の命名規則:
__tablename__をどう決めるか(単数形か複数形か、スネークケースかなど)はSQLAlchemy側が強制しないため、既存のテーブルがあるならその命名規則を指示文に書いておきます - nullable列の扱い: 「この列はNULLを許可する」という業務要件は、
Optional[...]を付けるかどうかにそのまま直結します。要件を伝えずに書かせると、既定でNOT NULLになった列があとから要件と食い違うことがあります
生成されたモデルは、CreateTableでCREATE TABLE文を出力させると、列の型・NULL制約・外部キーが意図どおりかをコードを実行するだけで確認できます。
from sqlalchemy.schema import CreateTable
print(CreateTable(User.__table__))モデル定義を実行して確認する
モデルを定義しただけでは、列名や型が正しいかはまだ分かりません。SQLite上のインメモリDBに対してテーブルを実際に作成し、値を1件書き込んで読み出すところまで実行すると、Mappedの型注釈どおりのテーブルが作られているかをコード実行だけで検証できます。
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
engine = create_engine("sqlite://", echo=True)
Base.metadata.create_all(engine)
with Session(engine) as session:
user = User(name="taro", fullname="Taro Yamada")
session.add(user)
session.commit()
stmt = select(User).where(User.name == "taro")
print(session.scalars(stmt).one())create_engine("sqlite://", echo=True)は、SQLite上のインメモリDBへの接続を作ります。echo=Trueを指定すると、発行されるSQLが標準出力に流れます。Base.metadata.create_all(engine)が、これまでに定義したすべてのモデルクラスからCREATE TABLE文を組み立てて実行します。この時点で外部キー制約やNOT NULL制約が意図どおりかは、標準出力に流れるDDLをそのまま確認すれば分かります。
Sessionでオブジェクトをadd・commitし、select()で取得し直すところまで一度動かしておくと、リレーションシップのback_populatesが正しく設定されているかどうかも、実際にコレクションへアクセスして確かめられます。Claude Codeに生成させたモデルをレビューするときは、この一連の流れをそのまま実行させると、型注釈の見た目だけでは気づきにくい設定ミスを、実行時のエラーとして早い段階で拾えます。
よくあるつまずき
back_populatesの対応漏れ: 片方のrelationship()にだけback_populatesを書き、もう片方に対応する属性名を書き忘れると、マッピング時にエラーになります。リレーションシップを追加・変更したときは、両側の属性名を確認します- 循環参照する型名を文字列にし忘れる:
Mapped["Address"]のように、まだ定義されていないクラスを参照するときは文字列(前方参照)にする必要があります。Mapped[Address]と書くと、クラスの定義順によってはNameErrorになります - 多対多の中間テーブルを
mapped_column()で定義してしまう: 中間テーブルはORMのマッピング対象ではなくCore側のテーブルなので、ColumnとTableで定義します。mapped_column()を使うとモデルクラスとして扱われ、想定した多対多になりません Optionalを付け忘れてnullable要件と食い違う:Mapped[str]はNOT NULL、Mapped[Optional[str]]はNULL許可という対応です。型注釈を業務要件と照らし合わせずに書かせると、あとから列制約を直す手間が発生します- 旧スタイルの
Column()直書きが混ざる:id = Column(Integer, primary_key=True)という1.x系の書き方自体は2.0系でも動きますが、Mappedによる型注釈が付かないため、IDEの補完や型チェッカーの恩恵を受けられません。同じファイル内でスタイルが混在していないか確認します
リレーションシップの使い分け早見表
4つのリレーションシップパターンは、外部キーの置き場所と型注釈の形で見分けられます。
| パターン | 外部キーの位置 | 型注釈の形 | 典型的な用途 |
|---|---|---|---|
| 1対多 | 外部キーの位置「多」側のテーブル | 型注釈の形「1」側: Mapped[List["X"]] | 典型的な用途親1件に子データが複数ぶら下がる通常のケース |
| 多対1 | 外部キーの位置自分の属するテーブル | 型注釈の形自分側: Mapped["X"](任意ならMapped[Optional["X"]]) | 典型的な用途複数の行が同じ参照先を共有するケース |
| 1対1 | 外部キーの位置どちらか一方 | 型注釈の形双方とも非コレクション型: Mapped["X"] | 典型的な用途詳細情報を別テーブルへ分割するケース |
| 多対多 | 外部キーの位置中間テーブル(secondary) | 型注釈の形双方: Mapped[List["X"]] | 典型的な用途タグ付けなど双方が複数件を持ち合うケース |
多対1のnullable制約は、外部キー列とrelationship()の両方にOptionalを付けるかどうかで決まります。参照先が必須なら両方ともOptionalなし、任意なら両方にOptionalを付ける対応関係です。
まとめ
SQLAlchemy 2.0のモデル定義は、DeclarativeBaseを継承した基底クラスと、Mapped型注釈・mapped_column()の組み合わせが骨格です。Claude Codeにモデルを書かせるときは、2.0スタイルであることとnullableの業務要件を先に伝えておくと、生成されたコードを見てからの手直しが減ります。リレーションシップは1対多・多対1・1対1・多対多で外部キーの置き場所と型注釈の形が変わるため、back_populatesの対応漏れと循環参照の文字列化忘れの2点は生成後に確認します。
Pythonの型ヒントをそのままスキーマや契約として使う設計は、SQLAlchemyのMapped型注釈と発想が近く、MCP Python SDKのデコレータでツールを定義する実装パターンでも同様の活用パターンを扱っています。モデルを書いたあと、そのモデルを操作するテストが失敗したときの切り分けはpytestの失敗をMCPサーバーでClaudeに解析させるが参考になります。