実行時の環境
- 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 alembicalembicの初期設定
まずは初期化しましょう。以下のコマンドで初期化できます。
引数の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.iniのsqlalchemy.urlは必要なくなるため、コメントアウトしてしまいましょう。
# sqlalchemy.url = mysql://user:pass@localhost/dbnamemigrationファイルの設定
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(共通で使い回す定義)を定義しておくと便利です。
defaultとserver_default、onupdateとserver_onupdateの違いについてですが、MySQLのCREATE TABLE時にデフォルトを指定するかを指定しています。server_をつけなければ、プログラム側で値のデフォルト値をセットします。
作成日時と違い、バッチ処理などで任意の更新日時で揃える際に有効なため、更新日時はserver_なしにしています。
続いて、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.pyのtarget_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)追加した行はemailとpasswordの間に挟み込みます。
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 historyalembic history
5ba7c3ae1200 -> 9c5881a40e10 (head), add-column-avatar-into-users
<base> -> 5ba7c3ae1200, create-users-tablealembicは癖はあるが強力なマイグレーションツール
ここまで見てきた通り、alembicは独自のコマンドがあり設定項目も多く、覚えるのに苦労します。高機能ゆえのデメリットとも言えます。
しかし、「こういうことしたいんだけど」といった要望に応えられる高品質さのために、多くのプロジェクトで採用されています。
データベース管理はソフトウェアの品質を左右する大切なタスクです。
alembicを使いこなして、適切なデータベース管理をしていきましょう。
