TOP
プログラミング独学コンテンツ一覧
SQLAlchemy2.0でalembicを使ってmigrationを管理する

SQLAlchemy2.0でalembicを使ってmigrationを管理する

実行時の環境
SQLAlchemy2.0.20
alembic1.12.0
Python3.11.5
PythonのデータベースツールキットやORMとして使われる機会が多いSQLAlchemyで、alembicを使ったmigrationを行う方法を紹介します。
実は、FastAPIの開発者であるSebastián Ramírez氏(@tiangolo)が開発しているSQLModelというORMもあります。しかしながら、まだ発展途上のため、安定感のあるSQLAlchemyで進めていきます。
今回はalembicメインの内容とするため、SQLAlchemyは導入済みの前提で進めます。

alembicのインストール

pipでインストールできます。

$ pip install alembic

alembicの初期設定

まずは初期化しましょう。以下のコマンドで初期化できます。
引数のmigrationsはディレクトリ名になるため、別の名称が良い場合は変えられます。

$ alembic init migrations

$ tree
.
├── alembic.ini
├── migrations
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions
上記のようなファイルとディレクトリが作成されます。
各ファイル、ディレクトリの説明は以下です。
  • alembic.ini: ログやDB設定などが記載された設定ファイル
  • migrations: migration関連のファイルのディレクトリ
  • README: migration関連のメモ
  • env.py: migrationの設定ファイル
  • script.py.mako: テンプレートエンジンMakoを使ったmigrationファイルのテンプレ
  • versions: migrationファイルが保存されるディレクトリ

データベース接続設定

データベースへの接続はalembic.iniファイルを編集します。
sqlalchemy.urlという項目があるので、その部分を書き換えます。今回はMySQLにつなぐため、MySQLへの接続情報を記載します。
alembic.ini
sqlalchemy.url = driver://user:pass@localhost/dbname
しかし注意点として、MySQLの接続情報を直接コードへ書いてしまうことは避けるべきなので、env.pyへmigration実行時に使える変数として設定します。
config.set_main_option('sqlalchemy.url', "接続情報")のようにすることで、sqlalchemy.urlが上書きされた状態で実行できます。
migrations/env.py
config = context.config
# ↓を追記
config.set_main_option('sqlalchemy.url', os.environ['DB_URL'])
.envファイルを作成し、以下のようにデータベース接続情報を記載しましょう。
.env
DB_URL=mysql://root:password@localhost/dbname
こうしておくことで、alembic.inisqlalchemy.urlは必要なくなるため、コメントアウトしてしまいましょう。
alembic.ini
# sqlalchemy.url = mysql://user:pass@localhost/dbname

migrationファイルの設定

alembic.iniにコメントアウトされたfile_templateという項目があるので、コメントを外しておきましょう。
alembic.ini
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

モデルの作成

ここから、実際にmigration対象となるモデルを作っていきます。以下のようなディレクトリ構成で進めます。
.
├── alembic.ini
├── migrations
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions
├── src
│   ├── __init__.py
│   ├── models
│   │   ├── __init__.py
│   │   ├── base.py
│   │   └── users.py
まずはベースとなるbase.pyを作成します。
src/models/base.py
from datetime import datetime

from sqlalchemy import String, DateTime, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.sql.functions import current_timestamp

import uuid


class Base(DeclarativeBase):
    pass


class BaseModelMixin:
    id: Mapped[str] = mapped_column(String(32), primary_key=True, default=uuid.uuid4)
    created_at: Mapped[datetime] = mapped_column(DateTime,
                                                 nullable=False,
                                                 server_default=current_timestamp())
    updated_at: Mapped[datetime] = mapped_column(DateTime,
                                                 nullable=False,
                                                 default=current_timestamp(),
                                                 onupdate=func.now())
    deleted_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
実際はclass Base(DeclarativeBase)で事足りますが、Mix-in(共通で使い回す定義)を定義しておくと便利です。
続いて、users.pyも作りましょう。
src/models/users.py
from datetime import datetime

from sqlalchemy import String, Text, DateTime
from sqlalchemy.orm import Mapped, mapped_column

from src.models.base import Base, BaseModelMixin


class User(BaseModelMixin, Base):
    __tablename__ = 'users'
    mysql_charset = ('utf8mb4',)
    mysql_collate = 'utf8mb4_unicode_ci'

    username: Mapped[str] = mapped_column(String(255), nullable=True, unique=True)
    email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
    password: Mapped[str] = mapped_column(Text, nullable=False)
    email_verified_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
    last_login_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
