Ruby
Rails

リソースの一部更新におけるURL設計

More than 1 year has passed since last update.

概要

Webアプリケーションにて、リソースの一部更新を行う際、どのようにURL設計を行うとシンプルで美しいか(本当はそこまで考えていなかったけど)悩んでいたところ、 @t_wada さんから素敵な設計指針をご教示いただきました。

本記事はその内容に加えて、実際に自分で行ったこと、調べたこと、思った事など、まとめております。

あらすじ

数週間前にSIピラミッドからヒモなしバンジーを決めてWebの世界に飛び込んだ私は、小さな小さなWebアプリケーションをrails newから手探りで作っていました。

そんなとき、簡単なリソースの一部更新機能をどう実装したもんかなーと悩んでました。以下、当時(といっても先週)の超雑なぼやき。

リンクをクリックしてモデルの一部を変更するのはどうしたらいいんだろう。

例)不参加をクリック -> 某カラムをtrueからfalseへ

  • リクエストオブジェクトに対象カラムの情報を詰めてPATCH
  • オブジェクト丸ごと詰めてPUT

だれか教えてください。

するとなんと、 @t_wada さんからのレスポンスが……!
以下、頂いた素晴らしいコメントです。

PUT か PATCH かなら、PATCH を使いましょう。PUT は全置き換え、 PATCH は部分置き換えなので、 Rails4 から更新処理は PATCH リクエストがデフォルトになりました。なので、PATCH がまずはオススメです。しかしさらに踏み込む場合は、 URL 設計をより深く考えましょう。設計によっては、 PUT や DELETE でも同様のことを表現できます。

URL 設計は

  1. 何に対して (URL)
  2. どうするか (HTTP Method)

です。

例を挙げましょう。

例えば dropbox のようなファイル共有システムを作っているとして、
そのファイルのロック機能を提供することになったとします。

/path/to/file に PATCH リクエストを投げても良いのですが、 /path/to/file/lock に PUT リクエストを投げることでロック、 DELETE リクエストを投げることでロック解除、という設計もできます。こうすれば、ロックの有無も GET で取得できるようになります。

こうすることで、単機能で冪等性のある機能を提供することができるようになるわけです。

ちなみに、画面にファイル一覧が表示されていてチェックボックスを付けていき、最後に一括ロックボタンを押す、というような画面の場合には、集合処理になりますね。そのような場合はファイルの集合を示す URL に POST リクエストを投げるかたちで実現するでしょう。

あらすじ終わり。ちなみに、本題もだいたい終わりました。
以下、自分なりにまとめていきます。

要点

今回の要点は、以下の通りかと思います。

  1. URL設計の超基本
  2. 更新系メソッドPUTとPATCHの違い
  3. リソースの属性をリソースと捉える設計

特に「リソースの属性をリソースと捉える設計」についてが目から鱗でした。

URL設計の基本

Simple is best. これが全ての基本だと感じました。

URL 設計は

  1. 何に対して (URL)
  2. どうするか (HTTP Method)

です。

ここで大事なことは、

  1. URLはリソースであること
  2. 処理内容に適したメソッドを選ぶこと

の2点だと思います。

2のメソッド選択については、それぞれのメソッドがどのように使われるか、どのような性質があるかといったことを理解しておく必要があるかと思います。

それについては、良書や良記事がたくさんあるので、そちらをご参照ください。

更新系メソッドPUTとPATCHの違い

更新を行うメソッドは、PUTとPATCHの2つが存在します。こちらも調べるとわんさか出てきますので、詳細な解説については毎度のことながら丸投げします。

簡単にまとめると以下の通りです。

メソッド 概要
PUT 全置き換え
PATCH 部分置き換え

この性質の違いから、リソースの一部更新をする場合には、そのリソース (URL) へPATCHリクエストを投げるのが、最も自然な設計となります。

リソースの属性をリソースと捉える設計

本題です。

設計によっては、 PUT や DELETE でも同様のことを表現できます。

/path/to/file に PATCH リクエストを投げても良いのですが、 /path/to/file/lock に PUT リクエストを投げることでロック、 DELETE リクエストを投げることでロック解除、という設計もできます。こうすれば、ロックの有無も GET で取得できるようになります。

これは、リソースの一部(属性)をリソースとして捉えることによって実現しています。 より具体的に言うと、リソースの属性をそのリソースの子リソースとして捉えている状態だと思います。

上記の例ですと、ファイルがロックされているかどうかを、「lockリソースの有無」で表現しています。

  • GETしてリソースが取れれば、ロック中
  • PUTでリソースを作成すれば、ロック開始
  • DELETEでリソースを削除すれば、ロック解除

このように、リソースの属性をリソースと捉えることで、非常にシンプルで冪等性のある設計ができる場合があるそうです。

適用可能例

リソースが持つBoolean型の属性に対しては、簡単にこの設計が適用できるのではないかと思います。基本的に属性をリソースとして捉えることができる場合に有効だと思います。

加えて、

ちなみに、画面にファイル一覧が表示されていてチェックボックスを付けていき、最後に一括ロックボタンを押す、というような画面の場合には、集合処理になりますね。そのような場合はファイルの集合を示す URL に POST リクエストを投げるかたちで実現するでしょう。

