Humanity

Edit the world by your favorite way

やはり俺の AUTO INCREMENT を含むテーブルに対するリソースに PUT を使うのはまちがっている、または HTTP PUT の冪等性と AUTO INCREMENT の相性が最悪な件について (あるいは私が PUT を諦め POST を使うまで)

3行で伝える代わりにタイトルで説明してみた (親切心)。

(2016/07/30 2:45 追記) ちなみにコメント欄で指摘頂いた通り、今回問題視した PUT の設計も冪等と言えます。 私の冪等性の理解が間違っていたので、以下の冒頭の項目で説明する PUT の設計が RESTful じゃないと記載しているのは誤りです。

悩み

DB の AUTO INCREMENT な値 (以下「連番」) をメッセージボディの JSON に含め、 そのリクエストを受け付けたバックエンドが DB の連番と比較し、 違っていればエラーを返すという設計の API があるとして、 PUT であるリソースを更新すると当然連番がインクリメントされるので連続で送ることができない。 つまり冪等性を保っておらず RESTful ではない。 では RESTful に実現しようとするなら一体どうすればいいんだろう?というので最近悩んでいた。

多分 REST を諦めるか設計か仕様を変えない限りどうしようもないと思うので、 「連番を渡して違ったらエラーにする」という設計で「本当は何がしたいのか?」から考えてみる。

何がしたいの?

「何がしたいの?」はぶっちゃけ単純で 「(フロントエンド等の) クライアントで持っているリソースが古くなっていた (=別の誰かがすでに更新していた) らエラーにしたい」 というもの。 つまり仕様としてリソースのインスタンスが1つであることを強制している (=1つのリソースを複数のユーザが更新する可能性がある)。 ではこれを実現するために REST の原則を崩さずに PUT してリソースに書き込む*1 ためにはどうすればいいか?

RESTful な PUT

まず DB の AUTO INCREMENT の値をクライアントから受け付ける設計を考えたけど、これは間違っている気がする。 というのは、受け付けたデータの連番はそのままリソースのデータとして書き込まれるのではなく、インクリメントされてから実際の DB に書き込まれる。 なのでクライアントから見れば手元のリソースのデータ + 副作用が加えられて書き込まれることになる (手元のリソースのデータと差異が生じる)。

連番なり更新日などは DB が管理するためのメタデータなので、 それをクライアントに送信させることを強制する設計がおかしい。 ではこの「管理」とは何で、何のためのものなのか?というと、 冒頭で挙げた以下の仕様を満たすためです。

「(フロントエンド等の) クライアントで持っているリソースが古くなっていた(=別の誰かがすでに更新していた)らエラーにしたい」

では WebSocket で常に最新版をプッシュすれば皆がハッピーなのか?というと リソースの項目を編集中に「リソースが更新されました」っつってページがリロードされたら確実にキレる。 ではどうすればいいのか。

答え:POST にする

ズコー (AA略)

これでいいのか?とは思うけど、自分としてはこれぐらいしか思いつかない… けど URL 設計を考えている内にこれでいいんでは?という気になってきた。

例えば以下のようなエンドポイントがあるとする。

/tweets/{ID}

すると

POST /tweets/1234?seq=1

ID=1234 のリソースを連番 1 で登録して、(連番が同じで) 成功すれば /tweets/1234/1 というリソースが作られる。 もちろんこの URL の末尾の 1 は連番。 (ちなみに連番をメッセージボディで渡すとブログで書く都合上面倒なので、URL のクエリパラメータで連番を渡すことにしている)

つまりアイデアとしては 「いっそ連番はリソースを示す ID と同じようなものと考えて URL に含めてしまう」 というもの。 ただ仕様による制限故、普通のリソース(?)とはいくつか違う点がある。

  1. /tweets/1234/1 への PUT はない (POST で登録する)
  2. 最新版は /tweets/1234/latest で取得
    • これは別に/tweets/1234 で最新版が取得できてもいいかもしれない
  3. 最新版の連番が 2 の時、GET /tweets/1234/1 は 404*2 を返す

つまり API で言うと以下の3つ (実質2つ) のみ提供する。

  1. POST /tweets/{ID}
  2. GET /tweets/{ID}/latest or GET /tweets/{ID}
  3. GET /tweets/{ID}/{seq}
    • これを使う意義は無いような…

(追記) 答え その2:ETag を使う

KoRoN さんから教えてもらった ETag を使うと条件付きで更新することができます。 こちらの方が正道っぽいですね。

Azure API は ETag 使ってるみたいです (kamichidu さんありがとうございます)。

ETag での条件付き更新が失敗になった場合のエラーコードは 409 Conflict が適切だそうです。

あと 412 Precondition Failed というのもあるようです。

以上

こんな感じでどうでしょうか? とりあえず少なくとも REST の砦は守れたのでよかった (小並)。

冒頭に挙げたような複数人が同じリソースを更新するケースはよくありそうなので、 ぜひ自分ならこうするよ(してるよ)って方いたら反応くれるとうれしいです。


という結論を出した後で同じ結論に至った人を発見。

I'd suggest that you use POST, not PUT, for an auto-increment key, or do not use the auto-increment key in the resource ID.

http://stackoverflow.com/questions/9881085/rest-how-would-a-put-request-handle-auto-incremented-resource-identifiers

こちらも参考リンク。

http - PUT vs POST in REST - Stack Overflow

*1:ここでは PUT としていますが仕様によっては DELETE も該当しますね

*2:もっと他に良いエラーコードあるかな?