概要
SQLAlchemyはv1→v2でモデルの書き方が変わっている。
(v1.4に至るまでも色々変更はあったと思うが経緯は知らない)
公式ドキュメントを読めば書いてあるし、GPT-5に聞くとちゃんとv2系の書き方で出してくれる。
ただ、いまだにブログ記事はv1系の内容が多くGPT-5以外だとv1系の書き方をするのと、v1系の書き方でも動いてしまい、非推奨になったことに気づかず使っている人もいそうなので、母数を増やす目的で書く。
公式ドキュメント
むかし(v1.4)
https://docs.sqlalchemy.org/en/14/orm/quickstart.html#declare-models
いま(v2.0)
https://docs.sqlalchemy.org/en/20/orm/quickstart.html#declare-models
What's new?(v1.4→v2.0)
https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#orm-declarative-models
書き方
v1.4
v1.4ではDeclarative1とImperative2の2つのマッピング方法がある
Declarativeの例(公式ママ)
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
__tablename__ = "user_account"
id = Column(Integer, primary_key=True)
name = Column(String(30))
fullname = Column(String)
addresses = relationship(
"Address", back_populates="user", cascade="all, delete-orphan"
)
def __repr__(self):
return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey("user_account.id"), nullable=False)
user = relationship("User", back_populates="addresses")
def __repr__(self):
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
Imperativeの例(公式ママ)
from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import registry
mapper_registry = registry()
user_table = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
class User:
pass
mapper_registry.map_imperatively(User, user_table)
コメント
DeclarativeもImperativeもregistryにオブジェクトとテーブルをマッピングしていくが、Declarativeがオブジェクトとテーブルを併せて宣言しマッピングしていくのに対して、Imperativeでは先にテーブルを定義して、どのテーブルとオブジェクトが対応するのかを命令的にマッピングするという違いがある。
Declarative mappingで使用するBase = declarative_base()は
mapper_registry = registry()
Base = mapper_registry.generate_base()
の省略形であることから、DeclarativeとImperativeが、作成したregistryをベースにオブジェクトとテーブルを紐付ける点では共通していることがわかる。
...The declarative_base() function is in fact shorthand for first creating the registry with the registry constructor, and then generating a base class using the registry.generate_base() method...
v1.4は型ヒントが脆弱という点で使いにくさがある。
例えば前述のモデル定義の下でUser.idという式はint型として解釈してほしいところだが、そのままだと(sqlalchemy.)Column[int]型として解釈されてしまうので、補完やエラー検出といったORMの長所を発揮しきれない。
なおMypyプラグインを導入することでこの事象は解消できる模様。
v2.0
v1.4のDeclarative mappingをベースにしつつdeclarative_base()の代わりにDeclarativeBase、Columnの代わりにmapped_columnを使用する。
また、カラムはMappedで型宣言する。
そこまで大きく変わらないし、全然難しくない。
例(公式ママ)
from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
- Base = declarative_base()
+ class Base(DeclarativeBase):
+ pass
class User(Base):
__tablename__ = "user_account"
- id = Column(Integer, primary_key=True)
+ id: Mapped[int] = mapped_column(primary_key=True) # 以下差分略
name: Mapped[str] = mapped_column(String(30))
fullname: Mapped[Optional[str]]
addresses: Mapped[List["Address"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
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")
def __repr__(self) -> str:
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
DeclarativeBaseを継承したBaseが内部的にregistryとMetaDataを保持しており、これを継承することでオブジェクトとテーブルをマッピングする。
Mappedを使用することでUser.idがちゃんとint型として解釈されるようになり、使いやすい。
まとめ
v2からは、
- declarative_base()ではなくDeclarativeBase
- Columnではなくmapped_column
- Mappedで型アノテーション
を使いましょう。
以上