Claude Media
Claude CodeでSQLAlchemyのモデルを実装する手順

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つです。

  • 型ヒント(Mapped[Optional[...]]のような注釈)の基本文法
  • SQLAlchemy 2.0系または2.1系。Python 3.11以上ではpip install SQLAlchemyが最新の2.1系を入れるので、2.0系にとどめるなら<2.1を付けます
pip install SQLAlchemy
pip install "SQLAlchemy<2.1"
くらべる

どちらの系列が入るか

2.1系

指定なし

2.1.0は2026年9月24日の公開で、PyPI上の対応Pythonは3.11以上です。2.1系自体はPython 3.10以前に入らず、その環境では指定なしでも2.0系(2.0.54)が入ります。

2.0系

2.1系を除外する指定

2.0系の最新は2.0.54(2026年9月15日公開)で、PyPI上の対応Pythonは3.7以上です。

本記事の出力はSQLAlchemy 2.1.3(Python 3.12.2)で実行したものです。back_populatesと1対1の挙動は2.0.54でも同じ結果でした。

CLAUDE.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の書き間違いで起きること

エラーなし

片側だけに書く

User.addressesにだけback_populates="user"を書き、Address.userには書かない構成でも、マッピングは通ります。u.addresses.append(a)ならa.userがuになりますが、b.user = uと代入してもbはu.addressesに入りません。

エラー

属性名を間違える

back_populates="owner"のように相手側に無い名前を書くと、InvalidRequestError: Mapper 'Mapper[Address(address)]' has no property 'owner'.でマッピングが止まります。

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を付けない上の例では、同じUserを指すUserProfileを2行登録でき、u.profileにアクセスするとSAWarning: Multiple rows returned with uselist=False for lazily-loaded attribute 'User.profile'が出ました。同じ親を参照する行が複数登録されるのを防ぐには、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スタイルで」と明示し、入れる系列(2.0系か2.1系か)も伝えます。1.x系のColumn()直書きスタイルは2.0系でも動きますが、mapped_column()はMypyやPylanceなどの型ツールに属性の実際の振る舞いを伝える目的で導入されたものです
  • テーブル名の命名規則: __tablename__をどう決めるか(単数形か複数形か、スネークケースかなど)はSQLAlchemy側が強制しないため、既存のテーブルがあるならその命名規則を指示文に書いておきます
  • nullable列の扱い: 「この列はNULLを許可する」という業務要件は、Optional[...]を付けるかどうかにそのまま直結します。要件を伝えずに書かせると、既定でNOT NULLになった列があとから要件と食い違うことがあります

生成されたモデルは、CreateTableでCREATE TABLE文を出力させると、列の型・NULL制約・外部キーが意図どおりかをコードを実行するだけで確認できます。

from sqlalchemy.schema import CreateTable
 
print(CreateTable(User.__table__))

UserとAddressの出力は次のとおりです。Optional[str]のfullnameだけがNOT NULLなしで、user_idには外部キーが付いています。

CREATE TABLE user_account (
	id INTEGER NOT NULL,
	name VARCHAR(30) NOT NULL,
	fullname VARCHAR,
	PRIMARY KEY (id)
)
 
CREATE TABLE address (
	id INTEGER NOT NULL,
	email_address VARCHAR NOT NULL,
	user_id INTEGER NOT NULL,
	PRIMARY KEY (id),
	FOREIGN KEY(user_id) REFERENCES user_account (id)
)

モデル定義を実行して確認する

モデルを定義しただけでは、列名や型が正しいかはまだ分かりません。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文を組み立てて実行します。

手順

生成されたモデルを実行で確かめる流れ

  1. 1

    テーブルを作ってDDLを読む

    echo=Trueで流れるCREATE TABLEが、上のCreateTableの出力と一致するかを見ます。

  2. 2

    1件書いて読み戻す

    addとcommitのあとselect()で取得し直し、列名と型が意図どおりに往復するかを見ます。

  3. 3

    コレクションを動かす

    Addressを2件持つUserをcommitするとaddressは2行です。コレクションから1件pop()してcommitすると1行、session.delete(user)のあとは0行でした(ステップ2のcascade="all, delete-orphan"の構成)。

