Alembicというライブラリを知ったので、試しに触ってみたメモ。
そもそもAlembicとは#
Alembicとは、DBマイグレーションライブラリの1つ。SQLAlchemyと一緒に用いる。
今回やってみること#
今回行うマイグレーションは、以下の3つ。
- 空
User(email, password)
を作成User(email, password, name)
に変更
使い方#
- Alembicの初期化
- 使うDBとの紐づけ
- エンティティ作成
- マイグレーションファイルを作成
- マイグレーション
想定する状況と準備#
- 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.py
のtarget_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)
|