今回は Python のデータベースマイグレーションツールの Alembic について。 Alembic を使うとデータベースのスキーマをマイグレーションスクリプトにもとづいて管理できる。 マイグレーションスクリプトというのは、スキーマのバージョンを現在の状態から進める・戻すのに必要な手順が書かれたスクリプトのこと。 このブログでも、以前に Alembic でマイグレーションスクリプトを自動生成するための方法について書いたことがある。
そして、今回はアプリケーションの実運用が始まってしまった後からスキーマの管理を Alembic に移行するための手順を書いてみる。 スケジュールが厳しいプロジェクトなんかだと、リリースまでにマイグレーションまで手が回らないなんてこともまあ考えられる。
今回使った環境は次の通り。 RDBMS には MySQL 5.7 を使った。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.11.5 BuildVersion: 15F34 $ python --version Python 3.5.1 $ mysql --version mysql Ver 14.14 Distrib 5.7.13, for osx10.11 (x86_64) using EditLine wrapper
下準備
まずは Homebrew で MySQL をインストールしておく。
$ brew install mysql
そして MySQL サーバを起動しよう。
$ mysql.server start
次に Python の O/R マッパーの SQLAlchemy と MySQL ドライバの mysqlclient をインストールしておく。
$ pip install sqlalchemy mysqlclient $ pip list | egrep -i "(sqlalchemy|mysqlclient)" mysqlclient (1.3.7) SQLAlchemy (1.0.13)
今回、動作確認に使うためのデータベースを用意する。
$ mysql -u root -e "CREATE DATABASE IF NOT EXISTS migration CHARACTER SET utf8mb4"
「migration」という名前のデータベースだ。
$ mysql -u root -e "SHOW CREATE DATABASE migration" +----------+--------------------------------------------------------------------+ | Database | Create Database | +----------+--------------------------------------------------------------------+ | migration | CREATE DATABASE `migration` /*!40100 DEFAULT CHARACTER SET utf8mb4 */ | +----------+--------------------------------------------------------------------+
これで RDBMS に関しては準備ができた
テーブルとレコードを用意する
次に Alembic が導入されていない頃のプロジェクトを想定した状況を作る。 最初に SQLAlchemy でテーブル定義に対応するモデルを用意しよう。 このモデルは、ユーザを管理するための users というテーブルをひとつ持っている。
$ cat << 'EOF' > model.py #!/usr/bin/env python # -*- coding: utf-8 -*- from sqlalchemy.ext.declarative.api import declarative_base from sqlalchemy.sql.schema import Column from sqlalchemy.sql.sqltypes import BigInteger from sqlalchemy.sql.sqltypes import Text Base = declarative_base() class User(Base): __tablename__ = 'users' # 主キー id = Column(BigInteger, primary_key=True) # 名前 name = Column(Text, nullable=False) EOF
次に、上記のモデルを使って実際にデータベースにテーブルとレコードを追加するスクリプトを用意する。
$ cat << 'EOF' > insert.py #!/usr/bin/env python # -*- coding: utf-8 -*- from sqlalchemy.engine import create_engine from sqlalchemy.orm.session import sessionmaker from model import Base from model import User def main(): # エンジンを作る engine = create_engine( 'mysql+mysqldb://root@localhost/migration?charset=utf8mb4', echo=True, ) # テーブルを作る Base.metadata.create_all(engine) # セッション作成用のオブジェクトを作る SessionMaker = sessionmaker( bind=engine, autocommit=True, expire_on_commit=False, ) # セッションを作る session = SessionMaker() # 最初に行をひとつ追加しておく with session.begin(subtransactions=True): user = User(name='foo') session.add(user) if __name__ == '__main__': main() EOF
上記のスクリプトを実行しよう。
$ python insert.py 2016-06-11 12:46:19,645 INFO sqlalchemy.engine.base.Engine SHOW VARIABLES LIKE 'sql_mode' 2016-06-11 12:46:19,646 INFO sqlalchemy.engine.base.Engine () 2016-06-11 12:46:19,649 INFO sqlalchemy.engine.base.Engine SELECT DATABASE() ...(省略)... 2016-06-11 12:46:19,721 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name) VALUES (%s) 2016-06-11 12:46:19,721 INFO sqlalchemy.engine.base.Engine ('foo',) 2016-06-11 12:46:19,726 INFO sqlalchemy.engine.base.Engine COMMIT
これで Alembic を導入する前のデータベースの状態が用意できた。
$ mysql -u root -D migration -e "DESC users" +-------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | name | text | NO | | NULL | | +-------+------------+------+-----+---------+----------------+ $ mysql -u root -D migration -e "SELECT * FROM users\G" *************************** 1. row *************************** id: 1 name: foo
Alembic を導入する
さて、ここからは Alembic を導入するフェーズに入る。
$ pip install alembic $ pip list | grep -i alembic alembic (0.8.6)
ここからの作業は、先ほど用意した Python スクリプトと同じ場所で進める。
$ ls __pycache__ insert.py model.py
まずは alembic コマンドを使って必要なファイルセットを一式用意しよう。
$ alembic init alembic
これでディレクトリには alembic.ini というファイルと alembic というディレクトリができるはず。
$ ls __pycache__ alembic alembic.ini insert.py model.py
ここからは Alembic の設定ファイルを編集していくんだけど、その前に GNU sed が入っていなければインストールしておく。 Mac の sed はオプションが GNU 版と違うので。
$ brew install gnu-sed $ alias sed='gsed'
まずは alembic.ini の中にある sqlalchemy.url という項目を編集する。 必要に応じて接続用 URL のアカウントなどは適宜変更する。
$ sed -i -e 's!^sqlalchemy\.url = .*$!sqlalchemy.url = mysql+mysqldb://root@localhost/migration?charset=utf8mb4!' alembic.ini $ grep 'sqlalchemy\.url' alembic.ini sqlalchemy.url = mysql+mysqldb://root@localhost/migration?charset=utf8mb4
次に Alembic に管理対象のモデルを教えてやる。 これには alembic/env.py の target_metadata にモデルが継承しているオブジェクトの metadata メンバを指定する。
$ sed -i -e ' 2i import model s:^\(target_metadata = \)None:\1model.Base.metadata: ' alembic/env.py $ head -n 3 alembic/env.py from __future__ import with_statement import db from alembic import context $ grep ^target_metadata alembic/env.py target_metadata = model.Base.metadata
これで Alembic の下準備ができた。
マイグレーションスクリプトを用意する
次は Alembic のマイグレーションスクリプトを用意する。 これはデータベースに何もないまっさらな状態から users テーブルがある状態にするためのスクリプト。
最初のリビジョンのマイグレーションスクリプトを生成する。
$ PYTHONPATH=. alembic revision -m "Initial"
これで、なんか適当なリビジョンが振られたスクリプトができる。
$ ls alembic/versions 6b7569a12df9_initial.py __pycache__
生成されたスクリプトにマイグレーションの内容を記述する。
$ cat alembic/versions/6b7569a12df9_initial.py """Initial Revision ID: 6b7569a12df9 Revises: Create Date: 2016-06-11 13:39:23.954411 """ # revision identifiers, used by Alembic. revision = '6b7569a12df9' down_revision = None branch_labels = None depends_on = None from alembic import op import sqlalchemy as sa def upgrade(): op.create_table('users', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.Text(), nullable=False), sa.PrimaryKeyConstraint('id') ) def downgrade(): op.drop_table('accounts')
ちなみに、マイグレーションスクリプトの内容はある程度まで Alembic で自動化で生成することもできる。 ただし、今回は既にデータベースの状態とモデルの状態が一致してしまっている。 そのため自動で生成するときはまっさらなデータベースを別に用意する必要がある。
スキーマの管理を Alembic に移行する
さて、ここまでで全ての下準備が整った。 いよいよデータベースのスキーマを Alembic に移行してみよう。
Alembic では、今のスキーマの状態を alembic_version というテーブルで管理している。 もちろん、現状ではスキーマを Alembic で管理していないので、このテーブルがない。 そこで、プロジェクトの途中から管理を Alembic に移すときは手動でこのテーブルを作ってやれば良い。
まずは、次のようにしてテーブルとレコードを追加する。 レコードに入れるリビジョン番号は、今のデータベースの状態が Alembic のマイグレーションスクリプトをどこまで適用したかを示している。 今のデータベースの状態は、先ほど用意したマイグレーションスクリプトが既に適用された状態と捉えることができる。 要するに、ここには先ほど用意したマイグレーションスクリプトのリビジョンを指定すれば良い。
$ mysql -u root -D migration -e "CREATE TABLE alembic_version(version_num varchar(32) NOT NULL)" $ mysql -u root -D migration -e "INSERT INTO alembic_version(version_num) values ('6b7569a12df9')"
レコードを追加できたら alembic コマンドで upgrade head サブコマンドを実行してみよう。 これは Alembic のマイグレーションスクリプトを最新の状態までデータベースに適用することを意味している。
$ PYTHONPATH=. alembic upgrade head INFO [alembic.runtime.migration] Context impl MySQLImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL.
ここで上記のように、特に何も実行されなければ上手くいっている。
ちなみに、間違えてレコードを追加する前に上記のコマンドを実行してしまっても問題はない。 まずは、何やら例外が出て焦るかもしれない。
$ PYTHONPATH=. alembic upgrade head ...(省略)... sqlalchemy.exc.OperationalError: (_mysql_exceptions.OperationalError) (1050, "Table 'users' already exists") [SQL: '\nCREATE TABLE users (\n\tid INTEGER NOT NULL AUTO_INCREMENT, \n\tname TEXT NOT NULL, \n\tPRIMARY KEY (id)\n)\n\n']
上記は alembic_version テーブルがないことで Alembic がデータベースがまっさらな状態と考えてテーブルを作ろうとしているために発生したエラーだ。
alembic_version テーブル自体は上記のコマンドで作られる。
$ mysql -u root -D migration -e "CREATE TABLE alembic_version(version_num varchar(32) NOT NULL)" ERROR 1050 (42S01) at line 1: Table 'alembic_version' already exists
つまり、落ち着いて現在のリビジョンをレコードに追加することで Alembic に今データベースがどの状態にあるかを教えてやれば良い。
$ mysql -u root -D migration -e "INSERT INTO alembic_version(version_num) values ('6b7569a12df9')"
スキーマを更新してみる
Alembic に管理が移行できたところで、試しにスキーマを更新してみよう。
例えば users テーブルに年齢 (age) を入れるカラムを追加してみよう。 既存のユーザがマイグレーションするときは null を入れることにする。
$ cat << 'EOF' > model.py #!/usr/bin/env python # -*- coding: utf-8 -*- from sqlalchemy.ext.declarative.api import declarative_base from sqlalchemy.sql.schema import Column from sqlalchemy.sql.sqltypes import BigInteger from sqlalchemy.sql.sqltypes import Integer from sqlalchemy.sql.sqltypes import Text Base = declarative_base() class User(Base): __tablename__ = 'users' # 主キー id = Column(BigInteger, primary_key=True) # 名前 name = Column(Text, nullable=False) # 年齢 age = Column(Integer, nullable=True) EOF
今度は Alembic のスキーマの自動検出機能を使ってみよう。
$ PYTHONPATH=. alembic revision --autogenerate -m "Add age column"
するとマイグレーションスクリプトが追加されたことがわかる。
$ ls alembic/versions 6b7569a12df9_initial.py e64c12b8698d_add_age_column.py __pycache__
内容を確認すると users テーブルに age カラムを追加・削除するスクリプトが生成されている。
$ cat alembic/versions/e64c12b8698d_add_age_column.py """Add age column Revision ID: e64c12b8698d Revises: 6b7569a12df9 Create Date: 2016-06-11 14:20:27.826977 """ # revision identifiers, used by Alembic. revision = 'e64c12b8698d' down_revision = '6b7569a12df9' branch_labels = None depends_on = None from alembic import op import sqlalchemy as sa def upgrade(): ### commands auto generated by Alembic - please adjust! ### op.add_column('users', sa.Column('age', sa.Integer(), nullable=True)) ### end Alembic commands ### def downgrade(): ### commands auto generated by Alembic - please adjust! ### op.drop_column('users', 'age') ### end Alembic commands ###
早速、このスクリプトを使ってデータベースを更新してみよう。
$ PYTHONPATH=. 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 6b7569a12df9 -> e64c12b8698d, Add age column
すると確かにデータベースのスキーマが更新されている。
$ mysql -u root -D migration -e "DESC users" +-------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | name | text | NO | | NULL | | | age | int(11) | YES | | NULL | | +-------+------------+------+-----+---------+----------------+
既存のユーザについては age に null が入った。
$ mysql -u root -D migration -e "SELECT * FROM users" +----+------+------+ | id | name | age | +----+------+------+ | 1 | foo | NULL | +----+------+------+
もし、マイグレーションしたときに特定の値を入れなおすみたいな処理がしたければマイグレーションスクリプトにそれを記述すれば良い。 Alembic のマイグレーションスクリプトはあくまで、ただの Python モジュールに過ぎない。 だから、どんな処理を書いても構わない。
まとめ
- プロジェクトの途中からでも Alembic を導入してスキーマ管理を移行できる
- それには alembic_version というテーブルのレコードを手作業で用意すれば良い
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログを見る