LoginSignup
38
40

More than 3 years have passed since last update.

Flask-Migrateに関して自分の言葉で

Last updated at Posted at 2019-03-23

TL;DR

最終的に長くなってしまったので。

  • (Flask-)SQLAlchemyを使うと、Python上のモデル操作で、実際のデータベース構造を変更(参照)できる。
  • Flask-Migrateは通常イメージするデータベース間の「移行」ではなく、Python上のモデルから、実際のデータベースへの「変換」を行うモジュール。
  • Flask-Migrate(Alembic)を使うと、Python上のモデルと実際のデータベース構造の同期がとりやすい。

Flask-Migrateとは

公式

Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic.

Alembicを使って、SQL Alchemyによるデータベースマイグレーションを扱うためのExtension。これだけ読んでも意味がわかりませんでしたが、使ってみると用途がわかりました。

ここでいうマイグレーションとは、通常イメージするデータベース間の「移行」ではなく、Python上のモデルから、実際のデータベースへの「変換」を表しているという理解です。

Python上のデータベースモデル(Flask-SQLAlchemyについて)

pythonでデータベースを扱う方法はいくつかありますが、その1つはSQLAlchemyです。これはORM(Object-Relational Mapping)の1つで、オブジェクト志向言語のオブジェクトを、そうでないもの(ここではRelational Database)に変換するためのモジュールになります。
ここではFlask内で扱うことを前提に、flaskの拡張であるflask-sqlalchemyを使用します。

In[2]: from flask import Flask
In[3]: from flask_sqlalchemy import SQLAlchemy
# 最初にFlask自体をappという名前でインスタンス化します。
In[4]: app = Flask(__name__)
# 次にいくつかパラメータを設定します。
# sqliteのデータベース(ファイル)をどこに配置するか指定します。
In[5]: app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///C:\\temp\\flask\\app.db'
# TRACK_MODIFICATIONという機能を無効化します。(後述)
In[7]: app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# データベースをインスタンス化します。flaskの拡張であるflask-sqlalchemyを使用しているため、Flaskインスタンスを渡してデータベースを作っている点に注意です。
In[8]: db = SQLAlchemy(app)
# テーブルは、dbがもつModelというクラスを継承させた、Pythonのクラスとして作成します。
In[9]: class User(db.Model):
  ...:     id = db.Column(db.Integer, primary_key=True)
  ...:     username = db.Column(db.String(64), index=True, unique=True)
  ...:     email = db.Column(db.String(120), index=True, unique=True)
  ...:     
# 作成したクラス(テーブル)を実際にSQLite上に作成するのはcreate_allというメソッドです。
# この時点で、指定したディレクトリにapp.dbというファイルが作成されます。
In[13]: db.create_all()
# テーブルの確認
# ※for_bindじゃないものがあってもよさそうですが、見た感じこれしかありません。
In[14]: db.get_tables_for_bind()
Out[14]: [Table('user', MetaData(bind=None), Column('id', Integer(), table=<user>, primary_key=True, nullable=False), Column('username', String(length=64), table=<user>), Column('email', String(length=120), table=<user>), schema=None)]
# テーブルの中身をクエリしてみます。(この時点では何もなし)
In[15]: User.query.all()
Out[15]: []
# エントリを作成します。エントリは対応するクラスのインスタンスとして表現されます。
In[16]: u = User(username='Alice', email='alice@example.jp')
# DBへの追加の際はセッションを意識する必要があります。
# セッションを通して追加し(この追加は複数可能)、最後にcommitすることでデータベースに反映されます。
In[17]: db.session.add(u)
In[18]: db.session.commit()
# 内容の確認
In[19]: User.query.all()
Out[19]: [<User 1>]
In[21]: User.query.get(1).email
Out[21]: 'alice@example.jp'

ちなみに、app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = Falseを指定しないと、以下のような警告が表示されます。

