FlaskでのMigrationって結局どう運用するのが正しいのか

# はじめに

Flaskでデータベースを扱う場合、OR MapperとしてSQLAlchemyがまだ今の所デファクトスタンダードだと思われる。
個人的にはあまりライブラリ依存したくないのでFlask-SQLAlchemyを使わずSQLAlchemyをそのまま使うようにしている(WTformsとかもね)

ただそういうこだわりをもっていて何か大切なものを見失っては困るので大分前だがとあるプロジェクトにFlask-SQLAlchemyを導入した。
そうなるとFlask-Migrateにもお世話になるのが筋だと思いこちらも使ってみた。

Flask-Migrateについてはよく理解していないとハマる。
実はつい今しがたもそのプロジェクトのテスト環境を久々に作ろうとしてツボったのだ。
そのため備忘録というか少しでも参考になればと思い、私なりの使い方を紹介する。

# 基本的な使い方

一番最初はこの三点セット

```shell
flask db init    # マイグレーションのリポジトリ初期化(git initみたいなもん)
flask db migrate # マイグレーション用のスクリプトを生成(migrations/versions/配下のxxx_.py)
flask db upgrade # データベースへ書き込み
```

> 実際は FLASK_APP=run:app flask db init みたいに環境変数が必要

後はモデルの修正するたびに

```shell
flask db migrate
flask db upgrade
```

ってやればよい。リビジョン管理がされているのでロールバックというかダウングレードもできる。

```shell
flask db downgrade
```

以上、終了。


でだいたいどこに書いてある記事でもこんな説明になっている。
悩ましいのは生成されるmigrationsをgitの管理下に置くかということだ。

# migrationsディレクトリって環境毎につくるもの?

恐らくこれが正解だと思う。

Flaskアプリの環境とDBが分離していればよほど問題にならない。
私の場合Dockerのコンテナに両方を入れているので新しい環境にデプロイした場合データベースの同期をしない限り新しいデータベースが作成される。
そうすると新規でデータベースを作る時に次のような問題が生じる。

モデルの変更が一度もない場合だと`flask db migrate && flask db upgrade`ですんなりテーブル作成できる。
そうでない場合、`flask db migrate`は更新の差分情報しかもっていないので`flask db upgrade`でこける。

# こんなふうにしてみた

新しい環境にアプリを移行してデータベースを初期化する場合は以下のような回りくどい手順を実行すればなんとかなる。

## 現在のリビジョンを調べる

元となるmigrationsのリビジョンを調べる。
またはmigrations/versions/配下のファイルを漁るかmysqlなら

```shell
mysql> select * from alembic_version
+--------------+
| version_num  |
+--------------+
| abcdefghijkl |
+--------------+
```

最新リビジョンは`abcdefghijkl`だとわかる。

> flask db history というコマンドもあるようだが使ったこと無いので未確認

## 新規migrationsを作成する

次に新しい環境にデプロイされたmigrationsディレクトリを一旦退避する。

```shell
mv migrations migrations.bak
```

新しい環境で初期化の三点セットを実行

```shell
flask db init
flask db migrate
flask db upgrade
```
ここで発行されるリビジョンが`xxxxxxxxxx`だとする。

# 元のリビジョン番号に戻す

migrationsを元に戻す。

```shell
mv migrations migrations.new
mv migrations.bak migrations
```

最後にテーブルを書き換える

```sql
update into alembic_version set version_num='abcdefghijkl' where version_num='xxxxxxxxxxxx'
```

以上。


とここまで書いたが。。。
自分の書いたREADMEを読み直してみると

# 新しい環境でデータベースを初期化したい場合はきっとこちらが正しい

最初にユーザとデータベースの作成

```mysql
GRANT ALL PRIVILEGES ON *.* TO foo@"%" IDENTIFIED BY 'foo' WITH GRANT OPTION;
CREATE DATABASE mysite;
```

モデルからテーブルを作成

```shell
FLASK_APP=run:app flask shell
>>> from app.database import db
>>> db.create_all()
>>> db.session.commit()
```

マイグレーション実行

```shell
FLASK_APP=run:app flask db migrate
FLASK_APP=run:app flask db stamp head
FLASK_APP=run:app flask db upgrade
```

## 参考

`run.py`

```python
import os

from app import create_app

config = os.getenv('FLASK_ENV', 'production')
app = create_app(config)

if __name__=='__main__':
    app.run(host='0.0.0.0', debug=True)
```

`config.py`(抜粋)

```python
# 省略

class Config(object):
    ## Default constants
    DEBUG = False
    TESTING = False
    DEVELOPMENT = False

    # 省略

class ProductionConfig(Config):
    pass

class StagingConfig(Config):
    DEVELOPMENT = True

class DevelopmentConfig(Config):
    DEBUG = True
    DEVELOPMENT = True

class TestingConfig(Config):
    DEBUG = True
    TESTING = True

app_config = {
    'production': ProductionConfig,
    'staging': StagingConfig,
    'development': DevelopmentConfig,
    'testing':  TestingConfig,
}
```

`app/__init__.py`(抜粋)

```python
# 省略
from flask import Flask

from config import app_config


def create_app(config, local_config='config.py'):
    # create flask instance
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_object(app_config[config])

    # 省略

    return app
```

`app/database.py`

```python
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()

def init_db(app, migrate_dir=None):
    db.init_app(app)
    Migrate(app, db, migrate_dir) if migrate_dir else Migrate(app, db)
```

とある。うっ汗。
いやまあ手動でリビジョン直したい場合がどこかであるかもってことで。。。(いや、実際にどこかであったぞ)