なんとかはなったけど、イマイチ理解はできてない話でもある。
自作の書籍管理WEBアプリに、保管場所を表す BookShelf
モデルを追加して、データベース(SQLite3)のアップグレードを実行したところでハマった。
アップグレード前のデータベースには、4つのテーブルがあった。( )内はモデルのクラス名。
- books (Book)
- categories (Category)
- formats (Format)
- coverarts (CoverArt)
ここに保管場所を記録するための bookshelves
テーブルを追加したい。モデルクラスは BookShelf
。models.py ファイルにこんなふうに定義した。
class BookShelf(db.Model): __tablename__ = 'bookshelves' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) description = db.Column(db.String) books = db.relationship('Book', backref='bookshelf', lazy='dynamic') def __repr__(self): return f'<BookShelf id={self.id} name={self.name}>' def to_dictionary(self): return { 'id': self.id, 'name': self.name, 'description': self.description }
app.py ファイルはこうなってる(変更なし)。最後の import views
は、ルーティングと対応する処理を定義したモジュールをインポートしている。
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) app.config.from_pyfile('./bruschetta.conf') db = SQLAlchemy(app) import models migrate = Migrate(app, db) import views
この状態で flask db migrate
を実行すれば、データベースをマイグレートするスクリプトを生成してくれる。実行するとこうなった(poetry をパッケージ管理に使ってるので poetry 経由だ)。
takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db migrate -m "create bookshelves table" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL.
migrations/versions 以下にマイグレート用のスクリプトができるはずだけどできていない。
takatoh@apostrophe:bruschetta-feature-bookshelf$ ls migrations/versions 0bff8cd02ef3_create_tables.py __pycache__ 981c2baab82e_create_coverarts_table.py
もとからあった2つしかない。おかしいと思いつつも flask db upgrade
を実行してみたけど、当然のようにエラーになった。
takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run 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 0bff8cd02ef3 -> 981c2baab82e, Create coverarts table Traceback (most recent call last): File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context (以下略)
いや、待て。マイグレート用のスクリプトが作られてないなら、データベースもアップグレードしないんだから、エラーもなく、何もせずに終了するはずでは?
そこで、データベースの中を覗いてみることにした。
takatoh@apostrophe:bruschetta-feature-bookshelf$ sqlite3 instance/bruschetta.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> .tables alembic_version categories formats books coverarts
テーブルが5つある。books
、categories
、formats
は一番始めに作ったテーブル、coverarts
はあとから追加したテーブル、そして alembic_version
は Flask-Migrate が内部で使っている alembic というデータベースマイグレーション用のライブラリが利用するテーブルだ。
alembic_version
に何が格納されているか見てみる(その前にヘッダを表示するようにしておく)。
sqlite> .headers on sqlite> SELECT * FROM alembic_version; version_num 0bff8cd02ef3
version_num
というカラムが1つだけあって、0bff8cd02ef3
という値(文字列)が格納されている。この値はマイグレーション用スクリプトの識別子で、ファイル名の頭に付いているほか、ファイルの中にも Revision ID の値として書き込まれている。
この 0bff8cd02ef3
という値は、過去2回のマイグレーションのうちの最初の方の値だ。2回目のマイグレーション(coverarts
テーブルを追加したとき)にこの値も書き換えられるはず、だと思うんだけどどういうわけかそうならなかったんじゃないか。だとすれば、これを書き換えてやればいいのかも。
sqlite> UPDATE alembic_version SET version_num="981c2baab82e" WHERE version_num="0bff8cd02ef3"; sqlite> SELECT * FROM alembic_version; version_num 981c2baab82e
これで書き換わった。データベースを抜けて、flask db migrate
を改めて実行する。
takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db migrate -m "create bookshelves table" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'bookshelves' INFO [alembic.autogenerate.compare] Detected added column 'books.bookshelf_id' INFO [alembic.autogenerate.compare] Detected added foreign key (bookshelf_id)(id) on table books INFO [alembic.autogenerate.compare] Detected added foreign key (coverart_id)(id) on table books Generating /home/takatoh/w/Bruschetta/bruschetta-feature- bookshelf/migrations/versions/3ccfddd40b69_create_bookshelves_table.py ... done
おお、今度はうまくいったっぽい。実際、migrations/versions 以下に新しいファイルが生成されている。3ccfddd40b69_create_bookshelves_table.py がそれだ。
takatoh@apostrophe:bruschetta-feature-bookshelf$ ls migrations/versions 0bff8cd02ef3_create_tables.py 3ccfddd40b69_create_bookshelves_table.py 981c2baab82e_create_coverarts_table.py __pycache__
では、flask db upgrade
を実行してみよう。
takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run 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 981c2baab82e -> 3ccfddd40b69, create bookshelves table Traceback (most recent call last): File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/bin/flask", line 8, in <module> sys.exit(main()) ^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask/cli.py", line 1064, in main cli.main() File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1078, in main rv = self.invoke(ctx) ^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1688, in invoke return _process_result(sub_ctx.command.invoke(sub_ctx)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1688, in invoke return _process_result(sub_ctx.command.invoke(sub_ctx)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1434, in invoke return ctx.invoke(self.callback, **ctx.params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 783, in invoke return __callback(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/decorators.py", line 33, in new_func return f(get_current_context(), *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask/cli.py", line 358, in decorator return __ctx.invoke(f, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 783, in invoke return __callback(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask_migrate/cli.py", line 150, in upgrade _upgrade(directory, revision, sql, tag, x_arg) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask_migrate/__init__.py", line 111, in wrapped f(*args, **kwargs) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask_migrate/__init__.py", line 200, in upgrade command.upgrade(config, revision, sql=sql, tag=tag) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/command.py", line 403, in upgrade script.run_env() File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/script/base.py", line 583, in run_env util.load_python_file(self.dir, "env.py") File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/util/pyfiles.py", line 95, in load_python_file module = load_module_py(module_id, path) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/util/pyfiles.py", line 113, in load_module_py spec.loader.exec_module(module) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<frozen importlib._bootstrap_external>", line 940, in exec_module File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/migrations/env.py", line 96, in <module> run_migrations_online() File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/migrations/env.py", line 90, in run_migrations_online context.run_migrations() File "<string>", line 8, in run_migrations File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/runtime/environment.py", line 948, in run_migrations self.get_context().run_migrations(**kw) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/runtime/migration.py", line 627, in run_migrations step.migration_fn(**kw) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/migrations/versions/3ccfddd40b69_create_bookshelves_table.py", line 27, in upgrade with op.batch_alter_table('books', schema=None) as batch_op: File "/home/takatoh/.pyenv/versions/3.11.7/lib/python3.11/contextlib.py", line 144, in __exit__ next(self.gen) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/operations/base.py", line 398, in batch_alter_table impl.flush() File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/operations/batch.py", line 162, in flush fn(*arg, **kw) File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/operations/batch.py", line 669, in add_constraint raise ValueError("Constraint must have a name") ValueError: Constraint must have a name
おおう、またエラーだ。”Constrain must have a name” (制約には名前が必要)だと。ググってみると StackOverflow にズバリの回答を見つけた。
曰く、これは正常な動作で、なぜなら SQLite3 は ALTER table をサポートしてないから。解決法としては、SQLAlchemy のメタデータにすべてのタイプの制約の命名テンプレートを作成するのがいい(今後のためにも)、という。
そういうわけで、app.py をつぎのように書き換えた。
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from sqlalchemy import MetaData app = Flask(__name__) app.config.from_pyfile('./bruschetta.conf') convention = { 'ix': 'ix_%(column_0_label)s', 'uq': 'uq_%(table_name)s_%(column_0_name)s', 'ck': 'ck_%(table_name)s_%(constraint_name)s', 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s', 'pk': 'pk_%(table_name)s' } metadata = MetaData(naming_convention=convention) db = SQLAlchemy(app, metadata=metadata) import models migrate = Migrate(app, db) import views
そしてさっきのマイグレーションスクリプトを削除して、やり直す。
takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db migrate -m "create bookshelves table" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added column 'books.bookshelf_id' INFO [alembic.autogenerate.compare] Detected added foreign key (bookshelf_id)(id) on table books INFO [alembic.autogenerate.compare] Detected added foreign key (coverart_id)(id) on table books Generating /home/takatoh/w/Bruschetta/bruschetta-feature- bookshelf/migrations/versions/1821fe006433_create_bookshelves_table.py ... done takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run 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 981c2baab82e -> 1821fe006433, create bookshelves table
今度はちゃんとデータベースをアップグレードできたようだ。データベースの中も見てみる。
takatoh@apostrophe:bruschetta-feature-bookshelf$ sqlite3 instance/bruschetta.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> .tables alembic_version bookshelves coverarts books categories formats sqlite> .headers on sqlite> SELECT * FROM alembic_version; version_num 1821fe006433
bookshelves
テーブルもできているし、alembic_version
テーブルも更新されている。
これでやっと、WEBアプリの機能追加にかかれる。