In[6]: db = SQLAlchemy(app)
C:\Users\ikedak2\PycharmProjects\TestSandbox\venv\lib\site-packages\flask_sqlalchemy\__init__.py:794: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '

もう1つ補足として、クラスはUserなのに、実際にできるテーブルはuserです。これはSQLAlchemyが対応するクラスの名前に基づいて、自動的にテーブル名を付与するためです。規則はsnake caseで、全て小文字、かつ区切りはアンダースコアになります。これを指定したければ、モデルクラスの中で、tablenameを指定すればよいらしい。

SQliteでのセッションエラーについて(2019/06/26追記)

commitした直後にqueryすると、以下のようなエラーで失敗する事象にあいました。原因は、PyCharmでほかのプロジェクトを開いていて、そこでも同じようにコンソールで、同じ名前(db)でfrom app import dbしていたためでした。

>>> db.session.commit()
>>> User.query.all()
sqlalchemy.exc.ProgrammingError: (sqlite3.ProgrammingError) SQLite objects created in a thread can only be used in that same thread.

Flask-Migrateによるデータベース操作、履歴管理

先の項目では、sqlalchemy(Flask-SQLAlchemy)によってデータベース、テーブルの作成を行いました。簡単な構成ではこれでも事足りますが、構成が複雑だったり、同じ構成を他で複製したい場合などは、毎回手動で作成するのは大変です。Flask-Migrateを使うと、モデル設計→実際のデータベース構成の変換が自動で実行され、さらに履歴も管理することができます。
先の項目ではPythonのコンソールから操作しましたが、ここからは上記のような使用例を考えて、アプリケーションをファイル化します。(githubとかで配布できる形を想定)

下準備

以下のようなフォルダ構成をとります。
TestSandBox
├venv/
├.flaskenv
├test_flask-migrate.py

Flask-Migrateはflask db xxというflaskのサブコマンドで操作します。flaskコマンドを使用するにあたり、大元になるFlaskインスタンスがどこで定義されているか(app=Flask()がどこで実行されているか)を指定する必要があります。デフォルトはapp.pyまたはwsgi.pyのようです。
参考

この指定はFLASK_APPという環境変数で行います。コンソールで実行する場合は、通常通りset FLASK_APP='test.py'のように指定しますが、これをファイル化するにはpython-dotenvというモジュールを使用します。
まず実行している仮想環境などで、pip install python-dotenvを実行します。

(venv) C:\Users\ikedak2\PycharmProjects\TestSandbox>pip install python-dotenv

そのうえで、ルートディレクトリに.flaskenvというファイルを作成し、以下のように記載します。

.flaskenv
FLASK_APP=test_flask-migrate.py

これでflaskコマンドが実行されると、test_flask-migrate.pyが読み込まれます。
次に、前の項目でコンソールから実施した内容と同じようなことをファイルにします。

test_flask-migrate.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
import os
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

# 実行されるファイル(test_flask-migrate.py)の置き場所をbasedirに保存
basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
# データベースファイルは実行ファイルと同じ場所にapp.dbという名前で作成
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///' + os.path.join(basedir, 'app.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
# migrateインスタンスを定義
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))

    # この関数は、インタープリタ(コンソール)からこのクラス(からできたインスタンス)を読んだ際に、どのように表示されるかを定義している。ここではusernameを表示させている。
    def __repr__(self):
        return '<User {}>'.format(self.username)

2019/06/24追記

ちなみに、ここで作成するpyの名前を、なんとなくflask_migrate.pyとすると、以下のように次のdb initで失敗します。

(venv) C:\Users\ikedak2\PycharmProjects\TimeRecorder2019>flask db init
Usage: flask db init [OPTIONS]

Error: Failed to find Flask application or factory in module "flask_migrate". Use "FLASK_APP=flask_migrate:name to specify one.

