ken2merのブログ

Webエンジニア, Ruby on Rails, Go

『Webを支える技術』を読んでRailsアプリ開発の断片知識をつなげる (その1)

Railsの業務案件を担当して一年が過ぎた。

参画したばかりの頃は、とにかくキャッチアップしなければと必死に踠いていた。幸いRubyRailsはネット上の情報が豊富で「この場合はこのようなコードを書けば良い」など、やり方についての記事はすぐに見つかるので助かった。

しばらくして業務にも慣れてくると、コードの書き方・作法などは何となく分かってきたなという気になってくる。しかし、いざ説明しようとすると言語化できないことに気づかされる。Web開発の基礎というべき知識が足りておらず、それを得るにはネット上の断片的なものではなく体系的なインプットが必要なのでは、というところに思い至った。

体系的な知識を得るには書籍が良いのではないか。まずはWebの周辺技術としてHTTPやRESTについて基礎知識を取り入れたい。調べていると『Webを支える技術』(以下、本書とする)という本が良さそうだった1ので今回はそれを読んでいく。

現時点で自分が持つWebアプリケーション開発についての知識は、Railsを使った開発で培った断片的なものにすぎない。なので、それらを相互に繋げていきたいと考えている。そこで本記事では、Railsで開発する際の断片的なノウハウと擦り合わせるようにして本書から得られた内容をまとめる。

リソース(3.3章)

リソースは、RESTにおける重要な概念の一つである。本書では以下のように説明されている。

・リソースとは、Web上の情報である
・世界中の無数のリソースは、それぞれURIで一意の名前を持つ
URIを用いることで、プログラムはリソースが表現する情報にアクセスできる

たとえばRailsアプリケーションをWeb上に公開していて、そこにアクセスすることで得られる情報がリソースということになる。どのようなリソースが得られるかはURIによって決められる。

RailsアプリケーションのURIを設定するには、config/routes.rb というファイルに以下のように記述する。

Rails.application.routes.draw do
  resources :photos
end

これだけで photos に関するリソースのパスが設定できる。この resources というのはメソッドで、これがアプリケーションの起動時に呼ばれることで実現される。

作成されるルーティングは以下のようになる。(Railsガイドより抜粋)

HTTP動詞 パス コントローラ#アクション
GET /photos photos#index
GET /photos/new photos#new
POST /photos photos#create
GET /photos/:id photos#show
GET /photos/:id/edit photos#edit
PATCH/PUT /photos/:id photos#update
DELETE /photos/:id photos#destroy

これらのURIにアクセス(リクエスト)があると適切なルーティングが行われるようになっている。この場合は PhotosController 内のアクション(メソッド)が呼ばれることになるだろう。そのアクションでは Photo というModelを利用した処理が行われ、取得したデータがViewに渡される。Viewでの出力が「リソースが表現する情報」ということになる。

次章では、これらのURIがどのような設計指針に基づいているかについてまとめる。

URIの設計指針(5.2章)

URIの設計指針として本書では以下が挙げられている。

URIプログラミング言語依存の拡張子を利用しない(.pl、.rb、.do、.jspなど)
URI実装依存のパス名を利用しない(cgi-bin、servletなど)
URIプログラミング言語のメソッド名を利用しない
URIにセッションIDを含めない
URIはそのリソースを表現する名詞である

特に重要なのは最後だと思う。

Railsresources メソッドにより上述のようなルーティングが設定可能だが、それ以外に独自のパスを設定する場合がある。その際、アクションが動詞(create, deleteなど)のためか、気を抜くと名詞ではなく動詞を含めてしまいがち2な点に気をつける必要がある。

これはURIとHTTPメソッドの関係を考えるようにすれば避けられる。本書では次のように記述がある。

HTTPではリソースに対して特定のHTTPメソッドだけを適用します。あるリソースを取得するのか更新するのかは、URIで指定するのではなく、URIに適用するHTTPメソッドで決定します。つまり、URIとHTTPメソッドの関係は、名詞と動詞の関係にあります。したがってURIは、全体として名詞となるように設計するべきです。

