SQLAlchemyによるテーブル定義

2024/5/12

SQLAlchemy

はじめに

本記事では、SQLAlchemyによるテーブル定義について記述しています。

公式に載っている記法で書いてみました。(2024/4 現在)

おまけとして、実践的な書き方ついても紹介してるので最後まで見ていってください。

SQLAlchemyとは

Pythonで広く使用されるオープンソースのSQLツールキットおよびORMで、

SQL文を書かずにデータベースを簡単に操作できるようにするライブラリです。

学べること

  • SqlAlchemyによるテーブル定義の方法
  • 実践的なカラムの定義方法

テーブル構成

今回は、例として以下テーブルを作成していきます。

テーブル定義

基底クラスの定義

SqlAlchemyのDeclarativeBaseと呼ばれるクラスを継承して、BaseというDB Modelの基底クラスを定義しています。

Baseクラスを継承することで、クラス内でDBのテーブルを定義できます。

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

DBモデルの定義

上記の基底クラスを継承して、実際にUserテーブルを作成していきます。

from sqlalchemy.orm import relationship, Mapped, mapped_column, DeclarativeBase
from sqlalchemy import String, DateTime, Uuid
from datetime import datetime
from sqlalchemy.sql import text
import uuid

# Modelの基底クラスの定義
class Base(DeclarativeBase):
    pass

# Userテーブル
class User(Base):
    __tablename__ = "user"

    id: Mapped[uuid.UUID] = mapped_column(
        Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="ユーザーID"
    )
    email: Mapped[str] = mapped_column(String(255), unique=True, comment="メールアドレス")
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("NOW()"), comment="作成日時", 
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=text("NOW()"),
        onupdate=text("NOW()"),
        comment="更新日時",
    )
  • tablename:テーブル名の指定
  • Mapped[type]:テーブルカラムの型注釈(str, int等)を指定。詳しくは後述します。
  • mapped_column:テーブルカラムを定義するための関数

mapped_column()の引数について

  • type:型を指定。StringIntegerUuid等を指定可能。

    他にも色々あるので、気になる方は、こちらを確認してください。

  • primary_key[True/False]:TrueにするとPrimary Keyとなる。(default: False)
  • unique[True/False]:Trueにすると、一意性制約が追加される。(default: False)
  • default: default値の設定。pythonの関数を指定。Pythonコードが呼び出された時点で実行されて、その結果がdefault値に設定される。
  • server_default: default値の設定。db serverの関数や式を指定する。指定方法:text("関数名")
  • onupdate: データupdate時に自動実行される関数を指定。指定方法:text("関数名")
  • autoincrement[True/False]:Trueにすると、Auto Incrementが有効になる。(default: False)
  • comment:カラムに対するコメントの追加。文字列を指定する。
  • ForeignKey: 外部キー設定。ForeignKey("<関係テーブル名.プロパティ名>")
  • nullable[True/False]:Trueにすると、null値が許容される(default: False)

関連テーブルの定義

Taskテーブルを追加して、Userテーブルとの関係を定義します。

from sqlalchemy.orm import relationship, Mapped, mapped_column, DeclarativeBase
from sqlalchemy import String, DateTime, ForeignKey, Uuid
from datetime import datetime
import uuid
from typing import List

# Modelの基底クラスの定義
class Base(DeclarativeBase):
    pass

# Userテーブル
class User(Base):
    ...
    tasks: Mapped[List["Task"]] = relationship(
        "Task", back_populates="user", cascade="all, delete-orphan"
    )

# Taskテーブル
class Task(Base, CommonColumns):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(
        primary_key=True, autoincrement=True, comment="タスクID"
    )
    user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), comment="ユーザーID")
    status: Mapped[str] = mapped_column(
        String(10), comment="ステータス"
    )
    content: Mapped[str] = mapped_column(String(255), comment="タスク内容")
    user: Mapped["User"] = relationship("User", back_populates="tasks")
  • relationship():テーブル同士の関係を定義するための関数

relationship()の引き数について

  • back_populate

    子テーブルから親テーブルへの逆向きのアクセスを可能にするための引数

  • cascade

    テーブルレコードに対する操作発生時に、関連するテーブルレコードに対して自動操作するためのパラメータ。

    具体的には、レコードを削除または更新したときに、関連レコードを自動的に削除または更新するかを指定をするオプション

    <指定可能なオプション>

    オプション

    関連する子テーブルレコードに対して実行される操作

    save-update

    保存、更新

    merge

    マージ

    expunge

    セッションから削除

    delete

    削除

    delete-orphan

    親レコードと関連が解除された場合に、削除

    refresh-expire

    親レコードが期限切れになった場合に、期限切れに設定

    all

    全操作を実行。save-update, merge, refresh-expire, expunge, deleteに相当

Mapped[type]について

マッピングされるオブジェクトに対して型を定義します。typeの部分に、型名を指定します。

Mappedで型指定すると、mapped_column()内の型指定は省略できます。

