flaskでDB migrationしたときにつまずいた所のメモ

flaskでDB migrationしたときにつまずいた所のメモ

タイトルにも書いていますが、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マイグレーションしたときにつまずいた所のメモでした。