Help us understand the problem. What is going on with this article?

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

sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away