Mapped[Optional[type]]で、null許容型に指定もできます。

以下に省略できないケースを記述します。

  • String型のカラム(文字数を指定できないため)
  • オプションを追加したい場合
# 省略できるケース
id: Mapped[int] = mapped_column()

# 省略できないケース(文字数設定が必要)
email: Mapped[str] = mapped_column(String(255))
# 省略できないケース(オプションを追加したい場合)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))

型のカスタマイズ(おまけ①)

カスタマイズした型を適用することで、mapped_column()の型を省略できたり複数カラムで使いまわせたりします。

以下にカスタマイズした型の定義、それを適用したテーブルクラスを記述します。

from typing_extensions import Annotated
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import DateTime, String, 
from datetime import datetime

# 型の作成
uuid_pk = Annotated[uuid.UUID, mapped_column(
        Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4
    )]
int_pk = Annotated[int, mapped_column(primary_key=True, autoincrement=True)]
str_255 = Annotated[str, mapped_column(String(255))]
# or
# str_255 = Annotated[str, 255]
timestamp = Annotated[
    datetime,
    mapped_column(DateTime(timezone=True), server_default=text("NOW()")),
]

class Base(DeclarativeBase):
    # ここで型の作成も可能
    # type_annotation_map = {
    #     str_255: String(255)
    # }
    pass

# Userテーブル
class User(Base):
    ...
    id: Mapped[uuid_pk] = mapped_column(comment="ユーザーID")
    email: Mapped[str_255] = mapped_column(unique=True, comment="メールアドレス")
    created_at: Mapped[timestamp] = mapped_column(comment="作成日時")

# Taskテーブル
class Task(Base):
    id: Mapped[int_pk] = mapped_column(ForeignKey("users.id"), comment="タスクID")
    ...

共通カラムの設定(おまけ②)

共通カラム用のクラスを継承することで、継承元のカラムを引き継ぐことができます。

各クラスに同じカラムを定義したくない場合に有効です。

以下にその例を記述します。

```python
class Base(DeclarativeBase):
    pass

class CommonColumns:
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("NOW()"), comment="作成日時", 
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=text("NOW()"),
        onupdate=text("NOW()"),
        comment="更新日時",
    )

class User(Base, CommonColumns):
    ...

class Task(Base, CommonColumns):
    ...

```

テーブル定義(完成形)

最終的に作成したテーブル定義したコードは以下になります。

from sqlalchemy.orm import relationship, Mapped, mapped_column, DeclarativeBase
from sqlalchemy import String, Text, DateTime, ForeignKey, Uuid
from typing_extensions import Annotated
from datetime import datetime
from sqlalchemy.sql import text
import uuid
from typing import List


# 型の作成
uuid_pk = Annotated[uuid.UUID, mapped_column(
        Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4
    )]
int_pk = Annotated[int, mapped_column(primary_key=True, autoincrement=True)]
str_255 = Annotated[str, mapped_column(String(255))]
str_10 = Annotated[str, mapped_column(String(10))]
timestamp = Annotated[
    datetime,
    mapped_column(DateTime(timezone=True), server_default=text("NOW()")),
]

class Base(DeclarativeBase):
    pass

# テーブル共通カラム
class CommonColumns:
    created_at: Mapped[timestamp] = mapped_column(comment="作成日時")
    updated_at: Mapped[timestamp] = mapped_column(onupdate=text("NOW()"),comment="更新日時")

# Userテーブル
class User(Base, CommonColumns):
    __tablename__ = "user"

    id: Mapped[uuid_pk] = mapped_column(comment="ユーザーID")
    email: Mapped[str_255] = mapped_column(unique=True, comment="メールアドレス")

    tasks: Mapped[List["Task"]] = relationship(
        "Task", back_populates="user", cascade="all, delete-orphan"
    )

# Taskテーブル
class Task(Base, CommonColumns):
    __tablename__ = "task"

    id: Mapped[int_pk] = mapped_column(comment="タスクID")
    user_id: Mapped[uuidpk] = mapped_column(ForeignKey("user.id"), comment="ユーザーID")
    status: Mapped[str_10] = mapped_column(comment="ステータス")
    content: Mapped[str] = mapped_column(Text, comment="タスク内容")

    user: Mapped["User"] = relationship("User", back_populates="task")

参考サイト

プロフィール


都内で約1年半Webエンジニアやってます!(2024/3現在)
業務ではフロントエンド, バックエンド, インフラ(AWS)まで担当してます。
利用言語:TS, JS, HTML, CSS, Python, C#
FW:Vue, React, FastAPI, Flask, Django, Streamlit
クラウド:AWS

タグ一覧

Ruff

alembic

Docker

pytest

Python

Poetry

i18n

SQLAlchemy

vscode

Nuxtr

Nodejs

Volta

MUI

Prettier

Eslint

Biome

Vite

swr

React

Netlify

daisyUI

Tailwind CSS

msw

microCMS

Nuxt

Vue

AWS