2020年7月13日月曜日

Flask-Migrate を使ってみた

概要

Flask + SQLAlchemy を使う場合には専用のライブラリがあります
データベースに変更を加える際のマイグレーションも専用のライブラリが用意されているので使ってみました

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-Migrate 2.5.3

MySQL 準備

今回は Homebrew で最新版をインストールします
最新版だと Python の mysqlclient がエラーになりますがトラブルシューティングに記載の方法で修正できるので最新版を使います

  • brew install mysql
  • brew services start mysql

また事前にデータベースを作成しておきましょう
ユーザやパスワードはデフォルトのものを使います

  • mysql -u root -e "createdb test;"

ライブラリインストール

必要なライブラリをインストールしましょう
今回は MySQL を使うのでドライバとして mysqlclient を追加でインストールします

  • pipenv install Flask-Migrate mysqlclient

Flask アプリの作成

マイグレーションするには Flask のアプリが必要になります
簡単なアプリで良いので作成しましょう

  • vim my_app/__init__.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

マイグレーション定義の追加

作成した Flask アプリにマイグレーションの定義を追加します
本当は別モジュールで管理したいところですが Flask-Migrate の仕様上そのままアプリに定義を追加します

from flask import Flask
app = Flask(__name__)

# migrate
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format("root", "", "localhost", "test")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))

@app.route('/')
def hello_world():
    return 'Hello, World!'

マイグレーションするテーブル定義は db.Model を継承したクラスを作成することで行います
このあたりは SQLAlchemy の使い方をそのまま流用できます
また Migrate クラスのオブジェクト (migrate) をこのファイル内のどこかで必ず生成する必要があります
これが生成されていないとトラブルシューティングにある KeyError が発生します

マイグレーションリポジトリの初期化

まずはマイグレーションするための専用のリポジトリを作成する必要があります
実行時に FLASK アプリが存在しているモジュール名を指定しましょう
あとは flask db init コマンドを実行すれば OK です

  • FLASK_APP=my_app pipenv run flask db init

成功すると migrations ディレクトリ配下にマイグレーションに必要なファイルが作成されます
どうやら alembic というマイグレーションライブラリの仕組みを使っているようです

マイグレーションスクリプトの作成

次にマイグレーションするためのスクリプトを作成します
この段階ではまだテーブルなどの作成、変更はされません
コマンドは flask db migrate になります (コマンドを見るとマイグレーションされそうですがされません)
-m を使ってマイグレーション時のコメントを残すことができます
コメントは履歴に残るのでどういったマイグレーションだったのかわかりやすいコメントを残しましょう

  • FLASK_APP=my_app pipenv run flask db migrate -m "Initial migration."
  • FLASK_APP=my_app pipenv run flask db history
<base> -> 2367123248c0 (head), Initial migration.

以下のように成功すれば OK です

INFO [alembic.runtime.migration] Context impl MySQLImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'user' Generating /path/to/work/migrations/versions/2367123248c0_initial_migration.py … done

migrations/versions/2367123248c0_initial_migration.py がマイグレーションに使用されたスクリプトになるので実際に実行されるコマンドを確認したい場合はこのファイルを確認しましょう

アップグレードしてみる

では実際にテーブルを作成してみます
コマンドは flask db upgrade になります

  • FLASK_APP=my_app pipenv run flask db upgrade
INFO [alembic.runtime.migration] Context impl MySQLImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 2367123248c0, Initial migration.

実行されたマイグレーションスクリプトが先程作成されたスクリプトであることが確認できます
これで実際に MySQL を覗いてみるとテーブルが作成されているのが確認できると思います

  • mysql -u root test -e "desc user;"
+——-+————–+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +——-+————–+——+—–+———+—————-+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(128) | YES | | NULL | | +——-+————–+——+—–+———+—————-+

データベースの構成を変更してみる

とりあえずテーブルを作成することはできました
今度はカラムを一つ追加するマイグレーションをしてみます

  • vim my_app/__init__.py
from flask import Flask
app = Flask(__name__)

