Posted at

Favoriteの設計実装はパターンとして使える

More than 5 years have passed since last update.

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設計のところまでは元記事もごらんください。


config/routes.rb

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することに意味がないからです。


app/controllers/tweet/favorites_controller.rb

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


実際のシステムでは、より複雑な処理、たとえば複数のリソースにまたがった変更をひとまとまりに扱う、いわゆるトランザクションが必要になるケースもあるでしょう。



データインポートなど複数のリソースに影響を及ぼす、バッチ的な動きをさせたい場合には「トランザクションリソースを作る」という考え方でリソース設計するようにしたところ、いろいろ捗りました。



config/routes.rb

resources :transactions


まずPOST /transactionsでトランザクションを作成し、ここにたとえばPUT /transactions/1によってリソースへの変更を登録し、最後に一括して実行します。

「トランザクションの実行」は、「トランザクション実行済み」というリソースをfalseからtrueに変える操作とみなせます。


config/routes.rb

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に変える操作とみなせます。


config/routes.rb

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つにまとめることも可能です。


config/routes.rb

resources :items do

resource :position_edge, module: 'item', only: [:show, :update]
end

こうするとposition_edgeリソースはtrue/falseではなく、top/bottom/otherの状態をもつようになります。ただここまで来ると単にpositionリソースとして数値で保持したほうがシンプルかもしれません。


まとめ

この考え方によって、「リソースに対する冪等な操作」はすべてこのパターンで書くことができるようになります。

これは「フラグリソースパターン」とでも命名しましょうか?(いい名前募集)

gemにしようと思ったのですが、実際のコントローラのコードは場合によってかなり違うので、どの部分を汎用的にすると便利かを考えるのに苦労していてまだ作れていません。今後にご期待ください。