偏見メモ Webサービスの設計〜書き込みも可能編〜
前回投稿した記事の続きで第5部、16章について偏見混じりでまとめていきます。
参考書籍は以下
書き込み可能なWebサービスの難しさ
ブログやSNS、書き込み可能なWebサービスがほとんどである。
ユーザからの書き込み処理がある場合は考えなければいけないことはいろいろある。
複数のユーザが同時に書き込んだらどうなる?🤔
バックアップしたデータをリストアするときのように同時に複数のリソースを更新するためにはどうすれば良い?🤔 とか
1️⃣リソースの作成
題材は前回の記事と同様、郵便番号検索サービスです。
新しい郵便番号を追加できるようにする事例が取り上げられています。
方法としては
1️⃣ ファクトリリソースへPOSTする
2️⃣ PUTで直接作成する
ファクトリリソースによる作成
ファクトリリソース:リソースを作成するための特別なリソース
ファクトリリソース自身はあらかじめWebサービスに用意しておき、POSTでリソースを作成する。
参考書籍ではトップレベルリソースをファクトリリソースとしており、トップレベルリソースに新しいリソースをPOSTすることで作成している。
PUTによる作成
PUTの場合は、作成する郵便番号をクライアントが知っているのでURIが決まっている。
よって作成したいリソースのURIにリクエストを直接送れば良い。
POSTを使うのが無難だが今回はPUTを使うのにも旨みがある。
メリデメは以下。
メリット😄
・POSTをサポートしなくて良くなるので、サーバ実装が簡単になる。
・クライアントが作成と更新を区別しなくて良くなるので、クライアント側の実装が簡単になる
デメリット😢
・クライアントがURI構造を把握しておく必要がある
・操作が作成か更新かが区別できない
2️⃣ リソースの更新
基本的にPUTで行い、バッチ処理はPOSTで行う
バルクアップデート
更新したいリソース全体をメッセージボディに入れる。
更新したい箇所は更新する内容で、それ以外は現状のデータをそのまま送るということ。
クライアントの実装が楽になるが、送受信のデータ量が大きくなる。
パーシャルアップデート
リソースの更新したい部分だけをサーバに送信する。
バルクアップデートはネットワーク帯域をたくさん使ってしまうため、非同期通信でユーザからの入力を都度サーバにデータを送信するには向いていない。
そんなときに採用する方法、らしい。
送受信するデータが少なくなるが、GETしたリソースの一部を修正してPUTする、みたいな使い方ができなくなる。
パーシャルアップデートを採用するならバルクアップデートもサポートするようにするのが望ましい。
更新できないプロパティを更新しようとした場合
郵便番号は一度決まると住所は変わることはあっても自身が変わることはない。
なので、郵便番号を更新しようとした場合は400 Bad Requestを返して更新できないことをクライアントに伝えるようにするのがベター。
作成日時や更新日時などをサーバで自動更新するプロパティの場合は、クライアントがリクエストしてきても無視して200 OKを返すのがベター。
3️⃣ リソースの削除
削除したいリソースのURIにDELETEを送る。
作成や更新に比べて単純。
気をつけるべきなのは、削除するリソースに子のリソースが存在する場合。
ex.) 「東京都」の子リソースには「東京都文京区」が存在する。
親の東京都が消された場合は、子の東京都文京区も削除するのが一般的。
4️⃣ バッチ処理
大量の郵便番号を作成・更新したい場合は都度サーバと接続するのはパフォーマンスが悪くなるので、一括で送信できる(バッチ処理)できるようにWebサービスを実装する必要がある。
参考書籍では、
リクエスト時はリソースをJSONの配列にして、それをPOSTして送信するようにしている。
PUTを用いていないのは、送信時にURIを指定しなければならず、複数の場合だとそれが実現できないから。
POSTが無難というのはこういうことだろうか?🤔
レスポンスを受け付けたサーバは、JSON配列を分解してそれぞれのリソースの更新を行う。
この時に問題になるのがバッチ処理中に何かのエラーが発生した場合だ。
一部の処理が失敗するケースが考えられる。
その対処方法としては二つある。
・バッチ処理をトランザクション化して、途中で失敗した場合は何も処理しないことをWebサービス内部で保証する(次節でまとめ)
・どのリソースが成功して失敗したのかをクライアントに伝える
・207 Multi-StatusとWebDAVの<D:multistatus>を組み合わせる
・200 OKと独自フォーマットを組み合わせる
シンプルな処理のエラーの対応であれば後者の200 OKと独自フォーマットでJSONを返すのが良さそう。
5️⃣ トランザクション
複数のリソースに跨った変更をひとまとまりに扱う = トランザクション
ex.) 銀行口座で5万円振り込む場合、振込先の口座は+5万円して、振り込み元は-5万円する。どちらか失敗すると損得する人が発生するので、失敗したらどちらも処理前の状態に戻す必要がある。
参考書籍では郵便番号を一括に削除するケースを例としています。
トランザクションをRESTfulに実現する際はトランザクションリソースを用いる。
このリソースを以下のようにして利用する
http://hogehoge.com/transactions
トランザクションの開始
ファクトリリソースにPOSTしてトランザクションを作成する。
成功するとレスポンスのLocationヘッダに生成したリソースのURIが返ってくる。
トランザクションリソースへの処理対象の追加
生成したURIに、削除したいリソースのURIを追加していく。
追加するにはPOSTのボディやクエリパラメータに指定したり、PUTで直接指定する方法がある。
トランザクションの実行
トランザクションリソースに処理を実行したい旨を伝える。
200 OKが返ってこればトランザクションは成功で、4xxか5xxが返ってこれば失敗。
トランザクションの削除
全てが成功したらトランザクションリソースをDELETEで削除する。
これを削除する時は関連した他のリソースも同時に削除するようにしておく。
6️⃣ 排他制御
複数のクライアントが同時に1つのリソースを編集してコンフリクトが起きないように、1つのクライアントのみが編集できるようにする処理。
解決方法は悲観的ロック、楽観的ロックの2つがある。
悲観的ロック
ユーザをあまり信用せずに競合を発生させないように制御する方法。
WebDAVのLOCK/UNLOCKメソッドを使うか、独自のロックリソースを使って実現する。
ロックのリクエストにはLOCKを使う。
ロックが成功すると200 OKと<D:prop>要素が返ってくる。
LOCKの結果として、どのようなロックがリソースに対して行われたのかを示す情報が含まれる。
ex.) ロックの情報、ロックの所有者、ロックトークン等
ロック済みのリソースを編集するにはIfヘッダーでロックトークンを指定する必要があり、これを指定しないと423 Lockedが返ってくる。
正しくトークンを指定するとリソースを更新できる。
更新が終了すればロックを解除する。
この時はUNLOCKを使う。
独自でロックリソースを作る場合は、リクエスト時にはロックしたいリソースに向けてPOSTで必要なロック情報を送るようにする。
ロックした結果、ロック対象の子リソースとしてロックリソースができる。
このリソースが存在する間は対象のリソースがロックしていることになり、ロックしたユーザだけが編集できるようになる。
他のユーザがロックをかけようとした時は、レスポンスとして423 Lockedを返すか400 Bad Requestを返すようにすると良い。(423の方が具体的なのでオススメされているのは前者)
楽観的ロック
悲観的ロックは不特定多数のユーザが使っている場合、ロックされ続けたまま編集ができないことが起こり得る。
そこで、コンフリクトが起きた場合にのみ対処するようにしようというのが楽観的ロックである。
HTTPで楽観的ロックを実装するには条件付きPUTと条件付きDELETEを利用する。
クライアントが更新リクエストを発行する際に、自分が更新しようとしているリソースに変更がないかを確認する必要があるので、条件付きPUTを用いてこれを解決する。
Last-ModifiedかETagの値を使う。
Last-Modifiedを使う場合は、If-Unmodified-Since(もし変更されていなければ)を使う。
ETagであれば、If-Match(マッチすれば)を使う。
条件付きDELETEの場合も基本的には条件付きPUTと同様。
条件付きDELETEを利用すると、他人が修正したのを知らずにリソースを削除するミスを防止できる。
これらメソッドを送った時に、リソースが変更されていると412 Precondition Failedを返す。
412を返した場合の対処としては以下の3つ。
・コンフリクトを起こしたユーザに確認をした上で更新or削除する
・コンフリクトを起こしたデータを競合リソースとして別リソースに保存する(PUTの場合のみ)
・コンフリクトを起こしたユーザに変更点を伝え、マージを促す(PUTの場合のみ)
*
*
*
書き込み可能 = 更新処理の設計について学びました。
実際多くのWebサービスはなんらかの更新や追加のアクションをすることが多いので、ちゃんとここで学んだことをインプットしておきたい。
正直インプットしただけではまだわからないことが多いので、まずは概念やこういうのがあるんだな〜みたいなのを把握しておくということを念頭にいれ、後ほど控えているアウトプットに活かしていきます💪