Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

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で応答します そうでない場合はステータス。

楽観的ロックがRE​​STAPIでどのように機能するかについての概要がわかったので、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つも見逃さないでください。


  1. Railsのセキュリティの脅威:インジェクション

    ユーザーデータを扱う場合は、それが安全であることを確認する必要があります。ただし、セキュリティを初めて使用する場合は、扱いにくく、退屈で、複雑に見える可能性があります。 この記事は、一般的なタイプのセキュリティの脆弱性と、それらがRailsの開発にどのように影響するかについて説明するシリーズの最初の記事です。この地形を通るマップとして、OWASPトップ10Webアプリケーションセキュリティリスクを使用します。 OWASPは、 Open Web ApplicationSecurityProjectの略です。 これは、Web上の重大なセキュリティ問題について世界を教育するために働く専門家のグル

  2. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた