RailsでのfavoriteのURL設計
http://d.hatena.ne.jp/tkawa/20110508/p1
かなり前にこういう記事を書いたのですが、最近たまたま似たものをRailsで何回か実装する機会があって、これはいろんなところで使えるんじゃないかと思ったので、その設計実装パターンを紹介してみます。
モデル
任意のツイートに任意のユーザーがお気に入りをつけられるというもの。別にツイートじゃなくても何でもOKです。
class Tweet < ActiveRecord::Base
has_many :favorites
end
class User < ActiveRecord::Base
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :tweet
belongs_to :user
end
URL設計
Partial Resource パターンを利用します。
http://rest-pattern.hatenablog.com/entry/partial-resource
URL設計のところまでは元記事もごらんください。
resources :tweets do
resource :favorite, module: 'tweet', only: [:show, :update, :destroy]
end
tweet_favorite GET /tweets/:tweet_id/favorite(.:format) tweet/favorites#show
PATCH /tweets/:tweet_id/favorite(.:format) tweet/favorites#update
PUT /tweets/:tweet_id/favorite(.:format) tweet/favorites#update
DELETE /tweets/:tweet_id/favorite(.:format) tweet/favorites#destroy
最近は、こういうネストするリソースのときmodule: 'tweet'
で親の名前の名前空間に入れてTweet::FavoritesController
とするのをよくやってます。ほかのリソースにもfavoriteがつくときはそれぞれ区別できてわかりやすいです。
(名前空間が単数形か複数形かは議論ある)
コントローラ
基本の形はscaffoldです。
PUTリクエスト
PUT /tweets/1/favorite HTTP/1.1
Content-Type: application/x-www-form-urlencoded
favorite=1
によって、favoriteリソースをtrueの状態に変更する(ここではfavorite='1'
で作成する)、と考えるのがポイントです。
また、show
は実用上は必要ないことも多いのですが、ここではfavoriteリソースの状態を取得するのに使えるということを示すため、あえて記述しました。
リソースを設計する上では GETすることに意味があるリソース であることが重要です。これは動詞のリソースがよくない理由の1つでもあります。動詞のリソースはGETすることに意味がないからです。
class Tweet::FavoritesController < ApplicationController
before_action :authenticate_user!
before_action :set_tweet
# PUT /tweets/:tweet_id/favorite
# PUT /tweets/:tweet_id/favorite.json
# updateだが新規作成に用いる
def update
@favorite = favorites.first_or_initialize(favorite_params)
respond_to do |format|
if @favorite.save
format.html { redirect_to @tweet, notice: 'Favorite was successfully created.' }
format.json { head :no_content }
else
format.html { render text: @favorite.errors.full_messages, status: :unprocessable_entity } # 手抜き
format.json { render json: @favorite.errors, status: :unprocessable_entity }
end
end
end
# DELETE /tweets/:tweet_id/favorite
# DELETE /tweets/:tweet_id/favorite.json
def destroy
favorites.first!.destroy!
respond_to do |format|
format.html { redirect_to @tweet, notice: 'Favorite was successfully deleted.' }
format.json { head :no_content }
end
end
# GET /tweets/:tweet_id/favorite
# GET /tweets/:tweet_id/favorite.json
def show
@favorite = favorites.first!
respond_to do |format|
format.html { render text: @favorite.value } # 手抜き
format.json { render json: @favorite.value }
end
end
private
def set_tweet
@tweet = Tweet.find(params[:tweet_id])
end
def favorites
current_user.favorites.where(tweet_id: @tweet.id)
end
def favorite_params
value = params.require(:favorite)
value.is_a?(Hash) ? value : { value: value }
end
end
モデル(追加)
class Favorite < ActiveRecord::Base
belongs_to :tweet
belongs_to :user
validates_acceptance_of :value
attr_accessor :value
after_find { @value = '1' } # true
end
このコードではわざわざ値が'1'
であることのチェックを行っていますが、あまりいらないかも。
応用編
favoriteのような対応するモデルがなくても、操作前・操作後という状態を持つ操作は「操作済みリソース」を作成する(もしくはfalseからtrueに変える)操作とみなせるので、同様に表現できます。
例:トランザクションリソースの実行
トランザクションリソースについてはこちらも参照。
http://rest-pattern.hatenablog.com/entry/transaction-resource
実際のシステムでは、より複雑な処理、たとえば複数のリソースにまたがった変更をひとまとまりに扱う、いわゆるトランザクションが必要になるケースもあるでしょう。
…
データインポートなど複数のリソースに影響を及ぼす、バッチ的な動きをさせたい場合には「トランザクションリソースを作る」という考え方でリソース設計するようにしたところ、いろいろ捗りました。
resources :transactions
まずPOST /transactions
でトランザクションを作成し、ここにたとえばPUT /transactions/1
によってリソースへの変更を登録し、最後に一括して実行します。
「トランザクションの実行」は、「トランザクション実行済み」というリソースをfalseからtrueに変える操作とみなせます。
resources :transactions do
resource :committed, module: 'transaction', only: [:show, :update]
end
ちょうどTransaction#committed?
メソッドのようなイメージで「実行済み」リソースcommittedを用意します。
そしてPUT /transactions/1/committed
によって、committedリソースをtrueの状態に変更することで「トランザクションの実行」を行います。
もしトランザクションが非同期に実行されるなら、いったん202 Acceptedを返し、現在の状況をGET /transactions/1
によって表示することができます。
例:リストの項目を先頭・末尾に移動
class List < ActiveRecord::Base
has_many :items, order: 'position'
end
class Item < ActiveRecord::Base
belongs_to :list
acts_as_list scope: :list
end
(例としてacts_as_listを使っていますが、どんな実装でもOK)
「アイテムを先頭に移動する」という操作は、「アイテムが先頭にある」というリソースをfalseからtrueに変える操作とみなせます。
resources :items do
resource :position_top, module: 'item', only: [:show, :update]
end
アイテムが先頭にあるかどうかを表すリソースposition_topを用意し、そしてPUT /items/1/position_top
によって、position_topリソースをtrueの状態に変更することで「先頭に移動」を行います。
同様にposition_bottomリソースを用意すれば「末尾に移動」が可能ですが、position_topとposition_bottomは排他的なため、1つにまとめることも可能です。
resources :items do
resource :position_edge, module: 'item', only: [:show, :update]
end
こうするとposition_edgeリソースはtrue/falseではなく、top/bottom/otherの状態をもつようになります。ただここまで来ると単にpositionリソースとして数値で保持したほうがシンプルかもしれません。
まとめ
この考え方によって、「リソースに対する冪等な操作」はすべてこのパターンで書くことができるようになります。
これは「フラグリソースパターン」とでも命名しましょうか?(いい名前募集)
gemにしようと思ったのですが、実際のコントローラのコードは場合によってかなり違うので、どの部分を汎用的にすると便利かを考えるのに苦労していてまだ作れていません。今後にご期待ください。