t0mmy

2020年12月30日に参加

学習履歴詳細

APIデザインパターン 8章 部分的な更新と取り出し 読了

やったこと

  • APIデザインパターン 8章 部分的な更新と取り出し 読了

学んだこと

ポイント

  • フィールドマスクとは
    • 何か
    • どういった問題を解決するか
    • どこに設定すると良いか
  • フィールドマスクのデフォルト値には何が適しているか
  • 「特定のプロパティを削除する」といった部分更新処理は、どう実装すると良いか
  • 無効なフィールドを指定されたときは、どう振る舞うべきか

学び

  • 部分取得・更新という考え方
  • 部分取得・更新を実装する、「フィールドマスク」の考え方
  • コレクションの、特定の要素だけ更新する、といった処理は提供するべきではない
  • 部分取得・更新は、SQLレベルの細やかな制御には不向き
    • この場合は、GraphQLの方が適任

メモ

対象とする問題

  • リソースの一部分だけを取得したい
  • リソースの一部分だけを更新したい
    • 「全リソースを更新する」という手段しかない場合、以下の問題が発生する
      • 更新処理の衝突が発生する
      • クライアントが古いバージョンのAPIクライアントを使用した場合、データの喪失が発生する

部分取得・更新を可能にする「フィールドマスク」

取得したい、特定のフィールド名を集めたリスト。
以下はイメージ。

fieldlMask:["title"]

APIは、フィールドマスクに設定したフィールドのみクライアントに返却する。
これは、更新処理にも適用できる。
=> フィールドマスクに設定したフィールドのみ更新する

JSONのような動的データ構造の場合、 "fieldMask"フィールドが存在しなくても、JSONのプロパティから取得(または更新)したいフィールドに当たりをつけることができる。

実装

部分取得を実装する

GETリクエストでフィールドマスクを送信することになる。

問題点

GETリクエストは、body を持つことが出来ない。
そのため、フィールドマスク値の送信を工夫する必要がある。

どう実装するか

クエリパラメータを使用して、fieldMaskを渡す。

?fieldMask=X&fieldMask=Y&...

クエリパラメータは、サーバサイドフレームワークによって実装が微妙に異なるため、要確認。

部分更新を実装する

PATCH リクエストでフィールドマスクを送信することになる。

問題点

PATCH リクエストは、通常、更新したいリソースのみ送信することを想定する。

  • 言い換えると、リソースとは無関係な、部分更新を行うためだけに必要な設定データ(≒fieldMask プロパティ)をbodyに含めてはならない
    • リソース指向の設計思想に反する
  • 技術的には可能だが、 標準のupdateメソッドの前提を破っており、将来的に矛盾を引き起こしかねない
どう実装するか

部分取得と同じく、クエリパラメータを使用する。

ネスト構造に対する部分取得/更新

JSONの中にJSONがあるような状況。

ネスト構造では、以下のルールに従うと良い。

  1. フィールドの指定には、区切り文字としてドットを使用する
  2. ネストされたメッセージのすべてのフィールドは、アスタリスクを使用して参照できる
  3. マップのキーは常に文字列
  4. キーに含まれる特殊文字をエスケープする場合は、バッククォートを使用
  5. バッククォートをエスケープする場合は、バッククォートを使用

◇サンプルリソース

{
"id":1,
"title":"test"
"settings": {
"language":"ja"
"volume":20
}
}

◇サンプルリソースのフィールドマスク

* // 全フィールド取得
"id"
"title"
"settings.*"
"settings.language"
"settings.volume"

クエリパラメータに設定する例

?fieldMask=*
?fieldMask=title&fieldMask=settings.*

コレクションフィールド

フィールドマスクでは、コレクションフィールドも扱うことが出来る。
ただし、コレクションのインデックスを指定するようなフィールドマスクはアンチパターン。

  • コレクションは順序が保障されない
    • データの増加によって、インデックスが指し示すリソースが変化する
  • インデックスを指し示すリソース更新は困難

コレクションの特定の要素だけ更新する、といった処理は困難なため、コレクション全体を置き換えることで更新する。

  • これでも、オブジェクト全体を置き換えるよりかはいくらかマシ

