質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Flask

FlaskはPython用のマイクロフレームワークであり、Werkzeug・Jinja 2・good intentionsをベースにしています。

SQLAlchemy

SQLAlchemyとはPython 用のORMライブラリです。MIT Licenceのオープンソースとして提供されています。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

0回答

2862閲覧

Flask-SQLAlchemyでの楽観的排他処理実装方法がわかりません

akbr

総合スコア88

Flask

FlaskはPython用のマイクロフレームワークであり、Werkzeug・Jinja 2・good intentionsをベースにしています。

SQLAlchemy

SQLAlchemyとはPython 用のORMライブラリです。MIT Licenceのオープンソースとして提供されています。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2019/06/19 14:28

編集2022/01/12 10:55

Flaskで開発中です。
環境はWindows/Python3.7.0/flask1.0.3/MariaDB(xampp)です。

実現したいこと

Flask-SQLAlchemyで楽観的排他処理を実装しようとしています。
回答がつかないようなので、もし回答がしづらい理由がある場合それをコメント頂けたらと思います

質問概要

  1. 調べているとSQLAlchemyのversion_id_col機能があるとわかりましたが、これがまずうまくいきません。
  2. また、うまくいったとしてFormでversionを持ちまわろうかで答えが出ません。

2019/06/25追記
1.については解決したので解決方法を末尾に追記します
2.については引き続き回答を募集中です

質問1

まずversion_id_col機能自体についてですが、この情報を参考にmodel実装しました。
https://stackoverflow.com/questions/22684145/augmenting-flask-sqlalchemy-declarative-base

しかし、viewでformにhiddenとしてversion情報を持たせ、post後の保存時にそのversion情報をセットしてsession.commitとすると、その古いversion情報で普通に保存できてしまいます(つまり変化なし)。そして排他がかかりません。
編集画面表示(A)→別タブで同じレコードの編集画面表示(B)→(A)を保存→(B)を保存、としてもエラーになりません。

また、post後のの保存時にversion情報をセットしない(queryで取得した値をそのまま使う)場合は、versionカラムが自動でカウントアップはされるものの、やはり排他がかかりません。versionカラムは(A)と(B)でそれぞれ+1、合計+2あがります。
何か間違えているのでしょうか。

python

1# model 2@as_declarative() 3class BaseModel(db.Model): 4 __abstract__ = True 5 6 version = db.Column(db.BigInteger, nullable=False, server_default=db.text('1')) 7 8 @declared_attr 9 def __mapper_args__(cls): 10 return {'version_id_col': cls.version} 11 12class Sample(BaseModel): 13 __tablename__ = 'sample' 14 __table_args__ = {'extend_existing': True} 15 16 id = db.Column(db.Integer, primary_key=True) 17 name = db.Column(db.Text) 18 19 def __repr__(self): 20 return '<Sample {}:{}>'.format(self.id, self.name)

python

1# view 2@bp.route('/<id>/edit/', methods=['GET', 'POST']) 3def edit(id): 4 sample = Sample.query.get(id) 5 form = SampleEditForm(request.form, obj=sample) 6 if sample is None: 7 abort(404) 8 if request.method == 'POST': 9 if form.validate_on_submit(): 10 sample.name = form['name'].data 11 sample.version = form['version'].data 12 13 db.session.add(sample) 14 db.session.commit() 15 16 flash("保存しました", "success") 17 return redirect(url_for('.detail', id=sample.id)) 18 19 flash("エラーがあります", "error") 20 return render_template('sample/edit.html', form=form) 21 22 return render_template('sample/edit.html', form=form)

質問2

次に、仮にversion_id_colによって楽観的排他が実装できたとして
version情報のようなユーザー書き換えを禁止したい値はどのように持ちまわるのが良いのでしょうか?

hiddenで持ちまわるとユーザー書き換えが可能だと思うので違うような気がします。
flaskはsessionに格納すると暗号化される(?)ので書き換えはできないかもしれませんが、(sessionを扱ったことがなくてよくわかっていないだけかもですが)全てのエンドポイントに他のエンドポイントのsession情報がいきわたり、さらに複数タブとか考えると管理が煩雑・・というか方法が思いつきません。

一つ思いついたのはhiddenの暗号化ですが、flask標準にそのような機能は無く、ググってもそもそもそういう情報がほぼ見つからなかったので一般的ではないのかもしれません。

よろしくお願いいたします。

質問1の解決方法

色々な記述方法を試していたら無事楽観的排他がかかりました。
結局ネット上では答えが探せず、いろんなパターンを試しただけのため、良い方法なのかはわかりませんが。
要点としては、

  • version_id_colの指定は正しかった
  • viewでpost後にmodelインスタンスを生成する際、dbからgetするのではなく空で作ってからmergeする
  • versionをhiddenで持ちまわりたいがversionはintに対してhiddenにするとstrになってしまうので変換する

※直接関係ないところは削除してるので多少矛盾があるかもしれませんご了承ください

python

1models/sample.py 2class BaseModelMixin(db.Model): 3 __abstract__ = True 4 5 @classmethod 6 def get(cls, pk): 7 return cls.query.get(pk) 8 9 @declared_attr 10 def id(cls): 11 return db.Column(db.Integer, primary_key=True) 12 13 def __repr__(self): 14 return15 16 17class VersionMixin(object): 18 version_id = db.Column(db.BigInteger, nullable=False) 19 20 __mapper_args__ = { 21 'version_id_col': version_id, 22 } 23 24 25class Sample(BaseModelMixin, VersionMixin): 26 __tablename__ = 'sample' 27 __table_args__ = {'extend_existing': True} 28 name = db.Column(db.String(length=255))

python

1forms/sample.py 2class HiddenField(simple.HiddenField): 3 def populate_obj(self, obj, name): 4 if self.data == "": 5 data = None 6 elif isinstance(self.object_data, int): 7 data = int(self.data) 8 else: 9 data = self.data 10 11 setattr(obj, name, data) 12 13 14class SampleEditForm(): 15 id = HiddenField() 16 version_id = HiddenField() 17 name = StringField("名前", validators=[DataRequired()])

python

1views/sample.py 2@bp.route('/<id>/edit/', methods=['GET', 'POST']) 3def edit(id): 4 sample = Sample.query.get(id) 5 form = SampleEditForm(request.form, obj=sample) 6 7 if sample is None: 8 abort(404) 9 if request.method == 'POST': 10 if form.validate_on_submit(): 11 sample = Sample() 12 form.populate_obj(sample) 13 db.session.merge(sample) 14 db.session.commit() 15 flash("保存しました", "success") 16 return redirect(url_for('.detail', id=sample.id)) 17 18 flash("エラーがあります", "error") 19 return render_template('sample/edit.html', form=form) 20 21 return render_template('sample/edit.html', form=form)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだ回答がついていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問