2020年12月24日木曜日

Flask-Migrate でカラム名を変更すると既存のカラムのデータがなくなってしまう

概要

flask-migrate を使ってカラム名を変更すると既存のカラムの情報が新規のカラムに引き継がれません
原因はマイグレーションスクリプト内で drop_column -> add_column しているからです
今回はカラム名を変更してもデータをロストせずにマイグレートする方法を紹介します

環境

  • macOS 11.1
  • Python 3.8.5
    • flask 1.1.2
    • flask-migrate 2.5.3

事象の確認

まずは本当にカラム名を変更したら既存のカラムのデータが引き継がれないのか確認します

Flask アプリ

  • vim my_app/__init__.py
from flask import Flask
from flask_migrate import Migrate
from my_app.database import db

app = Flask(__name__)
app.debug = True
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://root:@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_ECHO'] = False

db.init_app(app)
migrate = Migrate(app, db)

database.py

  • vim my_app/database.py
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

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

    def __repr__(self):
        return "%s,%s,%i" % (self.id, self.name, self.age)

データベース作成

  • mysql> create database test;

初期マイグレート

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

    作成されるテーブル情報は以下の通りです
mysql> show create table user \G *************************** 1. row *************************** Table: user Create Table: CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci 1 row in set (0.00 sec)

テストデータ登録

  • mysql> insert into user values (null, 'hawk', 10);
  • mysql> insert into user values (null, 'snowlog', 20);1
  • mysql> insert into user values (null, 'hawksnowlog', 30);

テーブル構成変更

name カラムを first_name というカラム名に変更してみます

  • vim my_app/database.py
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

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

    def __repr__(self):
        return "%s,%s,%i" % (self.id, self.name, self.age)
  • FLASK_APP=my_app pipenv run flask db migrate -m "A name column to first_name."
  • FLASK_APP=my_app pipenv run flask db upgrade


これで user テーブルを確認すると first_name カラムのデータが NULL になってしまっているのが確認できます

mysql> select * from user; +—-+——+————+ | id | age | first_name | +—-+——+————+ | 1 | 10 | NULL | | 2 | 20 | NULL | | 3 | 30 | NULL | +—-+——+————+

対策

flask db migarte 時に自動生成されるマイグレートスクリプトを修正することで対応してみます

マイグレートスクリプトの編集

まずは migrate するところまで進めましょう
upgrade までしてしまうと適用されてしまうので upgrade はまだしません

  • FLASK_APP=my_app pipenv run flask db migrate -m "A name column to first_name."

そして生成されるマイグレートスクリプトを編集します
リビジョン番号はそのたびに違うので自信の環境に併せて書き換えてください

upgrade と downgrade 関数を以下のように書き換えます
ポイントは alter_column という関数を使ってカラムの情報を書き換えるようにします
また existing_typetype_ を指定しましょう
前者は変更前のカラムのタイプで後者は変更後のカラムのタイプになります
今回はタイプは変更しないので同じものを指定しています


* vim migrations/versions/be9a17e479a6_a_name_column_to_first_name.py

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.alter_column('user', 'name', nullable=True, new_column_name='first_name', type_=sa.String(128), existing_type=sa.String(128))
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.alter_column('user', 'first_name', nullable=True, new_column_name='name', type_=sa.String(128), existing_type=sa.String(128))
    # ### end Alembic commands ###

動作確認: upgrade してみる

書き換えたマイグレートスクリプトを使って upgrade してみます

  • FLASK_APP=my_app pipenv run flask db upgrade

これでテーブルを確認するとちゃんと前のデータが引き継がれていることが確認できると思います

mysql> select * from user; +—-+————-+——+ | id | first_name | age | +—-+————-+——+ | 1 | hawk | 10 | | 2 | snowlog | 20 | | 3 | hawksnowlog | 30 | +—-+————-+——+ 3 rows in set (0.00 sec)



また upgrade 後のテーブル情報は以下のようになっています

mysql> show create table user \G *************************** 1. row *************************** Table: user Create Table: CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `first_name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci 1 row in set (0.00 sec)

最後に

flask-migrate を愚直に使うと drop_column してデータをロストしかねないのでマイグレートするときは必ず alter_column を使うようにしましょう

参考サイト

0 件のコメント:

コメントを投稿