HTTPメソッドそれ自体が動詞なので、POST /photos, DELETE /photos/:id のようにすべきであり、反対に POST /photos/create, DELETE /photos/:id/delete などとするのは冗長な表現である。個人的には、アクションの数だけパスのパターンが増えて複雑になってしまうのが嫌というのもある。

ここでHTTPメソッドについて役割・用途など確認しておく。

HTTPメソッド(7章)

HTTP1.1では現在9つのメソッドが定義されている。

resources メソッドによるルーティングでは、GET / POST / PATCH / PUT / DELETE の5つが対象となるため、それらについて見ていく。

GET(7.3章)

リソースの取得に使用する。対応するパスとコントローラアクションは以下(ルーティングの一部を再掲)。

HTTP動詞 パス コントローラ#アクション
GET /photos photos#index
GET /photos/new photos#new
GET /photos/:id photos#show
GET /photos/:id/edit photos#edit

これは以下のことを表している。

  • それぞれのパスに対してGETでのリクエストが可能
  • リクエストがあると、それぞれのアクションが(最終的に)呼ばれる

これらのアクションで期待される処理はそれぞれ以下のようになる。

  • photos#index: 写真の一覧を表示
  • photos#new: 写真を1つ作成するためのHTMLフォームを返す
  • photos#show: 特定の写真を表示する
  • photos#edit: 写真編集用のHTMLフォームを1つ返す

POST(7.4章)

子リソースの作成に使用する。対応するのは以下。

HTTP動詞 パス コントローラ#アクション
POST /photos photos#create

このアクションで期待される処理は写真を1つ作成すること、つまり /photos に対して「新しい子リソースが作成されること」である。photos というリソースがあり、そこに新しく一件(たとえば /photos/1)を追加する処理になる。リソースの作成が成功すると、そのリソースのURLにリダイレクトする処理が行われたりする。(Railsでは redirect_to メソッドが使われる)

ほかに期待されるものとして以下が例として挙げられている。

  • レスポンスで 201 Created というステータスコードが返ってくる
  • Location ヘッダに新しいリソースのパスが含まれる(/photos/1)

POSTなら必ずLocationヘッダを含めて 201 Created を返さなければならないかというと、そうではないと思う。上記は例であって、ブラウザからのリクエストの場合は上述のとおりリダイレクトさせる場合が多いだろう。redirect_to を使う場合、ステータスコードはデフォルトでは 302 Found となる。(参考)

では上の2点を実際に行うのはどのような場合があるだろうか?後で詳しく調べる(調べない)として、いまのところ思いつくのは下記の2パターンがあると思う。

  • ブラウザからのPOSTで作成したリソースにリダイレクトしない場合
  • APIとしてJSONレンダリングするとき

実際には、このような場合でも 200 OK204 No Content (レスポンスボディがない場合)を返したり、Locationヘッダの代わりにボディに新しいリソースへのパスを含めたりすることもあるようである。大切なのは、どのようなレスポンスだとクライアントから見て親切なのか、という視点なのかもしれない。

参考URL

PUT(7.5章)

本書ではPUTの機能について、リソースの更新と作成について述べられている。そのうちリソースの更新については、最新ではPATCHがデフォルトになるので後述の章を参照してほしい。

なぜPATCHがデフォルトになったかは2012年のブログが残っている。Rails 4.0からそのように変更されたらしい。

このブログを参考にすると、PUTでのリソース更新はreplacement(置換)となるため /photos/:id のようなリソースのpartial updates(部分的な更新)には適していないという。編集フォームでの送信も部分的な更新にあたることが多いとしている。このような処理はPATCHを使うべきである。

PUTを使うべきなのはリソースの置換である。例としてはファイルのアップロードがあげられる。アップロード先に既に同じファイルがある場合はそれを置き換え、なければ新たに作成されるようにする。本書で説明されるPUTでのリソースの更新・作成は、このような処理を指すと考えて良いだろう。

PATCH

つぎにPATCHであるが、これは上述の通り、リソース更新の中でも「部分的な更新」となる場合に使用する。