とあるように、集合をリソースとして捉えることもできそうです。

集合をリソースとして捉える場合

集合をリソースとして捉える場合、具体的にどのようなURL設計が良いのか、追加で質問してみました。

集合を示すURLは /path/to/files/locks といった感じでしょうか。
/path/to/files/locks に file_id の集合をPOSTし、該当の /path/to/file/lock を作成/削除する設計になるのでしょうか。

以下、 @t_wada さんの回答です。

ロックだけの集合なら /path/to/files/locks 案はシンプルで良いですね。

ロック以外にもいろいろ(削除とか)やった後に「反映」ボタンがあるような場合はファイルの集合 /path/to/files に POST リクエストを投げてしまいます。

やはりシンプルに、そのリソースに名前(URL)をつけるのが良さそうです。

Railsでの実装

冒頭でぼやいていたものに対して、アドバイスを元に実際にコーディングをしてみました。

簡単に説明をすると、usersテーブルのBoolean型であるactiveカラムの状態を変更するという超シンプルな機能を、「リソースの属性をそのリソースの子リソースとして捉える」設計で実装しました。

子リソースへのroutes.rbは以下のように記述しました。

routes.rb
resources :users do
  resource :activeness, only; [:update, :destroy]
end

生成されるURIパターンは/users/:id/activeness(.:format)となり、見事に子リソースっぽくなっています。

そして、実際の処理はactivenesses_controller.rbを用意し、そこへ記述していきました。

activenesses_controller.rb
def update
  # PUTの処理
end

def destroy
  # DELETEの処理
end

また、以下のようにroutes.rbを書く方法も試してみました。

routes.rb
resources :users do
  member do
    put 'activeness'
    delete 'activeness'
  end
end

生成されるURIパターンは先ほどと一緒ですが、処理するコントローラーが変わってきます。この場合、PUTでもDELETEでもusers_controller.rbactivenessメソッドに処理が渡されます。
そのため、リクエストのメソッドを判定し、処理を分岐させる必要があります。

users_controller.rb
def activeness
  if request.put?
    # PUTの処理
  end

  if request.delete?
    # DELETEの処理
  end
end

これでも動作しますが、1つのメソッド(アクション)に2つの役割を持たせてしまっているため、あまり良くありません。そこで、下記の用にコントローラーとアクションを指定します。

routes.rb
resources :users do
  member do
    put 'activeness' => 'users#activate'
    delete 'activeness' => 'users#deactivate'
  end
end

上記の通りに書くと、users_controller.rbactivateメソッドとdeactivateメソッドで処理を行えるようになります。

users_controller.rb
def activate
  # PUTの処理
end

def deactivate
  # DELETEの処理
end

また、put 'activeness' => activenesses#updateのように指定すれば、最初のresourceでの実装と同じように、activenesses_controller.rbupdateメソッドに処理を渡すこともできると思います。

こんな感じで実装してみたところ、シンプルに/users/:id/activenessにPUTやDELETEを投げるだけで属性の変更ができるようになりました。

POSTではなくPUTを使う理由

リソースの新規作成時に、POSTでなくPUTを採用しているのは何故なんだろうと疑問に感じたため調べてみました。

まずは手元にある良書(まだ読み終わってない)の、Webを支える技術を開いてみたところ、P.95にPOSTとPUTの違いが掲載されていました。

POST

  • リソース作成時にクライアントが URIを指定できない
  • URIの決定権はサーバ側にある

PUT

  • リソースのURIはクライアントが決定する

とのことです。この特性の違いについては、冪等性の違いから発生しているのかなと感じました。HTTP/1.1: Method Definitionsによると、POSTは冪等でなく、PUTは冪等であるっぽいです。ちなみに、前述したPATCHは冪等性は保証されてません。

自分なりに噛み砕くと以下の感じになります。大して引用元と変わっていませんが……。

POST

  • どんなURLになるか明確でない
  • 冪等性は保証しない

PUT

  • どんなURLになるか明確である
  • 冪等性を保証する

今回の例では、リソースの位置(URL)はとても明確で、/users/:id/activenessだと確定しています。また、何度作成や更新を繰り返そうと、同じメソッドを呼び出せば常に同じ状態になるため、完全に冪等です。

そのため、PUTを選択するのが適当だと思います。

また、

ちなみに、画面にファイル一覧が表示されていてチェックボックスを付けていき最後に一括ロックボタンを押す、というような画面の場合には、集合処理になりますね。そのような場合はファイルの集合を示す URL に POST リクエストを投げるかたちで実現するでしょう。

と、集合をリソースと見なしたものに対しては、POSTを投げると仰っていたのは、

  • 集合の要素は動的に変わる(冪等ではない)

ことが理由かなと感じました。

まとめ

  • URL設計はリソース (URL) と操作 (メソッド)
  • 属性の状態や集合もリソースと見なすことができる
  • POSTとPATCHは非冪等、PUTは冪等
  • PUTは全置き換え、PATCHは部分置き換え

謝辞

@t_wada さんには、非常に勉強になるアドバイスをいただいただけではなく、投稿前に内容の確認もしていただきました。本当にありがとうございます。