# migrate
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format("root", "", "localhost", "test")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer) # <- New

@app.route('/')
def hello_world():
    return 'Hello, World!'

こんな感じでカラムを一つ追加してみましょう
そして migrate -> upgrade してみます

  • FLASK_APP=my_app pipenv run flask db migrate -m "Add a new column."
  • FLASK_APP=my_app pipenv run flask db upgrade

先ほどと同様にマイグレーションスクリプトが作成されてそれが実行されたのが確認できると思います
MySQL を直接確認してもわかると思います
なお history でもちゃんとマイグレーションの履歴が追加されているのが確認できると思います

2367123248c0 -> 1f939fcbbde7 (head), Add a new column.
<base> -> 2367123248c0, Initial migration.

ダウングレードもできる

ちなみにロールバックするコマンドもあり downgrade を使います

  • FLASK_APP=my_app pipenv run flask db downgrade

この場合 history は変わらないので注意しましょう
現在どこまでマイグレーションスクリプトが適用されているか確認するのは current コマンドを使います

  • FLASK_APP=my_app pipenv run flask db current

heads や show コマンドはマイグレーションスクリプトの現在の最新を確認するコマンドなのでごっちゃにならないようにしましょう

おまけ: Python スクリプトとしても実行できる

今回は flask コマンドのサブコマンドとして実行しましたが普通に python コマンドからでも実行できます
Flask-Script というライブラリが追加で必要になるのでインストールします

  • pipenv install Flask-Script

スクリプトは Flask アプリのモジュールが参照できるパスに作成する必要があります

  • vim my_app/__init__.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

マイグレーション用のスクリプトは少し変わります
先程インストールした Flask-Script の Manager クラスを使います
またスクリプトとして実行できるように main を追加します

  • vim manager.py
from my_app import app
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format("root", "", "localhost", "test")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)
migrate = Migrate(app, db)

manager = Manager(app)
manager.add_command('db', MigrateCommand)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer)

if __name__ == '__main__':
    manager.run()

あとは init -> migrate -> upgrade の流れを実行すれば OK です
実行するときは作成した manager.py を python コマンドで実行します

  • FLASK_APP=my_app pipenv run python manager.py db init
  • FLASK_APP=my_app pipenv run python manager.py db migrate -m "Initial migration."
  • FLASK_APP=my_app pipenv run python manager.py db upgrade

またスクリプト版をテストする場合は MySQL 側のデータも作成おきましょう

  • mysql -u root test -e "drop table alembic_version;"
  • mysql -u root test -e "drop table user;"

最後に

Flask + Flask-Migrate を使って MySQL のマイグレート管理をしてみました
ディレクトリとファイルの構成にルールがあるのでそこだけ注意しましょう
これでデータベースのマイグレーションの変更履歴もちゃんと git で管理できるようになります

トラブルシューティング

KeyError: 'migrate' が出る場合

マイグレーションする場合は FLASK_APP で指定したアプリのモジュール配下にある __init__.py を使います
そこに Migrate オブジェクトの migarate が記載されていることを想定しているので別ファイルや別モジュールにマイグレーションの定義をしている場合は KeyError になるようです
ファイルの構成やディレクトリの構成を見直すのとちゃんと __init__.py に migrate オブジェクトが存在するか確認しましょう

Library not loaded: /usr/local/opt/mysql/lib/libmysqlclient.20.dylib

おそらく Mac の環境でなければでないと思います
今回使用した MySQL のバージョンは最新の 8.0.19 になります
その場合はクライアント用のライブラリが /usr/local/opt/mysql/lib/libmysqlclient.21.dylib になるようで Python の mysqlclient ライブラリだとまだそれを使用していないようでエラーになります

一番てっとり早いのは libmysqlclient.20.dylib のシンボリックリンクを作成すれば解決するはずです

  • cd /usr/local/opt/mysql/lib
  • ln -s libmysqlclient.dylib libmysqlclient.20.dylib

参考サイト

0 件のコメント:

コメントを投稿