デフォルト値

部分取得において、フィールドマスクが設定されていない場合。

  • ほとんどの場合、全フィールド取得を期待する
  • 全フィールド取得が悪手の場合、デフォルトで除外する、といった戦略は有り
    • この場合、デフォルトで除外するフィールドの名前、除外理由、代替の取得方法などをドキュメントに記載するべき
    • 除外も含めた全フィールド取得方法の代表は、「すべて」を表すフィールドマスク「 *」 で要求すること

部分更新で全フィールド更新は、replace と役割が被るため、あまり意味がない。
利用状況から、よく更新するパターンを見極め、デフォルト値として設定することになる。

暗黙のフィールドマスク

フィールドマスクを設定しない(≒全フィールド更新)は、replace と役割が被るため、あまり意味がない。
効率を上げるためには、何かしらの句数が必要。

一般には、 PATCH の body は、リソースの更新したいプロパティのみを含む。
このことから、body に含まれる(≒更新したい)フィールドから、フィールドマスクを推論すると良い。

この時、プロパティが存在しない(= undefined)と、プロパティの値が null は、区別すること。

  • undefined はフィールドマスクから除外する
  • nullは、「データを null へ更新する」と解釈する場合が多い
  • いずれにせよ、APIの設計に従うこと

動的なデータ構造の更新

PATCH処理にて、動的なデータ構造(JSONなど)のプロパティを削除したい。
どうしたらよいか?

  • PATCHのBody に含めない ... は、更新対象から除外することを意味するため利用できない

書籍では、以下の選択肢を提供している。

  • 削除したいプロパティを、フィールドマスクで宣言する
  • PATCHのBody に、空のオブジェクト({})を設定する

無効なフィールド

無効なプロパティを指定されたら、そのプロパティの値を undefinedとして扱おう。
言い換えると、無効なプロパティを指定された時、むやみにエラーを投げるのは良くない選択肢。

  • プロパティの追加/削除はよくあること
    • APIのアップデートなど
  • 利用者が、古いAPIクライアントを使用して、現在提供していないプロパティを指定する、といったことは十分考えられる
    • このとき、エラーを投げると、利用者に驚きと混乱を与えることになる

トレードオフ

フィールドマスクは、部分取得・更新を実装する強力な手段。
しかし、SQLレベルの細やかな制御は不向き。

  • 技術的には可能かもしれないが、そうするべきではない

この場合はGraphQLの方が適している。

部分取得・更新は、必ず実装する必要はない

扱うリソースが小さければ、全プロパティ置き換えでも問題はない。
言い換えると、全プロパティ置き換えが、無視できないほどパフォーマンスを悪化させる場合は、部分取得・更新を検討することになる。

そして、一貫性を保つため、リソース一つでも部分取得・更新を実装するのであれば、全リソースに部分取得・更新を実装すること。

パフォーマンスと、部分取得・更新開発コストのトレードオフ。

更新処理にPATCH以外を選択する

JSON Patch 、JSON Merge Patchなど。


取り扱うこと

  • なぜリソースの特定の部分だけ取得/更新したいか
  • 対象となるフィールドをAPIサービスに伝える最善の方法
  • 複雑なフィールド内のある特定の要素を指定可能にするかどうか
  • デフォルト値の定義と暗黙のフィールドマスクの扱い
  • 無効なフィールドの指定に対処する方法

まとめ

  • 部分取得は、以下のような状況では特に重要
    • 扱うリソースが大きい
    • リソースを扱う側(≒クライアント)のハードウェアスペックが限られている
  • 部分更新は、衝突を気にせずにきめ細かな更新を行うのに有効
  • フィールドマスクとは
    • フィールド、ネストしたフィールド、マップキーを指定する方法をサポートし、検索または更新されるべきフィールドを示すために使用されるべきもの
    • 配列フィールドに対して、特定の要素を指定する機能を提供すべきではない
    • デフォルトでは、部分検索はフィールドマスクの全値を仮定する
      • 部分更新では暗黙的なものを仮定する
  • フィールドが無効な場合、それらは存在するが値が undefinedのように扱う
WebAPI

2023年06月09日(金)

1.5時間