シンプルなユーザー情報テーブルの定義です。
後でmodelsをimportしたときに便利なように__init__.pyも忘れずに書きましょう。
src/models/__init__.py
from .users import User
ここまでできたら、env.pytarget_metadataを編集します。
migrations/env.py
from src.models.base import Base
target_metadata = Base.metadata
以下のコマンドでmigrationファイルを作成しましょう。

$ source .env && alembic revision --autogenerate -m 'create-users-table'

すると、migrations/versionsにmigrationファイルが生成されます。
以下のコマンドでこれを適用します。

$ alembic upgrade head

$ alembic upgrade head
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> xxxxxxxx, create-users-table
上記のように表示されたら、usersテーブルがMySQLのデータベース上に作成されたことがわかります。

行(カラム)の追加

作成されたテーブルに追加したいカラムがある前提とします。行を追加する方法も見ておきましょう。
先ほどのsrc/models/users.pyを編集します。今回はavatar(ユーザーの画像URL)を格納するカラムを追加します。
src/models/users.py
class User(BaseModelMixin, Base):
    __tablename__ = 'users'
    mysql_charset = ('utf8mb4',)
    mysql_collate = 'utf8mb4_unicode_ci'

    username: Mapped[str] = mapped_column(String(255), nullable=True, unique=True)
    email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
    avatar: Mapped[str] = mapped_column(String(255), nullable=True) # <- これを追加
    password: Mapped[str] = mapped_column(Text, nullable=False)
    email_verified_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
    last_login_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
追加した行はemailpasswordの間に挟み込みます。
alembic revisionを実行しましょう。
$ alembic revision --autogenerate -m 'add-column-avatar-into-users'
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'users.avatar'
  Generating /app/migrations/versions/2023_09_07_0618-9c5881a40e10_add_column_avatar_into_users.py ...  done
上記の結果にDetected added column 'users.avatar'と書いてあるのがわかります。「users.avatarが追加されたことを検知した」という意味です。
しかし、ここで注意です。作成されたmigrationファイルを見てみましょう。
migrations/versions/生成されたmigrationファイル.py
def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('avatar', sa.String(length=255), nullable=True))
    # ### end Alembic commands ###
alembicにはbatch(一群の意味)モードというまとめて実行するモードがあります。そのモードに含まれるadd_columnにはinsert_afterという順序を制御するオプションがあるので、それを使いましょう。(なぜ通常のadd_columnにないのか不明)
migrationファイルを編集します。
migrations/versions/生成されたmigrationファイル.py
def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('users', recreate='always') as batch_op:
        batch_op.add_column(sa.Column('avatar', sa.String(length=255), nullable=True),
                            insert_after='email')
    # ### end Alembic commands ###
ここでMySQLを見てみると、きちんとemailの後にavatarがあることを確認できます。
mysql> show columns from users;
--------------
show columns from users
--------------

+-------------------+--------------+------+-----+-------------------+-------------------+
| Field             | Type         | Null | Key | Default           | Extra             |
+-------------------+--------------+------+-----+-------------------+-------------------+
| username          | varchar(255) | YES  | UNI | NULL              |                   |
| email             | varchar(255) | NO   | UNI | NULL              |                   |
| avatar            | varchar(255) | YES  |     | NULL              |                   |
| password          | text         | NO   |     | NULL              |                   |
| email_verified_at | datetime     | YES  |     | NULL              |                   |
| last_login_at     | datetime     | YES  |     | NULL              |                   |
| id                | varchar(32)  | NO   | PRI | NULL              |                   |
| created_at        | datetime     | NO   |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updated_at        | datetime     | NO   |     | NULL              |                   |
| deleted_at        | datetime     | YES  |     | NULL              |                   |
+-------------------+--------------+------+-----+-------------------+-------------------+
なお、以下のコマンドで実行履歴を見ることが可能です。(表示される乱数はバージョンです)

$ alembic history

alembic history
5ba7c3ae1200 -> 9c5881a40e10 (head), add-column-avatar-into-users
<base> -> 5ba7c3ae1200, create-users-table

alembicは癖はあるが強力なマイグレーションツール

ここまで見てきた通り、alembicは独自のコマンドがあり設定項目も多く、覚えるのに苦労します。高機能ゆえのデメリットとも言えます。
しかし、「こういうことしたいんだけど」といった要望に応えられる高品質さのために、多くのプロジェクトで採用されています。
データベース管理はソフトウェアの品質を左右する大切なタスクです。
alembicを使いこなして、適切なデータベース管理をしていきましょう。