☆☆ 新着記事 ☆☆

2019年8月8日木曜日

FLASK - MIGRATE


Flask_Migrateは、SQLAlchemyベースのDBをマイグレーションする為のFlask Extensionです。

Alembic is a database migration tool for SQLAlchemy.
https://flask-migrate.readthedocs.io/en/latest/

 ◇ Step 1 :
 pip install flask-migrate

  Step 2 : modify main.py
 import os
 from flask import Flask
 from flask_migrate import Migrate  // Imported Flask Migrate
 from api.db import db
 from api.config import config
 ......
 db.init_app(app)
 migrate = Migrate(app, db) // It will allow us to run migrations
 ......
 @app.before_first_request
 def create_tables():
     db.create_all()
 if __name__ == '__main__':
     app.run()


==以下はコマンドライン操作

  Step 3 :Creation of Migration Repository (Directory).

flask dbの変更記録を保存するレポジトリーを作成する。

> export FLASK_APP=run.py (Windowsは set FLASK_APP=run.py)
flask db init (<- これでレポジトリ用のフォルダが作成される)

 Flask-Migrate exposes its commands through the flask command. You have already seen flask run, which
 is a sub-command that is native to Flask. The flask db sub-command is added by Flask-Migrate to manage
 everything related to database migrations. So let's create the migration repository for microblog by
 running flask db init:

  Step 4 :(generate an initial migration:)
 flask db migrate -m "コメント"
-m ”コメント”は、file名に付加される任意情報。なくても良い。

(Alembicで認識できるDBの変更には制限がある。)
Alembic is currently unable to detect table name changes, column name changes, or anonymously named  constraints.

既にあるDBにFlask_Migrateを適用して後から管理しようとすると、この段階で
No changes in schema detected. 等のMessageがでる。

対応方法:How To Add Flask-Migrate To An Existing Project (2020/04/10)
https://youtu.be/IxCBjUapkWk



  Step 5 :
 flask db upgrade
 or
 flask db downgrade

 Because this application uses SQLite, the upgrade command will detect that a database does not exist
 and will create it (you will notice a file named app.db is added after this command finishes, that is
 the SQLite database). When working with database servers such as MySQL and PostgreSQL, you have to  create the database in the database server before running upgrade.
You also have a flask db downgrade command, which undoes the last migration.

Flask dbで使えるその他のオプション: flask db とタイプしてリターンすると表示される。

Commands:
  branches   Show current branch points
  current    Display the current revision for each database.
  downgrade  Revert to a previous version
  edit       Edit a revision file
  heads      Show current available heads in the script directory
  history    List changeset scripts in chronological order.
  init       Creates a new migration repository.
  merge      Merge two revisions together, creating a new revision file
  migrate    Autogenerate a new revision file (Alias for 'revision...
  revision   Create a new revision file.
  show       Show the revision denoted by the given symbol.
  stamp      'stamp' the revision table with the given revision; don't run...
  upgrade    Upgrade to a later version


ということでやってみる。

tw_msg.py
import os
from flask import Flask, render_template,url_for,flash, redirect, request
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length
from flask_migrate import Migrate

app = Flask(__name__)
project_dir = os.path.dirname(os.path.abspath(__file__))
database_file = "sqlite:///{}".format(os.path.join(project_dir, "msg.db"))
app.config["SQLALCHEMY_DATABASE_URI"] = database_file
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] ='you-will-never-know'

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

#DB Model
class Tw_msg (db.Model):
     __tablename__ = 'tw_msg'
     id = db.Column(db.Integer, unique=True, primary_key=True)
     time = db.Column(db.String(6))
     message = db.Column(db.String(140))
     hashtag =db.Column(db.String(140))
     url = db.Column(db.String(140))

#WTF Model(入力フィールド定義)
class Message_Add(FlaskForm):
    
     id = StringField('ID',validators=[DataRequired(), Length(min=1, max=3) ])
     time = StringField('Time',
                        validators=[DataRequired(), Length(min=2, max=60) ])
     message = StringField('Message')
     hashtag = StringField('Hashtag')
     url = StringField('URL')

     submit = SubmitField('Submit')

#ルーティングと処理
@app.route('/',methods=['GET', 'POST'])
def message():
     # db.create_all()
    #db.create_all()は、windows環境ではあっても動作するが(Migrateが認識しない空白のオペレー
   ションが出来てしまうことになるが)、CentOS環境では動作しない。(DBは自動生成されない。)
  従って削除して、wtfで作成した方が良い。

   #テーブルを新しく追加する時も、既に既存のtableがある場合、エラーになる。


     form = Message_Add()
     #新規データ受付時処理
     try:
          if form.validate_on_submit():
      
               message = Tw_msg( time = form.time.data,  message = form.message.data ,
                                 hashtag = form.hashtag.data, url = form.url.data )
               db.session.add(message)
               db.session.commit()
               flash('New message has been created!', 'success')
               return redirect(url_for('message'))
     except:
          flash('Failed at somewhere!', 'Unsuccess')

     #登録データ一覧表示    
     messages = Tw_msg.query.all()
     return render_template("message.html",form=form, messages=messages)

