web-dev-qa-db-ja.com

Django:データベースエントリの同時変更に対する保護方法

複数のユーザーによる同じデータベースエントリの同時変更から保護する方法がある場合

2回目のコミット/保存操作を実行しているユーザーにエラーメッセージを表示することは許容されますが、データを静かに上書きしないでください。

ユーザーは「戻る」ボタンを使用するか、単にブラウザを閉じてロックを永久に残すため、エントリをロックすることはオプションではないと思います。

76
Ber

これは私が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()

上記のコードは、 Custom Manager のメソッドとして実装できます。

私は次のことを仮定しています:

  • filter()。update()は、フィルターが遅延しているため、単一のデータベースクエリになります
  • データベースクエリはアトミックです

これらの仮定は、他の誰も以前にエントリを更新していないことを保証するのに十分です。この方法で複数の行が更新される場合は、トランザクションを使用する必要があります。

[〜#〜] warning [〜#〜]Django Doc

Update()メソッドはSQLステートメントに直接変換されることに注意してください。直接更新の一括操作です。モデルでsave()メソッドを実行したり、pre_saveまたはpost_saveシグナルを送信したりしません。

46
Andrei Savu

この質問は少し古く、私の答えは少し遅れていますが、理解した後、これはDjango 1.4使用:

select_for_update(nowait=True)

docs を参照してください

トランザクションの終了まで行をロックするクエリセットを返し、サポートされているデータベースでSELECT ... FOR UPDATE SQLステートメントを生成します。

通常、選択した行の1つで別のトランザクションがすでにロックを取得している場合、ロックが解除されるまでクエリはブロックされます。これが目的の動作でない場合は、select_for_update(nowait = True)を呼び出します。これにより、呼び出しが非ブロッキングになります。競合するロックが別のトランザクションによってすでに取得されている場合、クエリセットが評価されるときにDatabaseErrorが発生します。

もちろん、これは、バックエンドが「select for update」機能をサポートしている場合にのみ機能します。これは、たとえばsqliteではサポートされていません。残念ながら:nowait=TrueはMySqlでサポートされていないため、使用する必要があります:nowait=False、ロックが解除されるまでのみブロックします。

35
giZm0

実際、トランザクションは、複数のHTTPリクエストでトランザクションを実行したい場合を除いて、ここではあまり役に立ちません(これはおそらく望まないでしょう)。

このような場合に通常使用するのは「オプティミスティックロック」です。 Django ORMは、私の知る限りそれをサポートしていません。しかし、この機能の追加についてはいくつかの議論がありました。

だからあなたは自分でいる。基本的には、モデルに「バージョン」フィールドを追加して、非表示フィールドとしてユーザーに渡す必要があります。更新の通常のサイクルは次のとおりです。

  1. データを読み取り、ユーザーに表示します
  2. ユーザーがデータを変更
  3. ユーザーがデータを投稿する
  4. アプリはそれをデータベースに保存します。

楽観的ロックを実装するには、データを保存するときに、ユーザーから返されたバージョンがデータベース内のバージョンと同じかどうかを確認し、データベースを更新してバージョンをインクリメントします。そうでない場合は、データがロードされてから変更があったことを意味します。

次のような1つのSQL呼び出しでそれを行うことができます。

UPDATE ... WHERE version = 'version_from_user';

この呼び出しは、バージョンが同じ場合にのみデータベースを更新します。

28
Guillaume

Django 1.11には、ビジネスロジック要件に応じてこの状況を処理するための つの便利なオプション があります。

  • Something.objects.select_for_update()は、モデルが解放されるまでブロックします
  • モデルが現在更新のためにロックされている場合は、Something.objects.select_for_update(nowait=True)とcatch DatabaseError
  • Something.objects.select_for_update(skip_locked=True)は、現在ロックされているオブジェクトを返しません

さまざまなモデルで対話型ワークフローとバッチワークフローの両方を備えたアプリケーションで、ほとんどの同時処理シナリオを解決するために、これら3つのオプションを見つけました。

「待機中」select_for_updateは、順次バッチ処理では非常に便利です。すべてを実行したいのですが、時間をかけてみましょう。 nowaitは、ユーザーが現在更新のためにロックされているオブジェクトを変更する場合に使用されます。この時点で変更されていることを伝えます。

skip_lockedは、ユーザーがオブジェクトの再スキャンをトリガーできる別のタイプの更新に役立ちます。トリガーされる限り、誰がトリガーするかは気にしません。したがって、skip_lockedを使用すると、重複したトリガーを静かにスキップできます。

13
kravietz

今後の参考のために、 https://github.com/RobCombs/Django-locking をご覧ください。ユーザーがページを離れるときのjavascriptのロック解除とロックタイムアウト(たとえば、ユーザーのブラウザーがクラッシュした場合)が混在することにより、永続的なロックを残さない方法でロックを行います。ドキュメントはかなり完成しています。

3

上記のアイデア

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メソッドをオーバーライドすることです。

これは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など)のスキップを伴わない素敵なPythonの方法で解決されることを願っています。

0
Kiril

探すべきもう1つのことは、「アトミック」という言葉です。アトミック操作とは、データベースの変更が正常に行われるか、明らかに失敗することを意味します。クイック検索で この質問 がDjangoのアトミック操作について尋ねています。

0
Harley Holcombe