PythonでDBマイグレーションの仕組みを導入(Alembic)

DjangoやRails、Laravel、CakePHP等のフルスタックなWebアプリケーションフレームワークには、データベースのスキーマを管理しやすくするマイグレーションツールが大体組み込まれています。
これらのフレームワークを利用しないケース（機械学習のバッチ処理等）でもマイグレーションツールが使いたいなと思って調べたところ、Alembicというツールで実現できるようです。
他にも似たようなライブラリはいくつかあったのですが、Alembicが既に使っていたSQLAlchemyの作者によって書かれたというのも見てこちらを採用してみました。ドキュメントもしっかりしています。
http://alembic.zzzcomputing.com/en/latest/

あまりイレギュラーな運用をしていないからというのもありますが、今のところ困ったことも無く使えています。

# 環境

Ubuntu: 16.04
Python: 3.6.2
pip: 9.0.1
PostgreSQL: 9.6.5

# インストール

※ psycopg2等は開発段階で使っているため前提になってしまっています

```bash
$ pip install alembic
```

現時点では0.9.6がインストールされました。
インストールするとalembicコマンドが使えるようになります。

# セットアップ

Alembicの設定ファイルやマイグレーションファイル等が格納されるフォルダを以下のコマンドで作成します。dbのところは好きな名前を付けることができます。

```bash
$ alembic init db
```

以下の構成でフォルダとファイルが作成されました。alembic.iniが設定ファイル、versionsフォルダにマイグレーションファイルが入っていきます。

```
project_root/
  ├ alembic.ini
  └ db/
    ├ env.py
    ├ README
    ├ script.py.maco
    └ versions/
```

alembic.iniのsqlalchemy.urlを自分の環境に合わせて書き換えます。

```alembic.ini
sqlalchemy.url = driver://user:pass@localhost/dbname <- ここを書き換える
```

（iniファイルの解説は http://alembic.zzzcomputing.com/en/latest/tutorial.html#editing-the-ini-file ）

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

usersテーブルを作成するためのマイグレーションファイルを作ってみます。

```bash
$ alembic revision -m "create users"
Generating .../db/versions/b9c14exxxxxx__create_users.py ... done
```
db/versions/以下にusersテーブルを作成するためのマイグレーションファイルが作成されました。
このファイルにusersテーブルを作成するロジックを追加します。理解が進んだ後であれば問題ありませんが、それまではupgradeとdowngradeメソッド以外には手を加えない方がいいと思います。

```db/versions/b9c14exxxxxx__create_users.py
"""create users

Revision ID: b9c14exxxxxx
Revises:
Create Date: 2018-01-30 05:00:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'b9c14exxxxxx'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('account', sa.String(50), nullable=False),
        sa.Column('password', sa.String(50), nullable=False),
        sa.Column('created_at', sa.DateTime, nullable=False, server_default=sa.func.now()),
        sa.Column('updated_at', sa.DateTime, nullable=False, server_default=sa.func.now()),
    )


def downgrade():
    op.drop_table('users')
```

どんなColumnタイプが使えるんだっけ？というのはSQLAlchemyがベースなので、SQLAlchemyのマニュアルを参照するのがいいと思います。
http://docs.sqlalchemy.org/en/latest/core/type_basics.html

2テーブル目以降も上記のalembic revisionコマンドを実行していくことで次のマイグレーションファイルを作成できます。


# マイグレーションの実行

以下のコマンドでマイグレーションが実行されて最新の状態になります。

```bash
$ alembic upgrade head
```

目的のusersテーブルとalembicの管理用のテーブルが作成されました。

```
postgres=# \dt
                    List of relations
 Schema |              Name              | Type  | Owner
--------+--------------------------------+-------+--------
 public | alembic_version                | table | postgres
 public | users                          | table | postgres

```

やり直したいから一つ遡りたいという場合は以下のコマンドになります。

```bash
$ alembic downgrade -1
```

全部やり直したい場合は以下のコマンドです。

```bash
$ alembic downgrade base
```

他にも好きなrevisionに合わせるということ等も可能ですのでチュートリアルを参照してください。
http://alembic.zzzcomputing.com/en/latest/tutorial.html

# 注意点（玄人じゃない方向け）

## マイグレーションツールをほとんど使ったことがない方へ

「あ、間違えた」となってマイグレーションファイルを修正する時は注意してください。既にマイグレーションを最新化した後であれば、修正したマイグレーションのrevisionまでdowngradeしてから再度最新化する必要があります。（毎回alembic downgrade baseして全クリアしてもいいですが、データも消えてしまいます。）
ただし、既に他の開発者に展開してしまっている時はその手順を説明して実行してもらうのは厄介なので、修正用のマイグレーションファイルを新規作成するのが無難かと思います。
無駄にマイグレーションファイルを増やしたくないという思想もあると思うので状況次第ではありますが、開発フェーズでは修正する、本番運用が始まったら新規作成という切り分けもアリかもしれません。

## 他のマイグレーションツールを少し使ったことがある方へ

Alembicはハッシュをベースにrevision管理をしていて各マイグレーションファイル内に前のハッシュを保持して前後関係を把握するような仕組みになっているので、途中のファイルを削除してしまうとうまく動かなくなってしまいます。途中のファイルを消したくなった場合にはその後ろのマイグレーションファイルのdown_revisionを書き換えてあげる必要があります。
まぁ上でも述べたようにあくまで自分のローカル環境の範囲（他の開発者を巻き込まない範囲）で失敗しちゃったからどうにかしたいというケースにとどめておくのがいいかなと思います。
並行開発の面等も考慮してタイムスタンプベースのrevision管理の方がいいという方は別のツールを選択した方がいいかもしれません。（Alembicもオプションで対応可能なのかはまだ確認できていません。）

[2018/02/09 追記]
管理方法が変わるわけではありませんが、alembic.iniのfile_templateの設定を変更することによりrevisionのフォーマットを変更することができます。

```alembic.ini
ile_template = %%(year)04d%%(month)02d%%(day)02d%%(hour)02d%%(minute)02d%%(second)02d_%%(rev)s_%%(slug)s
```

上記のように設定すると、タイムスタンプ＋ハッシュ＋名称.py のようなファイル名にできるので、マイグレーションファイルの整列に困るという場合にはカスタマイズすると嬉しいかもしれません。