Flaskで開発中です。
環境はWindows/Python3.7.0/flask1.0.3/MariaDB(xampp)です。
実現したいこと
Flask-SQLAlchemyで楽観的排他処理を実装しようとしています。
回答がつかないようなので、もし回答がしづらい理由がある場合それをコメント頂けたらと思います
質問概要
- 調べているとSQLAlchemyのversion_id_col機能があるとわかりましたが、これがまずうまくいきません。
- また、うまくいったとして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 return 略 15 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)
あなたの回答
tips
プレビュー