フリーキーズ | 独学プログラミング

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

最終更新日
実行時の環境

SQLAlchemy 2.0.20

alembic 1.12.0

Python 3.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への接続情報を記載します。

sqlalchemy.url = driver://user:pass@localhost/dbname

しかし注意点として、MySQLの接続情報を直接コードへ書いてしまうことは避けるべきなので、env.pyへmigration実行時に使える変数として設定します。
config.set_main_option('sqlalchemy.url', "接続情報")のようにすることで、sqlalchemy.urlが上書きされた状態で実行できます。

config = context.config
# ↓を追記
config.set_main_option('sqlalchemy.url', os.environ['DB_URL'])

.envファイルを作成し、以下のようにデータベース接続情報を記載しましょう。

DB_URL=mysql://root:password@localhost/dbname

こうしておくことで、alembic.inisqlalchemy.urlは必要なくなるため、コメントアウトしてしまいましょう。

# sqlalchemy.url = mysql://user:pass@localhost/dbname

migrationファイルの設定

alembic.iniにコメントアウトされたfile_templateという項目があるので、コメントを外しておきましょう。

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を作成します。

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も作りましょう。

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も忘れずに書きましょう。

from .users import User

ここまでできたら、env.pytarget_metadataを編集します。

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)を格納するカラムを追加します。

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ファイルを見てみましょう。

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ファイルを編集します。

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を使いこなして、適切なデータベース管理をしていきましょう。

関連するコンテンツ