#はじめに
先日、社内のオンライン開発合宿というイベント向けに、アプリケーションを作る一環として、
(コロナ禍もあってオンライン...)
REST APIの作成は完了したので、
今度は**FlaskにおけるDBマイグレーションの実装とSeeding(テストデータのINSERT処理)**を
実装・検証したので、共有したいと思います。
#DBマイグレーションとは
ソフトウェア工学において、スキーママイグレーション(データベースマイグレーション、データベースチェンジマネジメント)とは、リレーショナルデータベースのスキーマに対する増分的、可逆的な変更とバージョン管理の管理を指す。スキーママイグレーションは、データベースのスキーマを更新したり、新しいバージョンや古いバージョンに戻したりする必要がある場合に、データベースに対して実行されます。
参照元:wikipedia
※DeepL翻訳
ふむふむ、データベースのスキーマ管理、バージョン管理って意味合いですな。
#ORM
ORマッパーとは
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テーブルに
Seeds
をINSERT
をする実践
##main.py
でアプリ起動
- DB関連の初期化
- アプリ起動
- マイグレーションの自動実行
- 必要に応じてSeedingの自動実行
#!/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文は自動生成される※後述)
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
で設定したコンスタントを使って各種プロパティを指定。
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
で環境変数を定数にセット
# 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
で環境変数を管理
=
の左右に空白を入れないことがポイント
下記のように、くっつける。
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__ に格納すると、リストで指定したモジュールのみをインポートするように制限できる。
from .users import User, UserSchema
__all__ = [
User, UserSchema
]
##users.py
にテーブルスキーマを定義
ファイル名は、テーブル名と同じにし、Userモデルの定義と、CRUD系のメソッドを用意します。
Marshmallow
でテーブルのスキーマ管理をしています。
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
スクリプト実行用ファイルを別に用意します。
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
フォルダが自動生成されます。
#!/bin/bash
FLASK_APP=main.py flask db init
###exec_migration.sh
Userモデルの中身に変更があった場合に、マイグレーションが走ります。
テーブルスキーマのバージョン管理ができるようになります。
#!/bin/bash
FLASK_APP=main.py flask db migrate
FLASK_APP=main.py flask db upgrade
###exec_seed.sh
seed
は簡単に言うと初期データのことで、作成したテーブルに初期データやテストデータなどを投入する目的で使います。
※seedsディレクトリにseedデータとして入れたいクラスを保管する
#!/bin/bash
FLASK_APP=main.py flask seed run
##seed_users.py
で投入するseed
データを定義
- 上記の
shell
でrun
を走らせると、3人のユーザーが登録されるように指定したseed定義 -
Faker
クラスのUserモデルを渡し、テーブルの全カラム情報を渡す- カラムの記述を省略するとエラーが出た。
-
id
はautoincrement設定なので、None(=null)
に。 -
created_at, updated_at
は自動でtimestamp
の値が入るように設定しているので、None(=null)
に。 -
name
は正規表現で自動生成。
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ファイルを下記で共有します。
"""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
カラムが、追加されればいい感じに動いてる証拠です。
###Userモデルに変更を加えます。
...
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
の後に記述したが、カラム追加自体は最後になってしまたなあ。
どうやらカラムの追加位置は指定できないようなので、一番後方の列に配置されるみたいです。
###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が挿入されてますな。
###Downgrade
:一つ前に戻してみる
Docker
でFlask
アプリを起動してるもんで、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
カラムが削除された
####version_num
が前回のものに変更された(ダウングレードされた)
###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
カラムが再度追加された
####version_num
が最新のものに更新された(アップグレードされた)
##次はSeeding
を確認
###seed_users.py
の修正
上記のマイグレーションでemail
カラムを入れたので、Faker
にも追加します。
...
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
とかですね。
$ cd /Users/username/src/github.com/flask-challenge
$ docker-compose up
###users
テーブルの中身を確認
自動で三人分のユーザーが挿入されました!
###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
個人開発の枠を超えて、自分で作ったアーキテクチャで開発やってみたいもんですねぇ。
以上、ありがとうございました。