Django:データベースエントリの同時変更から保護するにはどうすればよいですか?


81

2人以上のユーザーによる同じデータベースエントリの同時変更から保護する方法はありますか?

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

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


4
1つのオブジェクトを複数の同時ユーザーが更新できる場合は、より大きな設計上の問題が発生する可能性があります。これが問題になるのを防ぐために、ユーザー固有のリソースを検討するか、処理ステップを別々のテーブルに分割することを検討する価値があるかもしれません。
S.Lott 2008年

回答:


48

これは私が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のメソッドとして実装できます。

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

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

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

警告 DjangoDoc

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


12
いいね!しかし、それは「&&」ではなく「&」であるべきではありませんか?
Giles Thomas

1
オーバーライドされた独自のsave()メソッド内に「update」の呼び出しを配置することで、「update」がsave()メソッドを実行しないという問題を回避できますか?
Jonathan Hartley

1
2つのスレッドが同時に呼び出しfilter、両方が変更されていない同一のリストを受信し、e次に両方が同時に呼び出すとupdateどうなりますか?フィルタと更新を同時にブロックするセマフォは見当たりません。編集:ああ、私は今怠惰なフィルターを理解しています。しかし、update()がアトミックであると仮定することの妥当性は何ですか?確かにDBは同時アクセスを処理します
totowtwo 2011年

1
@totowtwo ACIDのIは、順序付けを保証します(en.wikipedia.org/wiki/ACID)。同時(後で開始)SELECTに関連するデータに対してUPDATEが実行されている場合、UPDATEが完了するまでブロックされます。ただし、複数のSELECTを同時に実行できます。
キットサンデ2013

1
これは自動コミットモード(デフォルト)でのみ正しく機能するようです。そうしないと、最終的なCOMMITがこの更新SQLステートメントから分離されるため、それらの間で並行コードを実行できます。また、DjangoにはReadCommited分離レベルがあるため、古いバージョンを読み取ります。(ここで手動トランザクションが必要な理由-この更新とともに別のテーブルに行を作成したいためです。)ただし、すばらしいアイデアです。
Alex Lokk 2013

39

この質問は少し古く、私の答えは少し遅れていますが、私が理解した後、これはDjango1.4で次のように修正されました

select_for_update(nowait=True)

ドキュメントを参照してください

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

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

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


2
これは素晴らしい答えではありません-質問は明示的に(悲観的な)ロックを望んでいませんでした、そして2つのより高い投票の答えは現在その理由で楽観的同時実行制御(「楽観的ロック」)に焦点を合わせています。ただし、他の状況では、更新の選択は問題ありません。
RichVel 2014年

@ giZm0それでも悲観的なロックになります。ロックを取得する最初のスレッドは、ロックを無期限に保持できます。
knaperek 2014年

6
私はこの答えが好きです。なぜなら、それはDjangoのドキュメントであり、サードパーティの美しい発明ではないからです。
anizzomc 2015年

29

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

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

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

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

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

これは、次のような1回のSQL呼び出しで実行できます。

UPDATE ... WHERE version = 'version_from_user';

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


1
これと同じ質問がスラッシュドットにも出てきました。あなたがお勧めオプティミスティック・ロックもあり、提案、より良い私見ビットを説明した。hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367
hoplaを

5
また、あなたがこのような状況を回避するために、この上にトランザクションを使いたいんのでご注意:hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Djangoは開始、トランザクションにデータベース上のすべてのアクションをラップ自動的にミドルウェアを提供します最初のリクエストから、成功した応答の後にのみコミットする:docs.djangoproject.com/en/dev/topics/db/transactions注意:トランザクションミドルウェアは、楽観的ロックに関する上記の問題を回避するのに役立つだけで、ロックは提供しません単独で)
hopla

これを行う方法の詳細も探しています。今のところ運がない。
seanyboy 2009

1
これは、djangoの一括更新を使用して行うことができます。私の答えを確認してください。
Andrei Savu 2010年

14

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、重複したトリガーを黙ってスキップできます。