HTTP動詞 パス コントローラ#アクション
PATCH/PUT /photos/:id photos#update

このアクションで期待される処理は /photos/:id というリソースの一部を更新することである。たとえば編集フォームでは、一部の画面項目だけ変更して送信ボタンが押されることが多い。変更のあったものだけパラメータで受け取るようにして、その属性だけ更新するように処理を実装する。

参考URL

DELETE(7.6章)

リソースの削除に使用する。

HTTP動詞 パス コントローラ#アクション
DELETE /photos/:id photos#destroy

名前通り /photos/:id にあたるリソースを削除することが期待される。ただし、一般的にはレスポンスボディを持たないという点に注意する。そのためリクエストが成功した際に返却されるステータスコード200 OK204 No Content の場合がある。

POSTで PATCH / PUT / DELETE を代用する(7.9章)

以上で GET / POST / PATCH / PUT / DELETE の5つのHTTPメソッドについて見てきた。しかし、HTMLのフォームで設定できるメソッドはGETとPOSTだけという制限がある。そのため、PATCH / PUT / DELETE についてはPOSTメソッドで代用する手法が取られる。

Railsでは、本書でも紹介されているように _method という隠しパラメータが用意されていて、使用したいメソッドはここで指定する。これについてはRails ガイドでも説明されている。

Rails ガイドの例では form_tag が使用されている。たとえば form_tag(:photo_path, method: :patch) のようにしてoptionハッシュに指定すると、_method パラメータの値に文字列('patch')としてセットしてくれる(定義部分は method_tag を参照)。

べき等性と安全性(7.11章)

べき等とは「ある操作を何回行っても結果が同じこと」、安全とは「操作対象のリソースを状態を変化させないこと」を意味する。これらの性質によって、HTTPメソッドを分類することができる。

Webアプリケーション開発では、各HTTPメソッドでのリクエストに対し、この性質を考慮した適切な処理を実装する必要がある。以下では、これまで見てきた5つのHTTPメソッドの性質と、そのリクエストに対するサーバ側の処理を実装する際の注意点をまとめる。

GET: べき等かつ安全

あるリソースを何回GETしても、結果は変わらず、そのリソースのその時点での表現が取得できる。また、そのリソースの状態を変化させることもない。

ただし、操作対象以外のものに対しては副作用が及ぶ可能性があるので、この点には注意する。たとえば、サーバのログファイルへの追記や、Webページのヒットカウンタの更新があげられる。これらは操作対象のリソースではないため、GETの安全性とは切り離して考慮する必要がある。

サーバ側の実装としては、GETに対する処理の中では、対象のリソースに対して変更や更新処理をするべきではない。ただし、別のリソースへの変更は考慮されるので、ログファイルやカウンタなどの更新は行っても良いことになる。

PUT/DELETE: べき等だが安全でない

PUTとDELETEは、同じリクエストを複数回送信しても結果が変わらない性質を持つ。

本書の例では、まず /test に対してPUTを行う。リソースの内容が test2 に更新されるようにリクエストを送信する。このリクエストを何回行ってもリソースの内容は test2 となる。

DELETEの場合も同様で、test に対してリクエストを行い、もう一度同じリクエストを送信する。2回とも「リソースが削除されている」という結果になる。ただし、レスポンス(主にステータスコード)は異なる場合があるため注意する。

たとえば上記の例では、1回目のリクエストの前は /test というリソースが存在するため、DELETEを行うと 200 OK が返る。その後、2回目のリクエストの際は /test というリソースは削除済みのため、DELETEを行っても 404 Not Found のようになる。

このようにレスポンスは異なっている場合でも、/test というリソースが削除されているという結果は同じになるため、DELETEはべき等であると言える。

以上が PUT/DELETE の持つ性質である。

サーバ側では、同じ PUT/DELETE が何回行われても、リソースの内容が一定となるような処理が求められる。

POST: 安全でもべき等でもない

