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()
上記のコードは、CustomManagerのメソッドとして実装できます。
私は次の仮定をしています:
これらの仮定は、他の誰も以前にエントリを更新していないことを保証するのに十分です。この方法で複数の行が更新される場合は、トランザクションを使用する必要があります。
警告 DjangoDoc:
update()メソッドはSQLステートメントに直接変換されることに注意してください。直接更新の一括操作です。モデルでsave()メソッドを実行したり、pre_saveまたはpost_saveシグナルを発行したりすることはありません。
filter
、両方が変更されていない同一のリストを受信し、e
次に両方が同時に呼び出すとupdate
どうなりますか?フィルタと更新を同時にブロックするセマフォは見当たりません。編集:ああ、私は今怠惰なフィルターを理解しています。しかし、update()がアトミックであると仮定することの妥当性は何ですか?確かにDBは同時アクセスを処理します
この質問は少し古く、私の答えは少し遅れていますが、私が理解した後、これはDjango1.4で次のように修正されました。
select_for_update(nowait=True)
ドキュメントを参照してください
トランザクションが終了するまで行をロックするクエリセットを返し、サポートされているデータベースでSELECT ... FOR UPDATESQLステートメントを生成します。
通常、別のトランザクションが選択した行の1つですでにロックを取得している場合、クエリはロックが解除されるまでブロックされます。これが目的の動作でない場合は、select_for_update(nowait = True)を呼び出します。これにより、通話が非ブロッキングになります。競合するロックがすでに別のトランザクションによって取得されている場合、クエリセットが評価されるときにDatabaseErrorが発生します。
もちろん、これはバックエンドが「更新のために選択」機能をサポートしている場合にのみ機能しますが、たとえばsqliteはサポートしていません。残念ながら:nowait=True
MySqlではサポートされていないため、:を使用する必要がありますnowait=False
。これは、ロックが解除されるまでのみブロックされます。
実際、トランザクションはここではあまり役に立ちません...複数のHTTPリクエストでトランザクションを実行したい場合を除きます(おそらく望ましくないでしょう)。
そのような場合に通常使用するのは「楽観的ロック」です。私の知る限り、DjangoORMはそれをサポートしていません。しかし、この機能の追加についてはいくつかの議論がありました。
だからあなたはあなた自身です。基本的に、あなたがすべきことは、モデルに「バージョン」フィールドを追加し、それを非表示フィールドとしてユーザーに渡すことです。更新の通常のサイクルは次のとおりです。
楽観的ロックを実装するには、データを保存するときに、ユーザーから取得したバージョンがデータベース内のバージョンと同じであるかどうかを確認してから、データベースを更新してバージョンをインクリメントします。そうでない場合は、データがロードされてから変更があったことを意味します。
これは、次のような1回のSQL呼び出しで実行できます。
UPDATE ... WHERE version = 'version_from_user';
この呼び出しは、バージョンがまだ同じである場合にのみデータベースを更新します。
Django 1.11には、ビジネスロジックの要件に応じて、この状況を処理するための3つの便利なオプションがあります。
Something.objects.select_for_update()
モデルが解放されるまでブロックされますSomething.objects.select_for_update(nowait=True)
DatabaseError
モデルが現在更新のためにロックされているかどうかをキャッチしますSomething.objects.select_for_update(skip_locked=True)
現在ロックされているオブジェクトは返されませんさまざまなモデルでインタラクティブワークフローとバッチワークフローの両方を備えたアプリケーションで、並行処理シナリオのほとんどを解決するためにこれら3つのオプションを見つけました。
「待機」select_for_update
は、順次バッチプロセスで非常に便利です。すべてを実行してもらいたいのですが、時間をかけてください。ザ・nowait
は、ユーザーが現在更新のためにロックされているオブジェクトを変更したい場合に使用されます。現時点で変更されていることをユーザーに伝えます。
これskip_locked
は、ユーザーがオブジェクトの再スキャンをトリガーできる別のタイプの更新に役立ちます。オブジェクトがトリガーされている限り、誰がトリガーするかは関係ありません。そのためskip_locked
、重複したトリガーを黙ってスキップできます。
今後の参考のために、https://github.com/RobCombs/django-lockingをチェックしてください。ユーザーがページを離れるときのJavaScriptのロック解除と、ロックのタイムアウト(ユーザーのブラウザーがクラッシュした場合など)を組み合わせることにより、永続的なロックを残さない方法でロックを実行します。ドキュメントはかなり完成しています。
この問題に関係なく、少なくともdjangoトランザクションミドルウェアを使用する必要があります。
複数のユーザーが同じデータを編集するという実際の問題については...はい、ロックを使用してください。または:
ユーザーが更新しているバージョンを確認し(これを安全に行うと、ユーザーはシステムをハッキングして最新のコピーを更新していると言うことができなくなります!)、そのバージョンが最新である場合にのみ更新します。それ以外の場合は、編集中の元のバージョン、送信されたバージョン、および他のユーザーが作成した新しいバージョンを含む新しいページをユーザーに送り返します。変更を1つの完全に最新のバージョンにマージするように依頼します。diff + patchなどのツールセットを使用してこれらを自動マージしようとする場合もありますが、とにかく失敗した場合に手動マージ方法を機能させる必要があるため、それから始めます。また、誰かが意図せずまたは意図的にマージを台無しにした場合に備えて、バージョン履歴を保持し、管理者が変更を元に戻せるようにする必要があります。しかし、とにかくそれを持っているはずです。
これのほとんどをあなたに代わって行う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()
見栄えが良く、シリアル化可能なトランザクションがなくても正常に動作するはずです。
問題は、.update()メソッドを呼び出すために手動の配管を行う必要がないように、デフォルトの.save()動作をどのように拡張するかです。
カスタムマネージャーのアイデアを見ました。
私の計画は、更新を実行するためにModel.save_base()によって呼び出されるManager_updateメソッドをオーバーライドすることです。
これはDjango1.3の現在のコードです
def _update(self, values, **kwargs):
return self.get_query_set()._update(values, **kwargs)
IMHOで行う必要があるのは次のようなものです。
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ツールにOptimicticConcurencyControlのガイダンスがないのは奇妙です。
なぞなぞを解いたら、この投稿を更新します。うまくいけば、解決策は、大量のコーディング、奇妙なビュー、Djangoの重要な部分のスキップなどを伴わない素晴らしいpythonicな方法になるでしょう。
安全のために、データベースはトランザクションをサポートする必要があります。
フィールドがテキストなどの「自由形式」であり、複数のユーザーが同じフィールドを編集できるようにする必要がある場合(データに対して単一のユーザー所有権を持つことはできません)、元のデータをに保存できます。変数。ユーザーがコミットするときに、入力データが元のデータから変更されているかどうかを確認します(変更されていない場合は、古いデータを書き換えてDBを煩わせる必要はありません)。データベース内の現在のデータと比較した元のデータが同じである場合保存できます。変更されている場合は、ユーザーに違いを示し、ユーザーに何をすべきかを尋ねることができます。
フィールドが数値(口座残高、店舗内のアイテム数など)の場合、元の値(ユーザーがフォームへの入力を開始したときに保存された)と新しい値の差を計算すると、より自動的に処理できます。トランザクションを開始し、現在の値を読み取って差を追加してから、トランザクションを終了します。負の値を設定できない場合、結果が負の場合はトランザクションを中止し、ユーザーに通知する必要があります。
私はdjangoを知らないので、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()