1
更新用の選択をtransaction.atomic()でラップする必要がありますか?私が実際に結果を更新に使用している場合はどうなりますか?テーブル全体をロックしてselect_for_updateをnoopにしませんか?
PaulKenjora19年

3

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


3
私、これは本当に奇妙な考えです。
julx 2011年

1

この問題に関係なく、少なくともdjangoトランザクションミドルウェアを使用する必要があります。

複数のユーザーが同じデータを編集するという実際の問題については...はい、ロックを使用してください。または:

ユーザーが更新しているバージョンを確認し(これを安全に行うと、ユーザーはシステムをハッキングして最新のコピーを更新していると言うことができなくなります!)、そのバージョンが最新である場合にのみ更新します。それ以外の場合は、編集中の元のバージョン、送信されたバージョン、および他のユーザーが作成した新しいバージョンを含む新しいページをユーザーに送り返します。変更を1つの完全に最新のバージョンにマージするように依頼します。diff + patchなどのツールセットを使用してこれらを自動マージしようとする場合もありますが、とにかく失敗した場合に手動マージ方法を機能させる必要があるため、それから始めます。また、誰かが意図せずまたは意図的にマージを台無しにした場合に備えて、バージョン履歴を保持し、管理者が変更を元に戻せるようにする必要があります。しかし、とにかくそれを持っているはずです。

これのほとんどをあなたに代わって行うdjangoアプリ/ライブラリがおそらくあります。


これも、ギヨームが提案したように、楽観的ロックです。しかし、彼はすべてのポイントを獲得したようでした:)
hopla

0

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


トランザクションを実行したり、複数のリクエスト間でロックしたりする必要はありません。これには時間がかかる可能性があるためです(そして、まったく終了しない可能性があります)
Ber

トランザクションが開始された場合、それは終了する必要があります。ユーザーが「送信」をクリックした後でのみ、レコードをロックする(またはトランザクションを開始するなど)必要があります。レコードを開いて表示するときではありません。
ハーレーホルコム

はい、しかし私の問題は異なります。2人のユーザーが同じフォームを開いてから、両方が変更をコミットするという点です。ロックがこれに対する解決策ではないと思います。
Ber

あなたは正しいですが、問題はこれに対する解決策ないということです。1人のユーザーが勝ち、もう1人が失敗メッセージを受け取ります。後でレコードをロックすると、問題が少なくなります。
ハーレーホルコム

同意する。私は他のユーザーへの失敗メッセージを完全に受け入れます。私はこのケースを検出するための良い方法を探しています(これは非常にまれであると予想しています)。
Ber

0

上記のアイデア

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な方法になるでしょう。


-2

安全のために、データベースはトランザクションをサポートする必要があります。

フィールドがテキストなどの「自由形式」であり、複数のユーザーが同じフィールドを編集できるようにする必要がある場合(データに対して単一のユーザー所有権を持つことはできません)、元のデータをに保存できます。変数。ユーザーがコミットするときに、入力データが元のデータから変更されているかどうかを確認します(変更されていない場合は、古いデータを書き換えてDBを煩わせる必要はありません)。データベース内の現在のデータと比較した元のデータが同じである場合保存できます。変更されている場合は、ユーザーに違いを示し、ユーザーに何をすべきかを尋ねることができます。

フィールドが数値(口座残高、店舗内のアイテム数など)の場合、元の値(ユーザーがフォームへの入力を開始したときに保存された)と新しい値の差を計算すると、より自動的に処理できます。トランザクションを開始し、現在の値を読み取って差を追加してから、トランザクションを終了します。負の値を設定できない場合、結果が負の場合はトランザクションを中止し、ユーザーに通知する必要があります。

私はdjangoを知らないので、cod3を提供することはできません..;)


-6

ここから:
他の誰かが変更したオブジェクトの上書きを防ぐ方法

タイムスタンプは、詳細を保存しようとしているフォームの非表示フィールドとして保持されると想定しています。

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()

1
コードが壊れています。ifチェックとsaveクエリの間に競合状態が発生する可能性があります。objects.filter(id = ..&timestamp check).update(...)を使用し、行が更新されていない場合は例外を発生させる必要があります。
Andrei Savu 2010年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.