RailsRESTAPIでのオプティミスティックロック
次の架空のシナリオを想像してみてください。賃貸物件管理システムで、従業員Aが賃貸Xの連絡先情報の編集を開始し、電話番号を追加します。同じ頃、従業員Bは、まさにそのRental Xの連絡先情報のタイプミスに気づき、更新を実行します。数分後、従業員AがRental Xの連絡先情報を新しい電話番号で更新し、...タイプミスを修正する更新がなくなりました!
それは間違いなく素晴らしいことではありません!そして、これはかなり些細なシナリオです。金融システムで起こっている同様の紛争を想像してみてください!
将来、そのようなシナリオを回避できますか?幸いなことに、答えはイエスです!このような問題を防ぐには、同時実行性の保護とロック、特に楽観的ロックが必要です。
RailsRESTAPIでの楽観的ロックについて調べてみましょう。
「失われた更新」と楽観的ロックと悲観的ロック
今行ったシナリオは、「失われた更新」の一種です。 2つの同時トランザクションが同じ行の同じ列を更新すると、基本的に最初のトランザクションが発生しなかったかのように、2番目のトランザクションが最初のトランザクションからの変更を上書きします。
通常、この問題は次の方法で対処できます。
- データベースレベルで問題を処理する適切なトランザクション分離レベルを設定します。
- ペシミスティックロック—同時トランザクションが同じ行を更新するのを防ぎます。 2番目のトランザクションは、最初のトランザクションが終了するのを待ってから、データを読み取ります。ここでの大きな利点は、古いデータを操作できないことです。ただし、主な欠点は、特定の行からのデータの読み取りもブロックされることです。
- オプティミスティックロック—変更時の状態が読み取られたときと異なる場合、指定された行の変更を停止します。
私たちの問題は、同時データベーストランザクション(ビジネストランザクションのようなもの)に関するものではないため、最初のソリューションは実際には適用できません。これは、悲観的なロックと楽観的なロックが残っていることを意味します。
悲観的なロックは、最初に、仮想シナリオで失われた更新が発生するのを防ぎます。ただし、データへのアクセスを非常に長い間ブロックすると、ユーザーの生活が困難になります(30分以上にわたって一部のフィールドを読み取って編集することを想像してください)。
楽観的ロックは、複数のユーザーがデータにアクセスできるようになるため、制限がはるかに少なくなります。ただし、複数のユーザーが同時にデータの編集を開始した場合、操作を実行できるのは1人だけです。残りの部分には、古いデータを操作して再試行する必要があることを示すエラーが表示されます。理想的ではありませんが、適切なUXがあれば、これは必ずしもそれほど苦痛ではないかもしれません。
架空のRailsRESTAPIに楽観的ロックを実装する方法を見てみましょう。
RESTAPIでのオプティミスティックロック
実際のRailsアプリでの実装に入る前に、一般的なRESTAPIのコンテキストで楽観的ロックがどのように見えるかを考えてみましょう。
上記のように、オブジェクトを読み取るときにオブジェクトの元の状態を追跡して、更新中のその後の状態と比較する必要があります。最後の読み取り以降に状態が変化しない場合、操作は許可されます。ただし、変更されている場合は失敗します。
REST APIのコンテキストで理解する必要があるのは、次のとおりです。
- 特定のリソースのデータを読み取る場合、オブジェクトの現在の状態をどのように表現し、それをコンシューマーへの応答で返すのですか?
- 更新を実行するときに、コンシューマーはリソースの元の状態をAPIにどのように伝達する必要がありますか?
- 状態が変更されて更新が不可能な場合、APIはコンシューマーに何を返す必要がありますか?
すばらしいニュースは、これらすべての質問にHTTPセマンティクスで回答および処理できることです。
リソースの状態を追跡する限り、Entity Tags
を利用できます。 (またはETag)。リソースのフィンガープリント/チェックサム/バージョン番号を専用のETag
で返すことができます 後でPATCHリクエストで送信するAPIコンシューマーへのヘッダー。 If-Match
を使用できます ヘッダー。APIサーバーがリソースが変更されたかどうかを確認するのが非常に簡単になります。これは、チェックサム/バージョン番号/ETagとして選択したその他のものを比較する場合にすぎません。
現在のETag
の場合、リクエストは成功します およびIf-Match
値は同じです。そうでない場合、APIはめったに使用されない412 Precondition Failed
で応答する必要があります ステータス、この目的に使用できる最も適切で表現力豊かなステータス。
もう1つの考えられるシナリオがあります。 APIコンシューマーがIf-Match
を提供する場合にのみ、ETagを比較できます ヘッダ。そうでない場合はどうなりますか?同時実行保護を無視して、楽観的ロックを忘れることはできますが、それは理想的ではない可能性があります。もう1つの解決策は、If-Match
を提供することを要件にすることです。 ヘッダーを作成し、428 Precondition Required
で応答します そうでない場合はステータス。
楽観的ロックがRESTAPIでどのように機能するかについての概要がわかったので、Railsに実装しましょう。
レールの楽観的ロック
すばらしいニュースは、Railsがすぐに利用できる楽観的なロックを提供することです。ActiveRecord::Locking::Optimistic
が提供する機能を使用できます。 。 lock_version
を追加するとき 列(またはロック列を定義するためにモデルレベルで追加の宣言が必要ですが、他に必要なもの)、ActiveRecordは変更のたびにそれをインクリメントし、現在割り当てられているバージョンが期待されるバージョンであるかどうかを確認します。古い場合は、ActiveRecord::StaleObjectError
更新/破棄の試行時に例外が発生します。
APIで楽観的ロックを処理する最も簡単な方法は、lock_version
の値を使用することです。 ETagとして。これを、架空のRentalsController
の最初のステップとして実行しましょう。 :
class RentalsController
after_action :assign_etag, only: [:show]
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
private
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
end
もちろん、これは非常に単純化されたバージョンのコントローラーです。これは、認証、承認、またはその他の概念ではなく、楽観的ロックに必要なものにのみ関心があるためです。これは、適切なETagをコンシューマーに公開するのに十分です。 If-Match
の面倒を見てみましょう 消費者が提供できるヘッダー:
class RentalsController
after_action :assign_etag, only: [:show, :update]
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
def update
@rental = Rental.find(params[:id])
@rental.update(rental_params)
respond_with @rental
end
private
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
def rental_params
params
.require(:rental)
.permit(:some, :permitted, :attributes).merge(lock_version: lock_version_from_if_match_header)
end
def lock_version_from_if_match_header
request.headers["If-Match"].to_i
end
end
そして、それは実際には、楽観的ロックの最小バージョンを機能させるのに十分です!ただし、明らかに、競合がある場合に500の応答を返したくはありません。 If-Match
を作成します 更新にも必要です:
class RentalsController
before_action :ensure_if_match_header_provided, only: [:update]
after_action :assign_etag, only: [:show, :update]
rescue_from ActiveRecord::StaleObjectError do
head 412
end
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
def update
@rental = Rental.find(params[:id])
@rental.update(rental_params)
respond_with @rental
end
private
def ensure_if_match_header_provided
request.headers["If-Match"].present? or head 428 and return
end
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
def rental_params
params
.require(:rental)
.permit(:some, :permitted, :attributes)
.merge(lock_version: lock_version_from_if_match_header)
end
def lock_version_from_if_match_header
request.headers["If-Match"].to_i
end
end
そして、これは、前に説明したすべての機能を実装するために必要なほとんどすべてです。応答コードだけでなく、いくつかの追加のエラーメッセージを提供するなど、さらに多くのことを改善できますが、それはこの記事の範囲外です。
まとめ:RailsAPIでの楽観的ロックの重要性
同時実行性の保護は、REST APIを設計するときに見落とされることが多く、深刻な結果につながる可能性があります。
それでも、Rails APIに楽観的ロックを実装することは、この記事で示すように非常に簡単であり、潜在的に重大な問題を回避するのに役立ちます。
コーディングを楽しんでください!
P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。
-
Railsのセキュリティの脅威:インジェクション
ユーザーデータを扱う場合は、それが安全であることを確認する必要があります。ただし、セキュリティを初めて使用する場合は、扱いにくく、退屈で、複雑に見える可能性があります。 この記事は、一般的なタイプのセキュリティの脆弱性と、それらがRailsの開発にどのように影響するかについて説明するシリーズの最初の記事です。この地形を通るマップとして、OWASPトップ10Webアプリケーションセキュリティリスクを使用します。 OWASPは、 Open Web ApplicationSecurityProjectの略です。 これは、Web上の重大なセキュリティ問題について世界を教育するために働く専門家のグル
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた