LoginSignup
8
11

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-05-18

はじめに

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

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

DBマイグレーションとは

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

※DeepL翻訳

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

ORMORマッパーとは

O/Rマッピングとは、オブジェクト指向プログラミング言語におけるオブジェクトとリレーショナルデータベース(RDB)の間でデータ形式の相互変換を行うこと。そのための機能やソフトウェアを「O/Rマッパー」(O/R mapper)という。
参照元:e-words

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

FlaskのDBマイグレーションで使うライブラリの準備

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

$ 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

自分の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テーブルにSeedsINSERTをする実践

main.pyでアプリ起動

  • DB関連の初期化
  • アプリ起動
  • マイグレーションの自動実行
  • 必要に応じてSeedingの自動実行
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文は自動生成される※後述)
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で設定したコンスタントを使って各種プロパティを指定。

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で環境変数を定数にセット

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で環境変数を管理

=の左右に空白を入れないことがポイント
下記のように、くっつける。

.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__ に格納すると、リストで指定したモジュールのみをインポートするように制限できる。

models.py
from .users import User, UserSchema

__all__ = [
    User, UserSchema
]

users.pyにテーブルスキーマを定義

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

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スクリプト実行用ファイルを別に用意します。

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.shMigrationの初期化

migrationsディレクトリと関連ファイル・versionsフォルダが自動生成されます。

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

exec_migration.sh

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

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データとして入れたいクラスを保管する

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

seed_users.pyで投入するseedデータを定義

  • 上記のshellrunを走らせると、3人のユーザーが登録されるように指定したseed定義
  • FakerクラスのUserモデルを渡し、テーブルの全カラム情報を渡す
    • カラムの記述を省略するとエラーが出た。
  • idはautoincrement設定なので、None(=null)に。
  • created_at, updated_atは自動でtimestampの値が入るように設定しているので、None(=null)に。
  • nameは正規表現で自動生成。
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ファイルを下記で共有します。

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

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

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とかやります。

$ 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

d773119f6e29_.pyが生成された

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

Downgrade:一つ前に戻してみる

DockerFlaskアプリを起動してるもんで、Dockerコンテナ内に入り込んで、ダウングレードを実行します。
(※大半はログ出力です)

$ 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

version_numが前回のものに変更された(ダウングレードされた)

スクリーンショット 2021-05-18 17.54.06.png

Upgrade:現在のバージョンにアップグレード

さきほどダウングレードしたので、アップグレードしてみます〜
(※大半はログ出力です)

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

version_numが最新のものに更新された(アップグレードされた)

スクリーンショット 2021-05-18 17.57.09.png

次はSeedingを確認

seed_users.pyの修正

上記のマイグレーションでemailカラムを入れたので、Fakerにも追加します。

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.pymigration.exec_seed()を実行するので、
単純に起動してみます!

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

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

usersテーブルの中身を確認

自動で三人分のユーザーが挿入されました!
スクリーンショット 2021-05-18 18.14.49.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

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

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

8
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11