2人以上のユーザーによる同じデータベースエントリの同時変更から保護する方法はありますか?
2番目のコミット/保存操作を実行しているユーザーにエラーメッセージを表示することは許容されますが、データを黙って上書きしないでください。
ユーザーが「戻る」ボタンを使用するか、単にブラウザを閉じて、ロックを永久に残す可能性があるため、エントリをロックすることはオプションではないと思います。
2人以上のユーザーによる同じデータベースエントリの同時変更から保護する方法はありますか?
2番目のコミット/保存操作を実行しているユーザーにエラーメッセージを表示することは許容されますが、データを黙って上書きしないでください。
ユーザーが「戻る」ボタンを使用するか、単にブラウザを閉じて、ロックを永久に残す可能性があるため、エントリをロックすることはオプションではないと思います。
これは、Django で楽観的ロックを行う方法です。
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
.update(updated_field=new_value, version=e.version+1)
if not updated:
raise ConcurrentModificationException()
上記のコードは、カスタム マネージャーのメソッドとして実装できます。
私は次の仮定をしています:
これらの仮定は、他の誰もエントリを更新していないことを確認するのに十分です。この方法で複数の行が更新される場合は、トランザクションを使用する必要があります。
警告 Django Doc :
update() メソッドは直接 SQL ステートメントに変換されることに注意してください。直接更新の一括操作です。モデルで save() メソッドを実行したり、pre_save または post_save シグナルを発行したりしません。
この質問は少し古く、私の答えは少し遅れていますが、私が理解した後、これはDjango 1.4 で次のように修正されました:
select_for_update(nowait=True)
ドキュメントを参照してください
トランザクションが終了するまで行をロックするクエリセットを返し、サポートされているデータベースで SELECT ... FOR UPDATE SQL ステートメントを生成します。
通常、選択した行のいずれかで別のトランザクションがすでにロックを取得している場合、クエリはロックが解除されるまでブロックされます。これが必要な動作でない場合は、select_for_update(nowait=True) を呼び出します。これにより、通話が非ブロッキングになります。競合するロックが別のトランザクションによって既に取得されている場合、クエリセットが評価されるときに DatabaseError が発生します。
もちろん、これはバックエンドが「select for update」機能をサポートしている場合にのみ機能しますが、たとえば sqlite はサポートしていません。残念ながら、:nowait=True
は MySql でサポートされていません。使用する必要があります:nowait=False
は、ロックが解除されるまでのみブロックします。
実際には、ここではトランザクションはあまり役に立ちません...複数の HTTP リクエストでトランザクションを実行したい場合を除きます (これはおそらく望ましくありません)。
そのような場合に通常使用するのは「楽観的ロック」です。私の知る限り、Django ORM はそれをサポートしていません。しかし、この機能を追加することについていくつかの議論がありました.
だからあなたはあなた自身です。基本的に、モデルに「バージョン」フィールドを追加し、非表示フィールドとしてユーザーに渡す必要があります。更新の通常のサイクルは次のとおりです。
楽観的ロックを実装するには、データを保存するときに、ユーザーから返されたバージョンがデータベース内のバージョンと同じかどうかを確認し、データベースを更新してバージョンを増やします。そうでない場合は、データがロードされてから変更があったことを意味します。
次のような単一のSQL呼び出しでそれを行うことができます:
UPDATE ... WHERE version = 'version_from_user';
この呼び出しは、バージョンがまだ同じ場合にのみデータベースを更新します。
今後の参考のために、https://github.com/RobCombs/django-lockingを確認してください。ユーザーがページを離れたときの javascript のロック解除とロックのタイムアウト (ユーザーのブラウザーがクラッシュした場合など) を組み合わせることにより、永続的なロックを残さない方法でロックを行います。ドキュメントはかなり完全です。
この問題に関係なく、少なくとも django トランザクション ミドルウェアを使用する必要があります。
複数のユーザーが同じデータを編集するという実際の問題については...はい、ロックを使用してください。また:
ユーザーが更新しているバージョンを確認し (これを安全に行うことで、ユーザーが単にシステムをハッキングして、最新のコピーを更新していると言うことができなくなります!)、そのバージョンが最新である場合にのみ更新します。それ以外の場合は、ユーザーが編集していた元のバージョン、送信されたバージョン、および他のユーザーが作成した新しいバージョンを含む新しいページをユーザーに送り返します。変更を 1 つの完全に最新のバージョンにマージするよう依頼してください。diff+patch のようなツールセットを使用してこれらを自動マージしようとするかもしれませんが、とにかく失敗した場合に手動のマージ方法が機能する必要があるため、それから始めてください。また、バージョン履歴を保持し、誰かが意図せずまたは故意にマージを台無しにした場合に備えて、管理者が変更を元に戻せるようにする必要があります。しかし、とにかくそれを持っている必要があります。
あなたのためにこれのほとんどを行うdjangoアプリ/ライブラリがある可能性が非常に高いです。
探すべきもう 1 つのことは、「atomic」という単語です。アトミック操作とは、データベースの変更が成功するか、明らかに失敗することを意味します。クイック検索では、Django でのアトミック操作について尋ねるこの質問が表示されます。
上記の考え方
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
.update(updated_field=new_value, version=e.version+1)
if not updated:
raise ConcurrentModificationException()
見栄えがよく、シリアル化可能なトランザクションがなくても正常に動作するはずです。
問題は、デフォルトの .save() 動作を拡張して、手動で .update() メソッドを呼び出す必要がないようにする方法です。
カスタムマネージャーのアイデアを見ました。
私の計画は、更新を実行するために Model.save_base() によって呼び出される Manager _update メソッドをオーバーライドすることです。
これは Django 1.3 の現在のコードです
def _update(self, values, **kwargs):
return self.get_query_set()._update(values, **kwargs)
何をする必要があるか私見は次のようなものです:
def _update(self, values, **kwargs):
#TODO Get version field value
v = self.get_version_field_value(values[0])
return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)
同様のことが削除時に発生する必要があります。ただし、Django は django.db.models.deletion.Collector を介してこの分野でかなりのブードゥー教を実装しているため、削除は少し難しくなります。
Django のような modren ツールに Optimictic Concurency Control のガイダンスがないのは奇妙です。
なぞなぞが解けたらこの記事を更新します。うまくいけば、解決策は、大量のコーディング、奇妙なビュー、Djangoの重要な部分のスキップなどを伴わない、素晴らしいpythonicな方法になるでしょう.
安全のために、データベースはトランザクションをサポートする必要があります。
フィールドがテキストなどの「自由形式」で、複数のユーザーが同じフィールドを編集できるようにする必要がある場合 (データに対して単一のユーザー所有権を持つことはできません)、元のデータを変数。ユーザーがコミットするとき、入力データが元のデータから変更されているかどうかを確認します (変更されていない場合は、古いデータを書き換えて DB を煩わす必要はありません)。元のデータがデータベース内の現在のデータと比較して同じである場合保存できます。変更されている場合は、ユーザーに違いを示して、ユーザーに何をすべきかを尋ねることができます。
フィールドが口座残高、ストア内のアイテム数などの数値である場合、元の値 (ユーザーがフォームへの入力を開始したときに保存された値) と新しい値の差を計算すると、より自動的に処理できます。トランザクションを開始して現在の値を読み取り、差を追加してから、トランザクションを終了します。負の値を持つことができない場合、結果が負の場合はトランザクションを中止し、ユーザーに通知する必要があります。
私はジャンゴを知らないので、あなたに cod3 を与えることはできません.. ;)
ここから:
他の誰かが変更したオブジェクトを上書きしないようにする方法
タイムスタンプは、詳細を保存しようとしているフォームの非表示フィールドとして保持されると想定しています。
def save(self):
if(self.id):
foo = Foo.objects.get(pk=self.id)
if(foo.timestamp > self.timestamp):
raise Exception, "trying to save outdated Foo"
super(Foo, self).save()