@app.route("/<int:message_id>/edit", methods=['GET', "POST"])
def edit_message(message_id):
     msg = Tw_msg.query.get(message_id)
     form = Message_Add()
     if form.validate_on_submit():
          msg.id = form.id.data
          msg.time = form.time.data
          msg.message = form.message.data
          msg.hashtag = form.hashtag.data
          msg.url = form.url.data
          db.session.commit()
          flash('Your post has been updated!', 'success')
          return redirect(url_for('message'))
     elif request.method == 'GET':
          form.id.data = msg.id
          form.time.data = msg.time
          form.message.data = msg.message
          form.hashtag.data = msg.hashtag
          form.url.data = msg.url
         
     #return "I'm doin' fine."
     return render_template("edit_message.html",form=form)

@app.route("/<int:message_id>/delete", methods=['GET', "POST"])
def delete_message(message_id):
    
    msg = Tw_msg.query.get_or_404(message_id)
    db.session.delete(msg)
    db.session.commit()
    flash('Your post has been deleted!', 'success')
    return redirect(url_for('message'))


==さて、実行してみる。


C:\Users\[ユーザ名]\Desktop\top>set FLASK_APP=tw_msg.py
C:\Users\[ユーザ名]\Desktop\top>flask db init

Creating directory C:\Users\[ユーザ名]\Desktop\top\migrations ... done
Creating directory C:\Users\[ユーザ名]\Desktop\top\migrations\versions ... done
Generating C:\Users\[ユーザ名]\Desktop\top\migrations\alembic.ini ... done
Generating C:\Users\[ユーザ名]\Desktop\top\migrations\env.py ... done
Generating C:\Users\[ユーザ名]\Desktop\top\migrations\README ... done
Generating C:\Users\[ユーザ名]\Desktop\top\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'C:\\Users\\[ユーザ名]\\Desktop\\top\\migrations\\alembic.ini' bef
ore proceeding.

で、Current Directoryにmigrationsというフォルダができて、以下のようなファイルが格納されている。

次に、現状を確認
>>> from tw_msg import db
>>> from tw_msg import Tw_msg
>>> a = Tw_msg.query.all()
>>> a[0].__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x0000000004E9D940>, 'hashtag': '# ハッシュタグ', 'time': '12:20', 'url': 'https://mossymob.net', 'message': 'メッセージ1', 'id': 1}
 
こんな感じ。
 
#DB Model
class Tw_msg (db.Model):
     __tablename__ = 'tw_msg'
     id = db.Column(db.Integer, unique=True, primary_key=True)
     time = db.Column(db.String(6))
     message = db.Column(db.String(140))
     hashtag =db.Column(db.String(140))
     url = db.Column(db.String(140))
     length = db.Column(db.String(10))
 
と、1つコラムを追加。
 
>flask db migrate "add length column" "コメントをつけるとエラー。なしで実行。
 
(venv) C:\Users\Toshiro\Desktop\top>flask db migrate
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'tw_msg.length'
Generating C:\Users\Toshiro\Desktop\top\migrations\versions\9da06fe9a5aa_.py ... done
 
migrationsフォルダの中に\versionsフォルダができて
9da06fe9a5aa_.pyが作成された。
 
中身は、
"""empty message
Revision ID: 9da06fe9a5aa
Revises:
Create Date: 2019-08-08 01:43:33.173202
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '9da06fe9a5aa'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('tw_msg', sa.Column('length', sa.String(length=10), nullable=True))
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('tw_msg', 'length')
    # ### end Alembic commands ###
良さそうなので、
 
(venv) C:\Users\ユーザ名\Desktop\top> 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  -> 9da06fe9a5aa, empty message
 
>>> from tw_msg import db
>>> from tw_msg import Tw_msg
>>> a= Tw_msg.query.all()
>>> a[0].__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x0000000004ABAA58>, 'length': None, 'hashtag': '#
ハッシュタグ', 'time': '12:20', 'url': 'https://mossymob.net', 'message': 'メッセージ1', 'id': 1}
 
ということで、既存のデータを変更せずに、コラムの追加に成功した。
 



(( トラブル対応))
migrationsフォルダーをdeleteしても、upgrade/downgradeが出来ない時がある。


alembic_version という名前のテーブルが残ってしまって、バージョン管理に齟齬が出てしまっているから。 layerが違うらしうく、通常のテーブル検索では表示されないが、普通のテーブルの消去の仕方でテーブルを消せる。

dropTableStatement = "DROP TABLE alembic_version"
cursor.execute(dropTableStatement)

等、

baseに戻ってみるのもあるかも。
from flask_migrate import downgrade
downgrade(revision='base')

自分の事象は、これでは上手くいかなかったけど。

(追記)
あれ、alembic tableを取得できる場合もあるね。 混乱中。

>>> cursor.execute("select * from sqlite_master where type='table'")
<sqlite3.Cursor object at 0x7f6a1f5e01f0>
>>> for x in cursor.fetchall():
...     print(x)
...
('table', 'alembic_version', 'alembic_version', 2, 'CREATE TABLE alembic_version (\n\tversion_num VARCHAR(32) NOT NULL, \n\tCONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n)')

0 件のコメント:

コメントを投稿