2016-05-22 3 views
0

私のモデルには次のコードがあります。 2人が同時にレコードを更新する場合、2つのStateのオブジェクトがdefault: trueに設定されてしまう可能性があるため、Rails 4モデルで競合状態を修正してテストする方法

class State < ActiveRecord::Base 
    def make_default! 
    State.update_all(default: false) 
    update!(default: true) 
    end 
end 

:誰かが正しくmake_default!方法が競合状態を引き起こす可能性があることを指摘しました。

私はActiveRecordのロックを使用してこの回避策を使用:

class State < ActiveRecord::Base 
    def make_default! 
    # Prevent race condition using database-level locks. 
    State.transaction do 
     State.where.not(id: id).lock(true).update_all(default: false) 
     State.where(id: id).lock(true).first.update!(default: true) 
    end 
    end 
end 

誰かが、これは他の潜在的なバグを引き起こす可能性があることを指摘しました。 ロックを実装する最良の方法と、モデルスペック(RSpecを使用して)をテストする方法について知りたいですか?

何か助けがありがとう:)ありがとう!

+1

私は実際にトランザクションでそれをラップするだけで、実際には十分であり、ロックは必要ありません。あなたはそれを実行して、update_allとアップデートの間に大きなスリープを入れ、それを使わずに別のアップデートを実行し、何が起こるかを見てテストすることができます。私は最後のものが勝つはずだと思うし、デフォルトが真でなければならない。また、複数のデフォルトがtrueであることを受け入れることもできます。デフォルトを検索すると、常に最新のものを取ります(updated_atタイムスタンプ付き)。 – jrochkind

答えて

2

問題を完全に回避する最も簡単な方法は、ブール値のtrue/false列に頼るのではなく、デフォルトのエントリの参照を他の場所に保持することです。

だから、国家とpolymorphic associationを持ってdefault_entitiesテーブルを持つことができます。

entity_id | entity_type 
----------------------- 
     1 |  State 

は、デフォルト値を更新するだけで、単一の行を変更する必要があり、したがってatomic operationです。

個別のキー値ストアを使用しても、それはアトミックな操作になりますが、その場合はトランザクションの失敗でロールバックを処理する必要があります。

また、何らかの理由で既存のスキーマを保持したい場合は、PostgreSQL advisory locksを使用してテーブル全体をロックすることをお勧めします。このgood introductionから引用

[Postgresのアドバイザリロックは】アプリケーションは、データベースロックを強制されます。 アドバイザリ・ロックは、セッション・レベルおよび トランザクション・レベルで取得でき、セッションが終了したとき、またはトランザクションが完了したときに予期したとおりに解放されるか、または トランザクションが完了したときに解放されます。

したがって、デフォルト状態を更新する前にロックを取得しようとすることができます。それが成功すると、テーブルを更新してから解放します。ロックの取得に失敗した場合は、一定回数再試行できます。

ここでの利点は、状態テーブルに関する他の無関係の操作が妨げられないことです。

関連する問題