これは、作成した「flask_migrate.py」が、ファイル内でimportしているflask_migrateとかぶっているため。flaskアプリケーションを、自分で作成したflask_migrateではなく、外部ライブラリのflask_migrateから読もうとしている。
関数名やモジュール名はほかのものと重複しないように注意する必要があります。

マイグレーションレポジトリの作成

この状態で通常のプロンプトから、flask db initを実行します。

(venv) C:\Users\ikedak2\PycharmProjects\TestSandbox>flask db init
Creating directory C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations ... done
Creating directory C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations\versions ... done
Generating C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations\alembic.ini ... done
Generating C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations\env.py ... done
Generating C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations\README ... done
Generating C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'C:\\Users\\ikedak2\\PycharmProjects\\TestSandbox\\migrations\\alembic.ini' before proceeding.

このコマンドを実行することで、ルートディレクトリにmigrationというフォルダが作成されます。

マイグレーションの実施

次に、flask db migrateを実行し、マイグレーション用のスクリプトを生成します。
マイグレーションにはautomaticとmanualの2種類があり、automaticの場合、Python上でモデルに定義された形と、実際のDBのスキーマをAlembicが比較して、必要な差分を埋めるスクリプトを生成します。今回の場合、元のデータベースが存在しないので、データベース自体とUserテーブルを生成するスクリプトが作成されます。
※先ほど作成したデータベースとはディレクトリを使用しているので新規作成です。

(venv) C:\Users\ikedak2\PycharmProjects\TestSandbox>flask db migrate -m "Create user table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
Generating C:\Users\ikedak2\PycharmProjects\TestSandbox\migrations\versions\182c58313345_create_user_table.py ... done

userというテーブルと、インデックスとなるフィールドがモデルの定義通りに認識されています。
この時点で、migration\versionsフォルダに、182c58313345_create_user_table.pyというファイルが作成されており、これが実際のSQL操作を実行するスクリプトです。ファイル名はバージョンを表しており、すなわちバージョン管理されています。
-mというオプションで、gitのコミットのように、バージョンに対して名前(コメント)を付けることができます。

スクリプトの中には以下のように記載されており、「この変更をどのように実施するか(Upgrade)」と「どのように切り戻すか」(Downgrade)の2つが用意されています。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('user',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('username', sa.String(length=64), nullable=True),
    sa.Column('email', sa.String(length=120), nullable=True),
    sa.Column('password_hash', sa.String(length=128), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
    op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_index(op.f('ix_user_username'), table_name='user')
    op.drop_index(op.f('ix_user_email'), table_name='user')
    op.drop_table('user')
    # ### end Alembic commands ###

履歴は以下のように確認できます。

(venv) C:\Users\ikedak2\PycharmProjects\TestSandbox>flask db history
<base> -> 182c58313345 (head), Create user table

履歴管理されているため、特定のバージョンに戻すことが可能みたいです。(未実施)

マイグレーションの実行

flask db upgradeを実行して、upgradeの内容(テーブル作成)を実行します。

(venv) C:\Users\ikedak2\PycharmProjects\TestSandbox>flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 182c58313345, Create user table

実際にできたかどうかは、コンソールから実施した際と同様です。

(venv) C:\Users\ikedak2\PycharmProjects\TestSandbox>python
Python 3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:04:45) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from test_flask_migrate import db
>>> db.get_tables_for_bind()
[Table('user', MetaData(bind=None), Column('id', Integer(), table=<user>, primary_key=True, nullable=False), Column('username', String(length=64), table=<user>), Column('email', String(length=120), table=<user>), Column('
password_hash', String(length=128), table=<user>), schema=None)]

この後は、Python側でモデルに変更を加えたときに、マイグレーションを実行することでデータベースの構成とモデルの同期をとることができます。

感想

今まではORM = PythonからSQLを実行するためのもの、くらいにしか認識していませんでしたが、ようやくORMとか何か、なんとなくわかったように思われます。

38
40
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
38
40