POSTの場合は、リクエストの結果で何が起きるか分からない。同じPOSTが複数回送信された場合、リソースの作成が二重に行われる可能性がある。そのため、クライアント側でPOSTが複数回送信されないような対策が基本的には必要になるだろう。同時にサーバ側の処理でも、万が一複数送信があった場合の対策を行うことになる。

Railsでは、フォーム送信の場合は data-disable-with 属性 でユーザの2回クリックを防ぐ方法や、バックエンド側では重複データが登録されないようモデルでのバリデーションを行う方法があげられる。

メソッドの誤用(7.12章)

GETが安全でなくなる例

本書では次のように説明される。

GETを正しく利用しているかどうかの判断基準は、GETの発行前後でリソースに変更が加えられていないかどうか(安全かどうか)です。また、GETしようとしているリソースのURIに、「delete」「update」「set」などの動詞が入っている場合も要注意です。GETで取得するはずのURIに動詞が入っているのは矛盾しています。

GETでリソースを更新したり、リソースを削除したりするのは「GET: べき等かつ安全」の章で述べた通り、誤った利用方法である。また、リソースのURIは動詞ではなく名詞であるべきというのも「URIの設計指針(5.2章)」の章で述べた通りである。

ほかのメソッドでできることにPOSTを誤用した例

ほかに適切なメソッドが用意されているにもかかわらず、POSTでその機能を実現するのは避けるべきである。

GET、PUT、DELETEで実現できる機能をPOSTで実装した場合、たとえば本来であれば不要なはずの二重送信の対策をクライアント側で行う必要があり生産性が下がる、といったデメリットを被る場合がある。

PUTがべき等でなくなる例

PUTでリソース内容の相対的な差分を送信すると、PUTはべき等でなくなる。

本書では、トマトの価格を表現する /tomato というリソースを例にしている。現在トマトの価格と100円として、そのリソースを更新するPUTの内容が +50円 であるとする。そのリクエスト内容を差分の表現として受け入れてしまうと、これが複数回送信された際には、その送信された回数 * 50円が増え続けてしまう。

そのためPUTでは、そのリソースのなるべく完全な表現を送信する必要がある。上記の例で言えば +50円 でリソースを置換するような処理とする。この場合、複数回送信された場合でもトマトの価格は常に 50円 となり、べき等性を担保することができる。

DELETEがべき等でなくなる例

DELETEの例では /latest という最新バージョンのリソースを指し示すエイリアスリソースを考える。

このリソースそのものを削除するようにWebサービスやWeb APIを実装した場合は、このリクエストはべき等となるが、一方で /latest が示す実際のリソースを削除するようにしてしまうと、べき等ではなくなるので注意する必要がある。

たとえば /v1.2 (バージョン1.2)が最新だとして DELETE /latest のようにした場合、1回目のリクエストでは /v1.2 が削除される。2回目のリクエスト時には /latest/v1.1 を指し示しており、今度はそちらが削除されてしまう、といった具合である。

これを避けるには、そもそもこのような「特殊なリソース(ある特定のリソースを永続的に示すのではなく、時間や状況で指し示すリソースが変化するリソース)」は、基本的には更新や削除などの操作ができないように設計するのがよい。

HTTPメソッド(7章)まとめ

以上でRailsresources メソッドによる GET / POST / PATCH / PUT / DELETE の5つのHTTPメソッドについて、それぞれの役割や性質、クライアントからのリクエストに対してどのような処理が行われることが期待されるのか、などを整理した。これらのHTTPメソッドの役割や性質を理解し、それに沿った処理を実装することがRESTfulなWebアプリケーション構築への一歩となるだろう。

続きは後半へ

思ったよりもHTTPメソッドに関するまとめが長くなってしまったので続きは後半へ。

後半は以下の内容を予定。


  1. RubyとRailsの学習ガイド2019年版 でさらっと紹介されていたのがきっかけだったと思う。他に この記事この記事 なども参考になった。何より著者の ブログ記事 がよかった。

  2. 初めの頃は自分もよくやっていたし、また新人のコードを見ていて指摘することも多くある

  3. 本書の出版後?にPATCHがRFCとなったためと思われる