経緯

Alembicというライブラリを知ったので、試しに触ってみたメモ。

そもそもAlembicとは

Alembicとは、DBマイグレーションライブラリの1つ。SQLAlchemyと一緒に用いる。

今回やってみること

今回行うマイグレーションは、以下の3つ。

  1. User(email, password)を作成
  2. User(email, password, name)に変更

使い方

  1. Alembicの初期化
  2. 使うDBとの紐づけ
  3. エンティティ作成
  4. マイグレーションファイルを作成
  5. マイグレーション

想定する状況と準備

  • FastAPI上でWeb APIを提供し、その中でDBを操作することを想定する。ただし、メインテーマがマイグレーションのため、この記事でFastAPIの話は一切出ない。プロジェクト作成について、詳しくはFastAPI入門を参照するとよい。この記事では、ディレクトリ構成を大きく参考にしている。
  • パッケージ管理にはpoetryを使う。
  • DBにはMySQLを使う。

マイグレーションは同期処理で行うため、入れるのはPyMySQLだけでよい。しかしFastAPIでDBを処理するときに非同期での操作を行うことを見越し、aiomysqlを入れる。この時点でPyMySQLも入る。

1
poetry add sqlalchemy alembic aiomysql

alembicの初期化

次のコマンドを実行する。

1
poetry run alembic init alembic

プロジェクト配下にalembic/というディレクトリが生成される。

使うDBとの紐づけ

alembic.iniを編集する。sqlalchemy.urlの記述を見つけたら以下のようにする。

1
sqlalchemy.url = mysql+pymysql://<url>/<name>?charset=utf8

<url><name>にはそれぞれ、mysqlのサーバーのURLとそのDBの名前を指定する。例えばmysqlがdbというDocker Composeのサービスとして稼働しており、3306ポートで受け付けており、そのDBの名前がappdbだった場合、次のようになる。

1
sqlalchemy.url = mysql+pymysql://root@db:3306/appdb?charset=utf8

api/db.pyにDBエンティティのベースを作っておく。

1
2
3
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

alembic/env.pytarget_metadataを以下のようにする。

1
2
3
from api.db import Base

target_metadata = Base.metadata

空のマイグレーション

まだエンティティを作成していない状態でマイグレーションを行ってみる。

マイグレーション用のファイル作成

以下でマイグレーション用のファイルを作成する

1
poetry run alembic revision --autogenerate -m "empty revision"

alembic/versionsにマイグレーション用のファイルが生成されている。

マイグレーション

以下で、alembic/versionsに作成されたマイグレーションファイルをもとに、マイグレーションを実行する。

1
poetry run alembic upgrade head

これにより、mysqlで新たにalembic_versionというテーブルが追加されている。そして新たにレコードが追加されている。

Userエンティティ作成

api/models/user.pyを作成する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sqlalchemy import Column, Integer, String
from api.db import Base


class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=true)
    email = Column(String(256), unique=true)
    password = Column(String(1024))

alembic/env.pyにて以下の記述を追加する。

1
from api.models import user

なぜこれをインポートするのかというと、こうしないとBase.metadataの中にUserの情報が入らないから。実際、REPLで確認してみると、api.models.userないしそのUsersクラスの宣言が読まれて初めて、Base.metadata.tablesの中に情報が入ることが分かる。

1
2
3
4
5
6
>>> from api.db import Base
>>> Base.metadata.tables
FacadeDict({})
>>> from api.models import user
>>> Base.metadata.tables
FacadeDict({'users': Table('users', MetaData(), Column('id', Integer(), table=<users>, primary_key=True, nullable=False), Column('email', String(length=256), table=<users>), Column('password', String(length=1024), table=<users>), Column('name', String(length=1024), table=<users>), schema=None)})

これで再びマイグレーションファイルを作成。

1
poetry run alembic revision --autogenerate -m "create user"

次のメッセージが出力されていたら、きちんとUserエンティティが認識され、新たなマイグレーションファイルが作成された証拠である。

1
info  [alembic.autogenerate.compare] detected added table 'users'

生成されたファイルはalembic/versions/<uuid>_create_user.pyである。中身を見ると、upgrade関数とdowngrade関数の中に諸々の処理が追加されている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('email', sa.String(length=256), nullable=True),
    sa.Column('password', sa.String(length=1024), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('email')
    )
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###

最後にマイグレーションを行う。

1
poetry run alembic upgrade head

これでmysqlを確認してみると、確かにusersテーブルが作成されていることがわかる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
mysql> show tables;
+-----------------+
| tables_in_demo  |
+-----------------+
| alembic_version |
| users           |
+-----------------+
2 rows in set (0.00 sec)

mysql> describe users;
+----------+---------------+------+-----+---------+----------------+
| field    | type          | null | key | default | extra          |
+----------+---------------+------+-----+---------+----------------+
| id       | int           | no   | pri | null    | auto_increment |
| email    | varchar(256)  | yes  | uni | null    |                |
| password | varchar(1024) | yes  |     | null    |                |
+----------+---------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

Userエンティティの変更

例えばUserエンティティに新たな属性nameを追加する。api/models/user.pyに追記する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sqlalchemy import Column, Integer, String
from api.db import Base


class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    email = Column(String(256), unique=True)
    password = Column(String(1024))
    name = Column(String(1024))

マイグレーションファイルを作成する。

1
poetry run alembic revision --autogenerate -m "modify user"

以下のメッセージが出力されたら、usersテーブルにnameカラムが追加されることをAlembicが認識した証拠である。

1
info  [alembic.autogenerate.compare] detected added column 'users.name'

alembic/versions/<uuid>_modify_user.pyを見てみると、実際にカラムを追加する処理が走っていることがわかる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('name', sa.String(length=1024), nullable=True))
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'name')
    # ### end Alembic commands ###

以下でマイグレーションの実行を行う。

1
poetry run alembic upgrade head

mysqlで見てみると、nameの項目が追加されていることが分かる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mysql> describe users;
+----------+---------------+------+-----+---------+----------------+
| field    | type          | null | key | default | extra          |
+----------+---------------+------+-----+---------+----------------+
| id       | int           | no   | pri | null    | auto_increment |
| email    | varchar(256)  | yes  | uni | null    |                |
| password | varchar(1024) | yes  |     | null    |                |
| name     | varchar(1024) | yes  |     | null    |                |
+----------+---------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)