型注釈の見た目だけでは気づきにくい設定ミスも、この流れをClaude Codeに実行させると、実行時の出力として早い段階で拾えます。

よくあるつまずき

  • 循環参照する型名を文字列にし忘れる: Mapped["User"]のように、まだ定義されていないクラスを参照するときは文字列(前方参照)にします。Mapped[User]と書くと、Userが定義済みでない限りNameError: name 'User' is not definedになります
  • 多対多の中間テーブルをmapped_column()で定義してしまう: 中間テーブルはORMのマッピング対象ではなくCore側のテーブルなので、ColumnとTableで定義します。Table()の中にmapped_column()を渡すと、ArgumentError: 'SchemaItem' object, such as a 'Column' or a 'Constraint' expected, got <...MappedColumn object ...>で止まります
  • Optionalを付け忘れてnullable要件と食い違う: Mapped[str]はNOT NULL、Mapped[Optional[str]]はNULL許可という対応です。型注釈を業務要件と照らし合わせずに書かせると、あとから列制約を直す手間が発生します
  • 旧スタイルのColumn()直書きが混ざる: id = Column(Integer, primary_key=True)という1.x系の書き方自体は2.0系でも動きますが、Mapped注釈を併用するスタイルとは書き方が異なります。同じファイル内で混在していないか確認します

リレーションシップの使い分け早見表

4つのリレーションシップパターンは、外部キーの置き場所と型注釈の形で見分けられます。

パターン外部キーの位置型注釈の形典型的な用途
1対多外部キーの位置「多」側のテーブル型注釈の形「1」側: Mapped[List["X"]]典型的な用途親1件に子データが複数ぶら下がる通常のケース
多対1外部キーの位置自分の属するテーブル型注釈の形自分側: Mapped["X"](任意ならMapped[Optional["X"]])典型的な用途複数の行が同じ参照先を共有するケース
1対1外部キーの位置どちらか一方型注釈の形双方とも非コレクション型: Mapped["X"]典型的な用途詳細情報を別テーブルへ分割するケース
多対多外部キーの位置中間テーブル(secondary)型注釈の形双方: Mapped[List["X"]]典型的な用途タグ付けなど双方が複数件を持ち合うケース

多対1で参照先が任意なら、外部キー列をMapped[Optional[int]]にし、relationship()もMapped[Optional["X"]]にそろえます。参照先が必須なら両方ともOptionalなしです。

まとめ

SQLAlchemy 2.0のモデル定義は、DeclarativeBaseを継承した基底クラスと、Mapped型注釈・mapped_column()の組み合わせが骨格です。Claude Codeにモデルを書かせるときは、2.0スタイルであることとnullableの業務要件を先に伝えておくと、生成されたコードを見てからの手直しが減ります。生成後は、back_populatesの両側の対応と、1対1にunique=Trueが付いているかを見ておくと、エラーにならないまま残る不整合に気づけます。

Pythonの型ヒントをそのままスキーマや契約として使う設計は、SQLAlchemyのMapped型注釈と発想が近く、MCP Python SDKのデコレータでツールを定義する実装パターンでも同様の活用パターンを扱っています。モデルを書いたあと、そのモデルを操作するテストが失敗したときの切り分けはpytestの失敗をMCPサーバーでClaudeに解析させるが参考になります。モデルの変更をDBへ反映する手順はAlembicのマイグレーションにあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.sqlalchemy.org/en/20/tutorial
  2. docs.sqlalchemy.org/en/20/orm/quickstart.html
  3. docs.sqlalchemy.org/en/20/orm/basic_relationships.html
  4. docs.sqlalchemy.org/en/20/orm/declarative_tables.html
  5. docs.sqlalchemy.org/en/20/orm/declarative_config.html
  6. pypi.org/project/SQLAlchemy
この記事を共有:XはてブLinkedIn