【Python】お、手軽！FlaskフレームワークのORM拡張でDBマイグレーションを実装してみた

#はじめに
先日、社内のオンライン開発合宿というイベント向けに、アプリケーションを作る一環として、
（コロナ禍もあってオンライン...）

https://qiita.com/ichi_zamurai/items/ebf9c0d2c4ea3fafe63e

REST APIの作成は完了したので、
今度は**FlaskにおけるDBマイグレーションの実装とSeeding（テストデータのINSERT処理）**を
実装・検証したので、共有したいと思います。


#DBマイグレーションとは
>ソフトウェア工学において、スキーママイグレーション（データベースマイグレーション、データベースチェンジマネジメント）とは、リレーショナルデータベースのスキーマに対する増分的、可逆的な変更とバージョン管理の管理を指す。スキーママイグレーションは、データベースのスキーマを更新したり、新しいバージョンや古いバージョンに戻したりする必要がある場合に、データベースに対して実行されます。
参照元：[wikipedia](https://en.wikipedia.org/wiki/Schema_migration)

※DeepL翻訳

ふむふむ、データベースのスキーマ管理、バージョン管理って意味合いですな。

#`ORM`ORマッパーとは
>O/Rマッピングとは、オブジェクト指向プログラミング言語におけるオブジェクトとリレーショナルデータベース（RDB）の間でデータ形式の相互変換を行うこと。そのための機能やソフトウェアを「O/Rマッパー」（O/R mapper）という。
参照元：[e-words](https://e-words.jp/w/O-R%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0.html)

モデルとRDBのデータ形式の相互互換を便利にしてくれるツールってことか。
今回は`Flask-SQLAlchemy`を使ってるな。

#FlaskのDBマイグレーションで使うライブラリの準備
Flask自体と、DBマイグレーションに関連する拡張ライブラリをインストールしていきます。
※[#] 以降に2021/05/18時点のversionを記載

```bash
$ pip install Flask # 1.1.2 => Flaskフレームワーク
$ pip install python-dotenv # 0.17.1 => 環境変数用
$ pip install PyMySql # 1.0.2 => MySQL接続用ドライバー
$ pip install Flask-SQLAlchemy # 2.5.1 => DB操作用
$ pip install flask-marshmallow # 0.14.0 => モデルのスキーマ管理
$ pip install marshmallow-sqlalchemy # 0.25.0 => SQLAlchemyとmarshmallowの依存解決
$ pip install Flask-Migrate # 3.0.0 => FlaskアプリのDBマイグレーション管理
$ pip install Flask-Seeder # 1.2.0 => Seeder
```
https://qiita.com/ichi_zamurai/items/dcbac977c1ff367b9094


#自分のFlaskアプリ:DBマイグレーション関連のファイル構成
※`REST API`作った時のファイルは関係ないので省略。

```
src
├── .env                  #環境変数の登録
├── main.py               #アプリ起動
├── db.py                 #dbインスタンスの初期化
├── settings.py           #[.env]から環境変数の読込と設定
├── migration             #自作のmigrationフォルダ
│   ├── migration.py      #マイグレーションを実行するSubprocess実行メソッドを格納
│   ├── exec_seed.sh      #Seedingを実行するシェルスクリプト
│   ├── exec_migration.sh        #Migrationを実行するシェルスクリプト
│   └── initialize_migration.sh  #Migrationを初期化するシェルスクリプト
├── migrations                   #初期化するとFlask-Migrateによって自動生成されるフォルダ
│   └── versions                 #Migrationの履歴バージョンを持つフォルダ
│       ├── 3679caf6e0bd_.py     #Userモデルを作成した時の最初のMigrationファイル
│       └── d773119f6e29_.py     #変更加えた時の2回目のMigrationファイル
├── model
│   ├── models.py       #定義したモデルモジュールを一括管理するファイル
│   └── users.py        #モデル
├── seeds               #seed用のファイルの管理フォルダ
│   └── seed_users.py   #Seed用クラス - モデルと対応
└── config
    └── config.py       #DBの各種設定
```

#Flask(Python)で書いたコード
各ファイル自体の目的を明確にして、その用途にあった必要なコードのみの記述を意識しました。


##今回の目的：FlaskのDBマイグレーションで主にやること
- UserモデルとDBスキーマとの整合性と変更履歴の確認
- usersテーブルに`Seeds`を`INSERT`をする実践

##`main.py`でアプリ起動
- DB関連の初期化
- アプリ起動
- マイグレーションの自動実行
- 必要に応じてSeedingの自動実行

```python:main.py
#!/usr/bin/python3
from flask import Flask
import os
from config import config
import db
# from model import models
from migration import migration

def create_app():
    # Generate Flask App Instance
    app = Flask(__name__)

    # Read DB setting & Initialize
    app.config.from_object(config.Config)
    db.init_db(app)
    db.init_ma(app)
    db.init_seeder(app)

    return app

app = create_app()
if __name__ == "__main__":
    # Migrate before running App 
    if not os.path.exists('./migrations'):
        # Run only when the migrations dir doesn't exist
        migration.initialize_migration()
    migration.exec_migration()
    # Comment out when unnecessary
    migration.exec_seed()

    # Run Flask App
    app.run(host='0.0.0.0', debug=True, port=8080, threaded=True, use_reloader=False)
    
```

##`db.py`でdbインスタンスの初期化
- `db`インスタンスの生成
- `Marshmallow`という`SQLAlchemy`で受け取ったデータをJSONに変換してくれるライブラリも起動して、`Flask`アプリのappに関連づけ
- `seeder`インスタンスの生成
- 起動中の`app`と作成した`db`を持って、`Flask-Migrate`に設定し、マイグレーション機能を立ち上げ
    - 例えば、Userモデルを変更してから、マイグレーションを走らせると、差分が履歴として残り、バージョン管理できるようになります。（変更SQL文は自動生成される※後述）


```python:db.py
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from flask_seeder import FlaskSeeder

db = SQLAlchemy()
ma = Marshmallow()
seeder = FlaskSeeder()

def init_db(app):
    db.init_app(app)
    Migrate(app, db)

def init_ma(app):
    ma.init_app(app)

def init_seeder(app):
    seeder.init_app(app, db)
```

##`config.py`でdb情報の設定を管理
`settings.py`で設定したコンスタントを使って各種プロパティを指定。

```python:config.py
import settings

class SystemConfig:
    # Flask
    DEBUG = True

    # SQLAlchemy
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{db}?charset=utf8mb4'.format(**{
        'user': settings.MYSQL_USER,
        'password': settings.MYSQL_PASSWORD,
        'host': settings.MYSQL_HOST,
        'db': settings.MYSQL_DATABASE
    })
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ECHO = True # Print executed SQL 

Config = SystemConfig
```


##`settings.py`で環境変数を定数にセット
```python:settings.py
# coding: UTF-8
import os
from os.path import join, dirname
from dotenv import load_dotenv

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

# MySQL
MYSQL_ROOT_PASSWORD = os.environ.get("MYSQL_ROOT_PASSWORD")
MYSQL_HOST = os.environ.get("MYSQL_HOST")
MYSQL_DATABASE = os.environ.get("MYSQL_DATABASE")
MYSQL_USER = os.environ.get("MYSQL_USER")
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD")
```


##`.env`で環境変数を管理
**`=`の左右に空白を入れないことがポイント**
下記のように、くっつける。

```text:.env
MYSQL_ROOT_PASSWORD=root_password
MYSQL_HOST=host
MYSQL_DATABASE=db
MYSQL_USER=user
MYSQL_PASSWORD=password
```

##`models.py`に定義したモデルを一括登録して呼び出しやすくする工夫
`import`するときに、`models`だけimportすればモデルを簡単に呼び出せるようにしました。
\_\_all\_\_ = ["module1", "module2", "module3", ...]
のように、モジュールをリストで \_\_all\_\_ に格納すると、リストで指定したモジュールのみをインポートするように制限できる。

```python:models.py
from .users import User, UserSchema

__all__ = [
    User, UserSchema
]
```

##`users.py`にテーブルスキーマを定義
ファイル名は、テーブル名と同じにし、Userモデルの定義と、CRUD系のメソッドを用意します。
`Marshmallow`でテーブルのスキーマ管理をしています。

```python:users.py
from db import db, ma
from sqlalchemy.dialects.mysql import TIMESTAMP as Timestamp
from sqlalchemy.sql.functions import current_timestamp

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, autoincrement=True, primary_key=True)
    name = db.Column(db.String(225), nullable=False)
    created_at = db.Column(Timestamp, server_default=current_timestamp(), nullable=False)
    created_by = db.Column(db.String(225), nullable=True)
    updated_at = db.Column(Timestamp, server_default=current_timestamp(), nullable=False)
    updated_by = db.Column(db.String(225), nullable=True)

    def __init__(self, id, name, created_at, created_by, updated_at, updated_by):
        self.id = id
        self.name = name
        self.created_at = created_at
        self.created_by = created_by
        self.updated_at = updated_at
        self.updated_by = updated_by

    def __repr__(self):
         return '<User %r>' % self.name

    def get_user_list():
        # SELECT * FROM users
        user_list = db.session.query(User).all()
        if user_list == None:
            return []
        else:
            return user_list

    def create_user(user):
        record = User(
            name = user['name'],
        )
        # INSERT INTO users(name) VALUES(...)
        db.session.add(record)
        db.session.commit()
        return user

    def get_user_by_id(id):
        return db.session.query(User)\
            .filter(User.id == id)\
            .one()

# Difinition of User Schema with Marshmallow
# refer: https://flask-marshmallow.readthedocs.io/en/latest/
class UserSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
      model = User
    #   fields = ('id', 'name', 'created_at', 'created_by', 'updated_at', 'updated_by')
```


##`migration.py`に、マイグレーション実行用Subprocessメソッドを格納
`Subprocess`では、`shell`スクリプト実行用ファイルを別に用意します。

```python:migration.py
import subprocess
from subprocess import PIPE

def initialize_migration():
    subprocess.run("./migration/initialize_migration.sh", shell=False, stdout=PIPE, stderr=PIPE, text=True)

def exec_migration():
    subprocess.run("./migration/exec_migration.sh", shell=False, stdout=PIPE, stderr=PIPE, text=True)

def exec_seed():
    subprocess.run("./migration/exec_seed.sh", shell=False, stdout=PIPE, stderr=PIPE, text=True)
```

###`initialize_migration.sh`で`Migration`の初期化
**`migrations`**ディレクトリと関連ファイル・`versions`フォルダが自動生成されます。

```bash:initialize_migration.sh
#!/bin/bash
FLASK_APP=main.py flask db init
```

###`exec_migration.sh`
Userモデルの中身に変更があった場合に、マイグレーションが走ります。
テーブルスキーマのバージョン管理ができるようになります。

```bash:exec_migration.sh
#!/bin/bash
FLASK_APP=main.py flask db migrate
FLASK_APP=main.py flask db upgrade
```

###`exec_seed.sh`
`seed`は簡単に言うと初期データのことで、作成したテーブルに初期データやテストデータなどを投入する目的で使います。
**※seedsディレクトリにseedデータとして入れたいクラスを保管する**

```bash:exec_seed.sh
#!/bin/bash
FLASK_APP=main.py flask seed run
```

##`seed_users.py`で投入する`seed`データを定義
- 上記の`shell`で`run`を走らせると、３人のユーザーが登録されるように指定したseed定義
- `Faker`クラスのUserモデルを渡し、テーブルの全カラム情報を渡す
    - カラムの記述を省略するとエラーが出た。
- `id`はautoincrement設定なので、`None(=null)`に。
- `created_at, updated_at`は自動で`timestamp`の値が入るように設定しているので、`None(=null)`に。
- `name`は正規表現で自動生成。

```python:seed_users.py
from flask_seeder import Seeder, Faker, generator
import sys
sys.path.append('../')
from model import models

class UserSeeder(Seeder):

    # Refer: https://pypi.org/project/Flask-Seeder/
    # Lower priority will be run first. All seeders with the same priority are then ordered by class name.
    # def __init__(self, db=None):
    #     super().__init__(db=db)
    #     self.priority = 1

    # run() will be called by Flask-Seeder
    def run(self):
        # Create a new Faker and tell it how to create User objects
        faker = Faker(
            cls=models.User,
            init={
                "id": None,
                "name": generator.String('[a-z]\d{4}\c{3}'),
                "created_at": None,
                "created_by": 'system',
                "updated_at": None,
                "updated_by": ''
            }
        )

        # Create 3 users
        for user in faker.create(3):
            print("Adding user: %s" % user)
            # Flask-Seeder will by default commit all changes to the database.
            self.db.session.add(user)
```

#`migrations`ディレクトリでマイグレーションのバージョン管理
`Flask-Migrate`は、`Alembic`というデータベースマイグレーションツールが主体となっているライブラリの模様。 データベースのスキーマ変更に対し、`Alembic`を使うと、`Python`コードで管理できるように。
Upgrate（現在）とDowngrade(一つ前)の情報をもっている。

**※すでに一回マイグレーションを走らせたので、生成されたpythonファイルを下記で共有します。**

```python:3679caf6e0bd_.py
"""empty message
Revision ID: 3679caf6e0bd
Revises: 
Create Date: 2021-05-17 13:11:35.394376
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

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

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
    sa.Column('name', sa.String(length=225), nullable=False),
    sa.Column('created_at', mysql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
    sa.Column('created_by', sa.String(length=225), nullable=True),
    sa.Column('updated_at', mysql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
    sa.Column('updated_by', sa.String(length=225), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###
```

#Flask マイグレーション & Seedの実践と動作検証！！

##まずはマイグレーションを実践

###DBのテーブルの状態
変更後、`email`カラムが、追加されればいい感じに動いてる証拠です。
![スクリーンショット 2021-05-18 16.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/ed8153fa-87ec-f81a-62e4-2159cdbb8bf4.png)


###Userモデルに変更を加えます。

```python:users.py
...
from sqlalchemy.schema import UniqueConstraint # ★変更点

class User(db.Model):
    __tablename__ = 'users'
    __table_args__=(UniqueConstraint('email', name='uq_email'),) # ★変更点
    id = db.Column('id', db.Integer, autoincrement=True, primary_key=True)
    name = db.Column('name', db.String(225), nullable=False)
    created_at = db.Column('created_at', Timestamp, server_default=current_timestamp(), nullable=False)
    created_by = db.Column('created_by', db.String(225), nullable=True)
    updated_at = db.Column('updated_at', Timestamp, server_default=current_timestamp(), nullable=False)
    updated_by = db.Column('updated_by', db.String(225), nullable=True)
    email = db.Column('email', db.String(225), nullable=True) # ★変更点

    def __init__(self, id, name, created_at, created_by, updated_at, updated_by, email):
        self.id = id
        self.name = name
        self.created_at = created_at
        self.created_by = created_by
        self.updated_at = updated_at
        self.updated_by = updated_by
        self.email = email # ★変更点
        ...
```
###アプリを起動！！
`exec_migration.sh`が自動で走るようにしているので、単純に起動してみます！

※`Docker`で管理しているもんで、`docker-compose up`で起動します。(突然すみません。)
※通常は`python main.py`とか`FLASK_APP=main.py flask run`とかやります。

```bash
$ cd /Users/username/src/github.com/flask-challenge
$ docker-compose up
Docker Compose is now in the Docker CLI, try `docker compose up`

Creating db ... done
Creating api ... done
Creating nginx ... done
Attaching to api
api      |  * Serving Flask app "main" (lazy loading)
api      |  * Environment: production
api      |    WARNING: This is a development server. Do not use it in a production deployment.
api      |    Use a production WSGI server instead.
api      |  * Debug mode: on
api      | [2021-05-18 17:38:50,031] [WARNING] :  * Running on all addresses.
api      |    WARNING: This is a development server. Do not use it in a production deployment.
api      | [2021-05-18 17:38:50,032] [INFO] :  * Running on http://172.20.0.3:8080/ (Press CTRL+C to quit)
api      | [2021-05-18 17:38:50,033] [INFO] :  * Restarting with stat
api      | [2021-05-18 17:38:52,157] [WARNING] :  * Debugger is active!
api      | [2021-05-18 17:38:52,159] [INFO] :  * Debugger PIN: 996-590-540
```

###テーブルの状態は？
見事、`email`カラムが追加されました!（一番右）
Userモデルでは`name`の後に記述したが、カラム追加自体は最後になってしまたなあ。
どうやらカラムの追加位置は指定できないようなので、一番後方の列に配置されるみたいです。
![スクリーンショット 2021-05-18 16.23.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/83e53825-1ec6-c57c-5421-fb1eedc04262.png)

###`d773119f6e29_.py`が生成された
```python:d773119f6e29_.py
"""empty message
Revision ID: d773119f6e29
Revises: 3679caf6e0bd
Create Date: 2021-05-18 17:40:33.323995
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'd773119f6e29'
down_revision = '3679caf6e0bd'
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('email', sa.String(length=225), nullable=True))
    op.create_unique_constraint('uq_email', 'users', ['email'])
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_constraint('uq_email', 'users', type_='unique')
    op.drop_column('users', 'email')
    # ### end Alembic commands ###
```
###自動追加された`alembic_version`テーブルの中身を見てみる。
実行したバージョンのUUIDが挿入されてますな。
![スクリーンショット 2021-05-18 17.46.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/fd414a0c-ceb3-f7d4-a8d9-204e22898275.png)


###`Downgrade`:一つ前に戻してみる
`Docker`で`Flask`アプリを起動してるもんで、`Docker`コンテナ内に入り込んで、ダウングレードを実行します。
（※大半はログ出力です）

```bash
$ docker exec -it api bash #起動しているdockerコンテナ「api」の中に入る
root@c10fa2095639:/api/src# FLASK_APP=main.py flask db downgrade #ダウングレード実行
　2021-05-18 17:47:08,090 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'sql_mode'
　INFO  [sqlalchemy.engine.Engine] SHOW VARIABLES LIKE 'sql_mode'
　2021-05-18 17:47:08,090 INFO sqlalchemy.engine.Engine [raw sql] {}
　INFO  [sqlalchemy.engine.Engine] [raw sql] {}
　2021-05-18 17:47:08,093 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'lower_case_table_names'
　INFO  [sqlalchemy.engine.Engine] SHOW VARIABLES LIKE 'lower_case_table_names'
　2021-05-18 17:47:08,093 INFO sqlalchemy.engine.Engine [generated in 0.00021s] {}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00021s] {}
　2021-05-18 17:47:08,095 INFO sqlalchemy.engine.Engine SELECT DATABASE()
　INFO  [sqlalchemy.engine.Engine] SELECT DATABASE()
　2021-05-18 17:47:08,095 INFO sqlalchemy.engine.Engine [raw sql] {}
　INFO  [sqlalchemy.engine.Engine] [raw sql] {}
　2021-05-18 17:47:08,097 INFO sqlalchemy.engine.Engine SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %(table_schema)s AND table_name = %(table_name)s
　INFO  [sqlalchemy.engine.Engine] SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %(table_schema)s AND table_name = %(table_name)s
　2021-05-18 17:47:08,097 INFO sqlalchemy.engine.Engine [generated in 0.00016s] {'table_schema': 'flask-challenge', 'table_name': 'alembic_version'}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00016s] {'table_schema': 'flask-challenge', 'table_name': 'alembic_version'}
　2021-05-18 17:47:08,099 INFO sqlalchemy.engine.Engine SELECT alembic_version.version_num FROM alembic_version
　INFO  [sqlalchemy.engine.Engine] SELECT alembic_version.version_num FROM alembic_version
　2021-05-18 17:47:08,099 INFO sqlalchemy.engine.Engine [generated in 0.00014s] {}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00014s] {}
　2021-05-18 17:47:08,103 INFO sqlalchemy.engine.Engine BEGIN (implicit)
　INFO  [sqlalchemy.engine.Engine] BEGIN (implicit)
　2021-05-18 17:47:08,105 INFO sqlalchemy.engine.Engine ALTER TABLE users DROP INDEX uq_email
　INFO  [sqlalchemy.engine.Engine] ALTER TABLE users DROP INDEX uq_email
　2021-05-18 17:47:08,105 INFO sqlalchemy.engine.Engine [no key 0.00015s] {}
　INFO  [sqlalchemy.engine.Engine] [no key 0.00015s] {}
　2021-05-18 17:47:08,133 INFO sqlalchemy.engine.Engine ALTER TABLE users DROP COLUMN email
　INFO  [sqlalchemy.engine.Engine] ALTER TABLE users DROP COLUMN email
　2021-05-18 17:47:08,133 INFO sqlalchemy.engine.Engine [no key 0.00016s] {}
　INFO  [sqlalchemy.engine.Engine] [no key 0.00016s] {}
　2021-05-18 17:47:08,206 INFO sqlalchemy.engine.Engine UPDATE alembic_version SET version_num='3679caf6e0bd' WHERE alembic_version.version_num = 'd773119f6e29'
　INFO  [sqlalchemy.engine.Engine] UPDATE alembic_version SET version_num='3679caf6e0bd' WHERE alembic_version.version_num = 'd773119f6e29'
　2021-05-18 17:47:08,206 INFO sqlalchemy.engine.Engine [generated in 0.00028s] {}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00028s] {}
　2021-05-18 17:47:08,207 INFO sqlalchemy.engine.Engine COMMIT
　INFO  [sqlalchemy.engine.Engine] COMMIT
```
###ダウングレード後のテーブルの状態を確認
####`email`カラムが削除された
![スクリーンショット 2021-05-18 17.52.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/2dc61a0d-7bed-d4c2-2dfb-2ffce8636e1e.png)
####`version_num`が前回のものに変更された(ダウングレードされた)
![スクリーンショット 2021-05-18 17.54.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/dc7d9b9d-4ab4-d1c3-5bf2-5e69316be8ad.png)



###`Upgrade`：現在のバージョンにアップグレード
さきほどダウングレードしたので、アップグレードしてみます〜
（※大半はログ出力です）

```bash
root@c10fa2095639:/api/src# FLASK_APP=main.py flask db upgrade
　2021-05-18 17:56:09,426 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'sql_mode'
　INFO  [sqlalchemy.engine.Engine] SHOW VARIABLES LIKE 'sql_mode'
　2021-05-18 17:56:09,426 INFO sqlalchemy.engine.Engine [raw sql] {}
　INFO  [sqlalchemy.engine.Engine] [raw sql] {}
　2021-05-18 17:56:09,429 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'lower_case_table_names'
　INFO  [sqlalchemy.engine.Engine] SHOW VARIABLES LIKE 'lower_case_table_names'
　2021-05-18 17:56:09,429 INFO sqlalchemy.engine.Engine [generated in 0.00014s] {}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00014s] {}
　2021-05-18 17:56:09,431 INFO sqlalchemy.engine.Engine SELECT DATABASE()
　INFO  [sqlalchemy.engine.Engine] SELECT DATABASE()
　2021-05-18 17:56:09,431 INFO sqlalchemy.engine.Engine [raw sql] {}
　INFO  [sqlalchemy.engine.Engine] [raw sql] {}
　2021-05-18 17:56:09,433 INFO sqlalchemy.engine.Engine SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %(table_schema)s AND table_name = %(table_name)s
　INFO  [sqlalchemy.engine.Engine] SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %(table_schema)s AND table_name = %(table_name)s
　2021-05-18 17:56:09,433 INFO sqlalchemy.engine.Engine [generated in 0.00017s] {'table_schema': 'flask-challenge', 'table_name': 'alembic_version'}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00017s] {'table_schema': 'flask-challenge', 'table_name': 'alembic_version'}
　2021-05-18 17:56:09,435 INFO sqlalchemy.engine.Engine SELECT alembic_version.version_num FROM alembic_version
　INFO  [sqlalchemy.engine.Engine] SELECT alembic_version.version_num FROM alembic_version
　2021-05-18 17:56:09,435 INFO sqlalchemy.engine.Engine [generated in 0.00014s] {}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00014s] {}
　2021-05-18 17:56:09,439 INFO sqlalchemy.engine.Engine BEGIN (implicit)
　INFO  [sqlalchemy.engine.Engine] BEGIN (implicit)
　2021-05-18 17:56:09,441 INFO sqlalchemy.engine.Engine ALTER TABLE users ADD COLUMN email VARCHAR(225)
　INFO  [sqlalchemy.engine.Engine] ALTER TABLE users ADD COLUMN email VARCHAR(225)
　2021-05-18 17:56:09,441 INFO sqlalchemy.engine.Engine [no key 0.00015s] {}
　INFO  [sqlalchemy.engine.Engine] [no key 0.00015s] {}
　2021-05-18 17:56:09,514 INFO sqlalchemy.engine.Engine ALTER TABLE users ADD CONSTRAINT uq_email UNIQUE (email)
　INFO  [sqlalchemy.engine.Engine] ALTER TABLE users ADD CONSTRAINT uq_email UNIQUE (email)
　2021-05-18 17:56:09,514 INFO sqlalchemy.engine.Engine [no key 0.00016s] {}
　INFO  [sqlalchemy.engine.Engine] [no key 0.00016s] {}
　2021-05-18 17:56:09,547 INFO sqlalchemy.engine.Engine UPDATE alembic_version SET version_num='d773119f6e29' WHERE alembic_version.version_num = '3679caf6e0bd'
　INFO  [sqlalchemy.engine.Engine] UPDATE alembic_version SET version_num='d773119f6e29' WHERE alembic_version.version_num = '3679caf6e0bd'
　2021-05-18 17:56:09,547 INFO sqlalchemy.engine.Engine [generated in 0.00025s] {}
　INFO  [sqlalchemy.engine.Engine] [generated in 0.00025s] {}
　2021-05-18 17:56:09,548 INFO sqlalchemy.engine.Engine COMMIT
　INFO  [sqlalchemy.engine.Engine] COMMIT

```

###アップグレード後のテーブルの状態を確認
####`email`カラムが再度追加された
![スクリーンショット 2021-05-18 17.57.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/a46f98dc-4878-3fda-79c9-5d9814fbfab3.png)


####`version_num`が最新のものに更新された(アップグレードされた)
![スクリーンショット 2021-05-18 17.57.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/144a1c84-7b96-be53-f73a-40592080441e.png)


##次は`Seeding`を確認
###`seed_users.py`の修正
上記のマイグレーションで`email`カラムを入れたので、`Faker`にも追加します。

```python:seed_users.py
...
class UserSeeder(Seeder):
    def run(self):
        faker = Faker(
            cls=models.User,
            init={
                "id": None,
                "name": generator.String('[a-z]\d{4}\c{3}'),
                "created_at": None,
                "created_by": 'system',
                "updated_at": None,
                "updated_by": '',
                "email": generator.String('[a-z]\d{4}\c{3}@test.com') # ★変更点
            }
        )
...
```

###アプリを起動！！
`exec_seed.sh`が自動で走るように
`main.py`で`migration.exec_seed()`を実行するので、
単純に起動してみます！

※`Docker`で管理しているもんで、`docker-compose up`で起動します。(度々すみません。)
※通常は`python main.py`とか`FLASK_APP=main.py flask run`とかですね。

```bash
$ cd /Users/username/src/github.com/flask-challenge
$ docker-compose up
```

###`users`テーブルの中身を確認
自動で三人分のユーザーが挿入されました！
![スクリーンショット 2021-05-18 18.14.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/b63faa26-d32a-002a-a21e-a659e60ec29e.png)


###MigrationもSeedingも動作確認できた✨
動くと嬉しす✨

#Flaskのマイグレーションでハマったポイント
- `Flask-Seeder`を実行した時に、DBへの接続エラーが出て、最初全然データがDBに挿入されずに、「なんでだなんでだ」って困っていたけど、今回ローカルの`Docker`上で`Flask`アプリを起動してたもんだから、実際にログ出力されていたのは、そのコンテナ内部だった。原因に全然気づけなくて時間ロスorz
    - `Faker`の中に定義するカラムは、モデルで記述したカラムと一致させる必要があったが最初省略してしまっていた
    - 単純にカラムに入れようとしたデータタイプの問題だった汗
- `Users`モデルにユニークな`email`カラムを追加したが、単一ユニークキーに名前づけがされていなくて、`Downgrade`で失敗してしまった。
    - `from sqlalchemy.schema import UniqueConstraint`を使用して名前をつける必要があった。
    - `__table_args__=(UniqueConstraint('email', name='uq_email'),)`
        - **最後の[,]に注目：未知の謎多きタプル型にしないとエラーが出てハマった**
            - タプルで単体の場合は、カンマが必要らしい。
- `Flask`アプリを起動する時に、`use_reloader=False`を使用しないと、アプリを起動した時に、なぜか自動でアプリが再起動して、`Seeder`が二回走ってしまい、二重登録してしまった。


#感想とまとめ
なかなか動作検証のところで、つまづいて、学びが多かったですねえ。

やはり表面上で実装しただけだと学べないことが、実践の中では得られるので、
今回は大いに、知見を得られたんじゃないかと思います。


マイグレーションを自分で実装したのは初めてだったので、
かつ`Python`も勉強になり、
かつ`Flask`も勉強になり、

**楽しさ倍増でした(☝︎ ՞ਊ ՞)☝︎（ぶちあげぇぇぇええええええ!!!!!!!!!!）**

これで、DBのバージョン管理ができるようになったので、
実際のシステム開発等に活かせるんじゃないかなって、少し自分の中で期待が膨らんでいるw

個人開発の枠を超えて、自分で作ったアーキテクチャで開発やってみたいもんですねぇ。



以上、ありがとうございました。