1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SQLAlchemyORM】v2系のモデル定義

1
Posted at

概要

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で型アノテーション

を使いましょう。

以上

  1. 宣言的

  2. 命令的

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?