REST におけるトランザクションについて (Re: Web を支える技術)
といいつつ、ひとつだけ理解できないというか、納得できないところが。トランザクションのところがなんだかRESTっぽくないのがすごく気になる
Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESSプラスシリーズ)(山本 陽平) - ただのにっき(2010-04-23)
「Web を支える技術」は自分もとてもいい本だと思う (教科書としてすばらしいし復習用としても読みやすいのでイイ) のですが、トランザクションの所だけは分かりづらいなと感じました。その原因は、atomic transaction で解決できる課題を例として使っているという点と、トランザクションと更新クエリのレイヤ分割がされていない、という2つの点によるものではないでしょうか。
HTTP 上でトランザクションを表現する必要があるケースのほとんどは、atomic transaction ではなく、2相コミットもしくは long-running transaction を行う場合だと思います。
たとえば、HTTP 上で2層コミットを行っているシステムの代表例としては、おさいふケータイのチャージ機能が挙げられます (あれって HTTPS ですよね?)。また、たとえば、数量に限定がある商品を課金代行サービスを使って販売する場合等においては、long-running transaction が必須になります。
なので、トランザクションレイヤを RESTful に表現する手法は理解しておいて損がないものだと思います。具体的には、
- トランザクションの開始は POST を使ったトランザクションリソースの生成
- トランザクション開始後は POST ではなく PUT を使い、クライアントがリクエスト ID を指定することで、ネットワーク障害への耐性を確保
- トランザクションのコミットも再送可能じゃないと困るので PUT
- コミットの返り値を確認してからトランザクションリソースを DELETE
といったあたりが基本になると思います。
以下、具体例として、「アライブドアチケット」というチケット販売サイトから、ミュージカル「夜のhidek様」のチケット (2010年5月24日の公演) を購入する例を示します。支払いには Yappo! ペイメントを利用します。
まずは、残席があるか確認します。
GET http://ticket.alivedoor.com/musical/20100524/夜のhidek様/seats-left HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"seats-left": 11
}
「夜のhidek様」は、歌舞伎町でも大人気な、あの往年のアイドルが主演する人気ミュージカル。残席が少ないです。今すぐチケットを買わないといけません。トランザクションリソースを生成して、トランザクションを開始します。
POST https://ticket.alivedoor.com/buy HTTP/1.1
Content-Length: 0
HTTP/1.1 201 Created
Location: https://ticket.alivedoor.com/buy/934085
...
次に、座席を確保します。サーバは席が確保できれば「201 Created」を、既に満席になっていれば「409 Conflict」を返します。
PUT https://ticket.alivedoor.com/buy/934085/1 HTTP/1.1
Content-Type: application/json
{
"url" : "http://ticket.alivedoor.com/musical/20100524/夜のhidek様",
"seats" : 2
}
HTTP/1.1 201 Created
...
座席が確保できたら、支払い金額を確認します。
GET https://ticket.alivedoor.com/buy/934085 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"amount": "10000",
"currency": "JPY"
}
次に、クライアントは Yappo! ペイメントを使って、アライブドアチケットの口座に指定された金額を振り込みます。振込が完了すると、Yappo! ペイメントは振込IDを返すので、その振込IDを指定してトランザクションをコミットします。
PUT https://ticket.alivedoor.com/buy/934085/commit HTTP/1.1
Conetnt-Type: application/json
{
"payment_id" : "https://payment.yappo.jp/paymentid/02394895923408904930"
}
HTTP/1.1 200 OK
...
「200 OK」が返ってきたので、支払が完了し、取引が成立したことがわかります (失敗した場合は「409 Conflict」が返ってくるでしょう)。最後にトランザクションリソースを削除します。
DELETE https://ticket.alivedoor.com/buy/934085 HTTP/1.1
HTTP/1.1 200 OK
...
以上のような流れになると思います。「夜のhidek様」言いたかっただけ (ry
処理の対象となるリソース (上の例では http://ticket.alivedoor.com/musical/20100524/夜のhidek様) が、トランザクション中の Request-URI に含まれていないのが気持ち悪い、という意見もあるかもしれません。ですが自分は、トランザクションリソースを使う場合は、HTTP はトランザクション処理に専念、HTTP 上のペイロードで処理対象のリソースを表現する方が、レイヤが分かれるので良いと考えます。
事例としてとてもわかりやすく、Webベースでのトランザクションの必要性が認識できました。
ただ、(主に教条的な面で)気になる点が2つ。
座席の確保時にPUTが使うリソースは、(好ましくないとされている)クライアント側で生成したURIです。しかもこれ自身はリソースとしての意味を持っていない。確保できなければ終了なのだから、ここはPOSTで良いのではないかとも思えます。
同様に支払い時のURI もクライアントが生成している上に、このURIが示すもの(commitという行為? それって動詞っぽくない?)は「リソース」と呼ぶには苦しいように思えます。
書籍の例も含めて、トランザクション処理は「REST上で実現できる」ということは示せていますが、RESTっぽさがずいぶん失われてしまっているように見えるのが、やはりどうしても気になってしまいます。
Posted by: ただただし | April 25, 2010 at 08:25 AM
> 座席の確保時にPUTが使うリソースは、(好ましくないとされている)クライアント側で生成したURIです。しかもこれ自身はリソースとしての意味を持っていない。確保できなければ終了なのだから、ここはPOSTで良いのではないかとも思えます。
一般に、リソース作成時に PUT を使うべきでないとされる理由は、クライアント間で名前の競合が発生する可能性があるからだと思います。トランザクションを組む場合にはクライアントは1台だけですから、PUT を避ける必要はないと考えます。
クライアント側で URI (というより "/1" のような論理シーケンス番号 (LSN)) を生成しないと、座席を確保する HTTP リクエストがタイムアウトした場合等、異常系の処理が面倒になります。PUT ならば同一の LSN を指定することで、座席の確保が2回発生しないようにできますが、POST だと、より冗長な別の方法で実装する必要があります。
LSN を使って再送制御を行うというパターンは TCP のシーケンス番号等にも見られる簡便で効率の良い手法です。LSN を実装するのであれば、POST の上に屋上屋を重ねるよりも、べき等性を備えた更新メソッドである PUT を使うべきというのが私の考えです。
> 同様に支払い時のURI もクライアントが生成している上に、このURIが示すもの(commitという行為? それって動詞っぽくない?)は「リソース」と呼ぶには苦しいように思えます。
なるほどです。であれば、commit はコレクション URI (https://ticket.alivedoor.com/buy/934085) に対する PUT を使うか、あるいは座席確保と同様に LSN ("/2") を指定し、その PUT コンテンツの中で commit を指示する、という形でもよいかと思います。
> 書籍の例も含めて、トランザクション処理は「REST上で実現できる」ということは示せていますが、RESTっぽさがずいぶん失われてしまっているように見えるのが、やはりどうしても気になってしまいます。
そうですね。トランザクションレイヤはリソース制御の下にないとダメだから、トランザクション制御に HTTP の機能を使うことになりますしね。最終的な捜査対象であるリソースの制御が HTTP レベルに現れない、という意味では REST っぽくないですね。
自分には、トランザクション内の各メッセージがリソースに見えるので気になりませんが (^^;
Posted by: kazuho | April 25, 2010 at 11:04 AM
実装上の理由からLSNへのPUTを選択するというのは、わかります。
私がこのサンプルに違和感をおぼえるのは、URIが存在するということ、すなわち、それをGETすると意味のある「何か」が得られるという点が徹底されていないからじゃないかと気づきました。
例えば/934085/1をGETすれば(おそらく)シートを2つ仮押さえした、という情報が得られるはずで、それならOKだと思います。トランザクションリソースの子要素として妥当な情報です。上のjsonには「仮押さえ」という情報が含まれていませんが。
一方/934085/commitはGETしても何が得られるのかよくわからない(commitが動詞だから)。それ以前に/934085をGETしたら支払い金額が返ってくるというのも何か違う気がします(まだGET /934085/1すると金額が追加されている方が納得できる)。
トランザクションリソースの操作には、実装が頭の中に先にあって、それをURIに当てはめているような無理やり感を感じます。先にリソースを考えて、そのあとにリンクをつなげて実装するという本来の「リソース設計」がされてないからかなぁ……と思うんですが(今は思ってるだけ)。
Posted by: ただただし | April 25, 2010 at 03:18 PM
横から失礼します。
エントリの例ですと、buyリソースにPOSTしてトランザクションを開始していますが、これはあまり良くないのではと思います。エントリにある通り、トランザクションはリソースですから、/transactionsにPOSTしてIDを受け取り、/transactions/${ID}にPUTして内容を入れ、最後に/transactions/${ID}にPOSTすることで実行、のような設計になるのではと思います。GETすれば途中のトランザクションの情報が返ることが期待されますし、DELETEすればキャンセルできると理解できます。基本的には、buyやcommitといった動詞がURIに含まれている時には、注意が必要だと思っています。
1年以上前ですが、近い議論がこちらでされているのでご参考までに。
http://community.jboss.org/wiki/TransactionalsupportforJAXRSbasedapplications
Posted by: rika_t | April 25, 2010 at 04:12 PM
ただただしさん:
> 例えば/934085/1をGETすれば(おそらく)シートを2つ仮押さえした、という情報が得られるはずで、それならOKだと思います。トランザクションリソースの子要素として妥当な情報です。
同意です。そう実装すべきだと思います。(ご指摘の)金額のほか、仮押さえの有効期限なんかも入っているかもしれませんね (commit が完了していれば、有効期限は消える)。
> 上のjsonには「仮押さえ」という情報が含まれていませんが。
これは、意図的にそうしています。そもそも "/buy" という名前空間に紐づいた処理なので、行う作業は1件以上の仮押さえと決済処理しかないからです。
"/transactions/" のように汎用的なトランザクションレイヤを用意して、その中に登録する更新命令として「仮押さえ」を作成するというアプローチもありますが、そのようなアプローチは筋が悪いと考えるので紹介しませんでした。
「なんでもまとめてコミットできるウェブサービス」なんて作りませんよね? 購入処理とキャンセル処理は同一のトランザクションに入らない、という設計が自然だし、だったら "/buy" や "/cancel" といったトランザクションが必要な単位で URI 設計を行うべきだと考えるからです (そうすることで「仮押さえ」を JSON 内で指定するといった冗長性を排除できる)。
> 一方/934085/commitはGETしても何が得られるのかよくわからない(commitが動詞だから)。
commit に成功していれば、決済に使われた振込ID が返ってくるのでいいのではないかと思います。
> それ以前に/934085をGETしたら支払い金額が返ってくるというのも何か違う気がします。
/943085 をコレクションリソースだととらえれば、GET したら子要素の合計金額が返ってくるのは自然だと思います。
リソース設計を説明するということは特に考えていなかったのですが、
/buy -- コレクション (トランザクション群を管理)
/buy/ -- コレクション (各購入処理に関するリソースを集約)
/buy//\d+ -- 各購入項目
/buy//commit -- 決済情報
である、とすればいいでしょうか。
rika_t さん:
> /transactionsにPOSTしてIDを受け取り、/transactions/${ID}にPUTして内容を入れ、最後に /transactions/${ID}にPOSTすることで実行、のような設計になるのではと思います。
この手法だと、/transactions/${ID} に内容を入れるたびにリソースのステートは変化するのでしょう (つまりべき等性がない) から、POST を使うべきだと思います。そして、べき等性がない、ということはトランザクションを行っている途中で通信障害が発生した場合のリカバリ処理に HTTP のもっている特性を使えない、ということになります。それでも問題ない単純なケースでは、おっしゃっている手法でもいいかと思います。
> 基本的には、buyやcommitといった動詞がURIに含まれている時には、注意が必要だと思っています。
自分がうっかり "buy" という動詞を持ち出したのが悪いのですが、これは実際にはコレクションリソースであって、動詞ではありません。気持ち悪いのであれば "purchase" と読み替えていただければと思います。
"transactions" のような汎用性を排除している理由については、このコメントの上のほうをご覧いただければと思います。
Posted by: kazuho | April 25, 2010 at 07:53 PM
何度もすみません。
>この手法だと、/transactions/${ID} に内容を入れるたびにリソースのステートは変化する
それは実装によるのでは…。ステートレスにするのであれば、PUTされた内容で全てを置き換えるように実装するべきかと思います。
>"purchase" と読み替えていただければ
これは理解しました。ちょっと記事の例を見落としていたのですが、リソースの仮押さえをするだけで、Webサービス間のトランザクションを行うWebサービスではないのですね。
>「なんでもまとめてコミットできるウェブサービス」なんて作りませんよね?
それを作る/作らないというのがREST業界で議論されているところかと思っています。SOAPでいうところのWS-TransactionやWS-ATに相当するインターフェースを作るか作らないかという議論ですね(個人的には今のところ反対ですが)。
Posted by: rika_t | April 25, 2010 at 09:40 PM
とりあえず今回は、なんでも扱う汎用トランザクションではなく、buyという名前空間で定義される「購入トランザクション」を扱うということで話を進めるのは了解です。
私もrita_tさんがおっしゃるように、PUTで毎回内容をすべて送信することでステートレスにするのが、まずは検討すべきスタイルだと考えます。ただそれだと、サーバサイドの実装がやや複雑になるかな、という危惧は感じます。PUTで書き換えられた部分を徐々に実行するためのステート管理(?)みたいな部分で。
その危惧を解消するために(つまり実装上の都合から)LSNを使うスタイルを導入するというのもアリかも知れませんが、前置きなしでそこに行き着くのは良くないんじゃないかな。奥さんのサンプルは、そういう実装上の都合も含めて検討した結果だと捉えればいいのかな。
Posted by: ただただし | April 25, 2010 at 10:41 PM
rika_t さん、たださん、ありがとうございます。
> 私もrita_tさんがおっしゃるように、PUTで毎回内容をすべて送信することでステートレスにするのが、まずは検討すべきスタイルだと考えます。ただそれだと、サーバサイドの実装がやや複雑になるかな、という危惧は感じます。PUTで書き換えられた部分を徐々に実行するためのステート管理(?)みたいな部分で。
トランザクションを開始している時点で既にステートレスではない (必ず rollback 処理が必要になる) ので、単一の PUT リクエストに部分的な書き換え機能を持たせるのではなく、不可分な単位までリソース (Request-URI) を分解するほうが良いと思います。
一度にひとつの商品しか購入できない API でかまわないのであれば LSN は明らかに不要です。が、そのような API でいいケースというのは少ないのではないでしょうか (決済や配送コストの問題があるので)。
... といった検討点を列挙しないままプロトコルの実装例を出しているのは、確かに分かりにくいですね。お金もらってる仕事じゃないので許してください...
rika_t さん、
> >「なんでもまとめてコミットできるウェブサービス」なんて作りませんよね?
> それを作る/作らないというのがREST業界で議論されているところかと思っています。SOAPでいうところのWS-TransactionやWS-AT に相当するインターフェースを作るか作らないかという議論ですね(個人的には今のところ反対ですが)。
ありがとうございます。リンク先、ゆっくり読ませていただきます。
Posted by: kazuho | April 26, 2010 at 01:06 AM