🐫

Rails の resources によって生成されるアクションに縛ったルーティングの定義について

に公開

RESTful ルーティング

Rails の scaffold 機能は素晴らしい機能です。データベースのテーブルに対する CRUD 操作を行うコードを自動生成するだけでなく、アプリケーション構築における重要な学びも提供してくれます。

scaffold は自動的に 7つのアクション(index, show, new, edit, create, update, destroy)を生成し、CRUD 操作を扱えるようにします。しかし、多くのアプリケーションでは、これ以上のアクションは不要とされ、むしろ作成すべきではないと考えられています。

REST には Uniform Interface(統一インターフェース) という概念があります。これは、リソースを表す URI に対して HTTP メソッドを通じた操作を標準化するものです。Rails の config/routes.rb で以下のように1行記述するだけで、この概念を簡単に実装できます。

resources :users

その機能、それほどには特殊なものではないんです。

リソースを決定し、すべてのルーティングを resources (or resource) で定義し、アプリケーションのすべてを提供される 7つのアクション内で処理する。この制約のもとでアプリケーションを構築することは可能でしょうか?
ほとんどの場合、可能だと考えています。

「でもでも、私のアプリケーションはちょっと特殊な事情を含んでいて難しいんです!」 と思うかもしれません。しかし、その難しさの多くは、リソースに対する洞察をより深めることで大抵は光の道が見えます。

たとえば、あるアプリケーションにおいて「登録済みユーザーの退会」といった操作を考えてみます。

# config/routes.rb
resources :users do
  post :resign
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def resign
    # resign process.
  end
end

UsersController#resign というアクションを定義することで、この機能を実現することは可能です。
しかし、この方法では post :resign のように resources 以外のルーティング宣言が必要になります。これは、 POST /users/:id/resign というパスとなり、 URL に動詞が出現することを意味します。
本来であれば、scaffold によって生成されるアクションのみを使用し、このような逸脱を避けるのが理想的です。

この問題に対処するために、「Resignation」のような別の名詞(リソース)を特定し、それに対して CRUD 操作を考えるのが有効かもしれません。さらに、現在のコントローラー内で users/ という名前空間を利用して整理することで、より明確な構造にすることができます。

# config/routes.rb
resources :users do
  resource :resignation, only: %i[create], module: 'users'
end
# app/controllers/users/resignations_controller.rb
class Users::ResignationsController < ApplicationController
  def create
    # resign process.
  end
end

サブリソース

ユーザーが一意に識別されることを前提とし、将来的にサブリソースが増える可能性を考慮すると、Users::ApplicationController を定義するのも有益です。
この設計により、@user の代入処理を一元化でき、各コントローラーでの重複を避けることができます。
(ただし、 action hook や暗黙に見える @user へのアサインなどを好ましくないと考える人もいるかもしれません)

# app/controllers/users/application_controller.rb
class Users::ApplicationController < ApplicationController
  before_action :set_user

  private

  def set_user
    @user = User.find(params[:user_id])
  end
end
# app/controllers/users/resignations_controller.rb
class Users::ResignationsController < Users::ApplicationController
  def create
    # resign process for @user
  end
end

例えば、後から「そのユーザーのログイン履歴の一覧表示や詳細表示」のアクションを追加したい場合、次のように定義できます。

# config/routes.rb
resources :users do
  resource :resignation, only: %i[create], module: 'users'
  resources :login_histories, only: %i[index show], module: 'users'
end

# 上記が冗長だと感じる場合は、以下のように scope を使ってまとめることもできます。
resources :users do
  scope module: 'users' do
    resource :resignation, only: %i[create]
    resources :login_histories, only: %i[index show]
  end
end
# app/controllers/users/login_histories_controller.rb
class Users::LoginHistoriesController < Users::ApplicationController
  before_action :set_login_history, only: %i[show]

  def index
    @login_histories = @user.login_histories
  end

  def show
  end

  private

  def set_login_history
    @login_history = @user.login_histories.find(params[:id])
  end
end

これらはもちろん、説明上の簡単なケースです。
しかし、より複雑な解決すべき課題に直面したときでも、やりたいことに対し洞察を深めることで「動詞」ではなく「名詞」を大抵は見出すことができます。
その場合、コントローラの実装は scaffold が生成するコードとそこまでギャップのないシンプルな構成を維持できるはずです。


参考

Discussion