タイトルにも書いていますが、flaskでDBのmigrationをした際にいくつかハマった箇所があったのでメモを残しておきます。
環境
- python : 3.7
- mysql : 5.7
pythonで使用したライブラリは以下の通りです。
- Flask==1.1.1
- SQLAlchemy==1.3.8
- Flask-Migrate==2.5.2
- Flask-SQLAlchemy==2.4.0
- PyMySQL==0.9.3
ディレクトリ構成
flaskアプリケーションの構成(一部抜粋)
app
├── src
│ ├── Dockerfile
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ └── index.py
│ ├── config.py
│ ├── database.py
│ ├── instance
│ ├── models
│ │ ├── __init__.py
│ │ └── user.py
│ ├── requirements.txt
│ ├── static
│ └── templates
│ └── front
├── db
│ └── data
├── docker-compose.yml
├── migrations
│ ├── README
│ ├── alembic.ini
│ ├── env.py
│ ├── script.py.mako
│ └── versions
│ └── xxxxxxxxxxxxxxx.py
環境による接続先の変更
今回作っているアプリケーションは個人開発しているもので、現時点で公開するかどうか決まっていません。ですが、一般的な機能なのでflaskでの実装してみました。
環境の識別子
環境変数でアプリケーションが動作している環境が開発 / 本番 / テスト を判断します。
環境変数名は ENV とし、docker-composeで以下のように設定しています。
# docker-compose.yml
services:
flask:
environment:
- FLASK_ENV=development
接続先の判定
環境変数をもとに使用するconfigモジュールを決めています。
/app/src/__init__.py
def create_app(test_config=None):
# flaskインスタンスの生成
# アプリケーションの設定を読み込み、flaskインスタンスに設定
if test_config is None:
# load the instance config, if it exists, when not testing
config_object_string = 'chatbot.config.DevelopmentConfig'
if app.config['ENV'] == 'production':
config_object_string = 'chatbot.config.ProductionConfig'
elif app.config['ENV'] == 'test':
config_object_string = 'chatbot.config.TestingConfig'
app.config.from_object(config_object_string)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
設定ファイルの書き方
環境ごとにconfigクラスを定義しています。
/app/src/config.py
import os
class Config(object):
DEBUG = False
TESTING = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(Config):
DEBUG = True
# SQLAlchemy
dbCOnfig = {
'user': os.getenv('DB_USER', 'username'),
'password': os.getenv('DB_PASSWORD', 'password'),
'host': os.getenv('DB_HOST', 'mysql'), # dockerのコンテナ名を指定
'database': os.getenv('DB_DATABASE', 'database'),
}
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{database}?charset=utf8'.format(
**dbCOnfig)
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = False
class TestingConfig(Config):
TESTING = True
dockerでmysqlコンテナが起動できない
エラーメッセージ:initialize specified but the data directory has files in it.
docker-compose.yml で以下の設定を行い、 /app/db/data ディレクトリを事前に作成していた事が原因でした。
services:
mysql:
image: mysql:5.7
container_name: mysql
volumes:
- ./db/data:/var/lib/mysql
dataディレクトリはmysql初期化時に自動的に作られるので、事前に作っておいてはいけませんでした。
flask db migrate で alembicがスキーマを見つけてくれない
Flask_Migrateでは、内部でalembicというDBスキーマ管理ツールを使ってマイグレーションファイルを自動生成してくれます。
/app/src/models/user.py に usersテーブルのスキーマを記述しマイグレーションファイル生成を試みたのですが、以下のログが出力されるだけでファイルが生成されませんでした。
$ flask db migrate
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.env] No changes in schema detected.
てっきりmigrationディレクトリがある階層(/app)配下のpyファイルを走査して、スキーマの定義を見つけてくれると思っていたのですが勘違いだったようです。
色々と試してみたのですが、結果的にflaskアプリケーションとして src.models.userモジュールをimportしていない事が原因でした。
userテーブルはログイン認証などで使用する予定なので、認証用のBlueprintを作成し登録し、改めて flask db migrate すると無事にマイグレーションファイル( /app/migrations/versions/xxxxxxxxxxxx.py )が作成されました。
/app/src/api/auth.py
import functools
from flask import (
Blueprint, g, render_template, request
)
from werkzeug.security import check_password_hash, generate_password_hash
# ここで userモジュールを import
from chatbot.models import user
bp = Blueprint('auth', __name__, url_prefix='/api/auth')
/app/src/__init__.py
def create_app(test_config=None)
:
# 認証用Blueprintを登録
from chatbot.api import auth
app.register_blueprint(auth.bp)
app.add_url_rule('/api/auth', endpoint='auth')
以上、flaskでDBマイグレーションしたときにつまずいた所のメモでした。