エンジニア的なネタを毎週書くブログ

東京でWebサービスの開発をしています 【英語版やってみました】http://taichiw-e.hatenablog.com/

更新系のAPIって難しい1 部分更新のはなし

勉強不足なのは承知のうえで、実務上困ってることとか考えていることを今日は書いてみたいと思います。

 

例えば以下の様なデータを取り扱うREST APIをつくろうとしたとします。

  • あるECサイトにおいて、商品の料金を取り扱うAPI
  • 商品は既にDBに登録されていて、料金のみこのAPIで扱う
  • 1商品あたり、料金は2種類ある。通常料金と、お得意様用料金。
  • 2種類の料金は、以下の組み合わせが許容される

    f:id:taichiw:20140705125422j:plain

初期投入の時は、こういうデータをPOSTすると思います。(Item IDはURIに入れるほうが正しいかも)

{ "itemID": 123,
  "rate":[
      {"rateType":"通常料金", "value":"1000"},
      {"rateType":"お得意様用料金", "value":"800"} 
  ]
}

通常料金のみも可能なので、こういうリクエストもありです。

{ "itemID": 123,
  "rate":[
      {"rateType":"通常料金", "value":"1000"}
  ]
}

お得意様料金のみはNGというルールがあるので、このようなリクエストは許容されず、エラーになります。

{ "itemID": 123,
  "rate":[
      {"rateType":"お得意様用料金", "value":"800"} 
  ]
}

新規登録はとてもシンプルですね。
一方、困るのが、更新の時のRequest設計です。

全部送ってこいやモデル

APIの実装者として一番簡単なのは、「部分更新は認めない」「全部送ってこい」です。
元のデータがこういう状態で

{ "itemID": 123,
  "rate":[
      {"rateType":"通常料金", "value":"1000"},
      {"rateType":"お得意様用料金", "value":"800"} 
  ]
}

こういうリクエストが来たら…

{ "itemID": 123,
  "rate":[
      {"rateType":"お得意様用料金", "value":"800"}
  ]
}

通常料金は、「設定なし」に変更される という考え方です。

ですので、既存のデータがどうであれ、このリクエストはNGです。
なぜならこのリクエストを実行すると、お得意様用料金のみ存在し、通常料金が存在しない という、ルール上許容されていない状態になるからです

このような、全更新モデルですと、APIの実装はシンプルになります。
予想される更新結果が全てリクエストに含まれているので、DB上の既存の値がどうあれ、このリクエストに対してルールのチェックをすればいいからです。

一方で、APIの使い手であるクライアントのアプリケーションは不便を強いられます。
くアライアントの実装者は、
「通常料金だけ更新する画面」のを作りたいかもしれません。
そのような場合、クライアントは一旦Get系のAPIで通常料金/お得意様料金双方を取得した上で、通常料金だけ書き換えて、通常料金とお得意様料金双方をAPI送る必要があります。

部分更新モデル

だったら、rateオブジェクトは送られてきたものだけ更新する
というルールにしたらうまく行きそうな気がします。

こういうリクエストが来たら、

{ "itemID": 123,
  "rate":[
      {"rateType":"通常料金", "value":"1000"}
  ]
}

 お得意様用料金は何もせずに、通常料金だけとにかく1000に更新する。
これなら、クライアントは使い勝手がいいです。

ではこのようなリクエストが来た場合どうするでしょう?

{ "itemID": 123,
  "rate":[
      {"rateType":"お得意様用料金", "value":"800"}
  ]
}

 この場合少し複雑で、まず、DB上の現在のデータを調べ、通常料金が存在するかしないかを確認します。
通常料金が存在していれば、問題ありません。
一方、通常料金が存在していない場合(つまり、もともとどっちの料金も入っていなかったケース)、このままではお得意様用料金のみ登録されてしまうので、エラーにしないといけません。

同じような話ですがもう一例。

{ "itemID": 123,
  "rate":[
      {"rateType":"通常料金", "value":null}
  ]
}

 あまり正しくないかもしれませんが、valueにnullがあった場合、その料金を空にするというルールにしたとします。
このようなリクエストが来た場合、やはり既存のデータによって、このリクエストが成功するか失敗するか決まります。

  • 既存のデータが通常料金のみの場合 → 成功。料金が何も登録されていない状態になる
  • 既存のデータが通常料金・お得意様用料金双方ある場合 → エラー。通常料金のみ消すのはルール違反なので、更新されない。

つまりいいたいのは

このように、APIで取り扱う対象の中にルールが存在した場合に、

  • 全部送ってこいやモデル
    →リクエストに対してチェックを行えばいいのでシンプルな実装になる。
     反面、クライアントは使いづらい。
    (更にクライアントとサーバ間で無駄な通信が発生することになる)
  • 部分更新モデル
    →クライアントの実装はシンプル。
     反面、APIの実装は複雑になり、バグの温床にもつながる
    (今まで触れていませんでしたが、実際に、この手の複雑さが引き起こしているバグを見つけたことがあります)

という問題がありまして、
私が、更新系のAPIって難しいなぁと思っている理由の一つになっています。

…といいつつ、実は自分の中では、これがベターかな?という
解決方法が見えているので、次回はそれについて触れたいと思います。

 

※古く、かつ実装面に関しては言及されていませんが、IFがどのようにあるべきかについて議論された記事を見つけました。
http://www.infoq.com/jp/news/2010/11/rest-partial-update