share facebook facebook2 twitter menu hatena pocket slack

2014.04.21 MON

Ruby on Rails: Scaffoldで量産されたControllerの処理をConcernで共通化してみる

WRITTEN BY山口 与力

cloudpack の ヤマグチです。

Railsネタです。

RailsにはConcernという仕組みがあります。ActiveSupportが提供する機能のひとつなのですが、一言でいうと複数モデル間で共通のロジックを1つのモジュールにまとめてDRYにできる機能で、Railsの開発者であるDHHもこれを推しているようです。

Concernよりも有名と思われるRailsの機能にScaffoldがあり、これはrails generate scaffoldコマンドを打つだけで関連するmodel、controller、viewを基本的な機能をあらかじめ実装した形で一気に自動生成してくれるというウルトラ便利機能です。

しかしこのScaffold、複数回使用するとその分だけほとんど内容の変わらないmodelやcontrollerが量産されてしまい、そんなのDRYじゃねえ!状態になりがちです。

上の記事ではmodelに対してconcernを使っていますが、同じ考え方はcontrollerにも使えます。

Scaffoldが生成するcontrollerはリソースの作成・更新・読み取り・削除(いわゆるCRUD)を提供します。そこでこのCRUDな機能をconcernとしてまとめてみようと思います。

まず適当なRailsプロジェクトを作り、2つのモデルをscaffoldします。

rails new test_proj --skip-bundle
cd test_proj
bundle install --path=vendor/bundle

bundle exec rails generate scaffold author name:string birth_date:date description:text
bundle exec rails generate scaffold book title:string release_date:date description:text

念のため動作を確認しましょう。

bundle exec rake db:migrate
bundle exec rails server

authorsとbooksがブラウザでCRUDできると思います。さて、これらのcontrollerを見てみましょう。

app/controllers/authors_controller.rb

class AuthorsController < ApplicationController
before_action :set_author, only: [:show, :edit, :update, :destroy]

# GET /authors
# GET /authors.json
def index
@authors = Author.all
end

# GET /authors/1
# GET /authors/1.json
def show
end

# GET /authors/new
def new
@author = Author.new
end

# GET /authors/1/edit
def edit
end

# POST /authors
# POST /authors.json
def create
@author = Author.new(author_params)

respond_to do |format|
if @author.save
format.html { redirect_to @author, notice: 'Author was successfully created.' }
format.json { render :show, status: :created, location: @author }
else
format.html { render :new }
format.json { render json: @author.errors, status: :unprocessable_entity }
end
end
end

# PATCH/PUT /authors/1
# PATCH/PUT /authors/1.json
def update
respond_to do |format|
if @author.update(author_params)
format.html { redirect_to @author, notice: 'Author was successfully updated.' }
format.json { render :show, status: :ok, location: @author }
else
format.html { render :edit }
format.json { render json: @author.errors, status: :unprocessable_entity }
end
end
end

# DELETE /authors/1
# DELETE /authors/1.json
def destroy
@author.destroy
respond_to do |format|
format.html { redirect_to authors_url }
format.json { head :no_content }
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_author
@author = Author.find(params[:id])
end

# Never trust parameters from the scary internet, only allow the white list through.
def author_params
params.require(:author).permit(:name, :birth_date, :description)
end
end

app/controllers/books_controller.rb

class BooksController < ApplicationController
before_action :set_book, only: [:show, :edit, :update, :destroy]

# GET /books
# GET /books.json
def index
@books = Book.all
end

# GET /books/1
# GET /books/1.json
def show
end

# GET /books/new
def new
@book = Book.new
end

# GET /books/1/edit
def edit
end

# POST /books
# POST /books.json
def create
@book = Book.new(book_params)

respond_to do |format|
if @book.save
format.html { redirect_to @book, notice: 'Book was successfully created.' }
format.json { render :show, status: :created, location: @book }
else
format.html { render :new }
format.json { render json: @book.errors, status: :unprocessable_entity }
end
end
end

# PATCH/PUT /books/1
# PATCH/PUT /books/1.json
def update
respond_to do |format|
if @book.update(book_params)
format.html { redirect_to @book, notice: 'Book was successfully updated.' }
format.json { render :show, status: :ok, location: @book }
else
format.html { render :edit }
format.json { render json: @book.errors, status: :unprocessable_entity }
end
end
end

# DELETE /books/1
# DELETE /books/1.json
def destroy
@book.destroy
respond_to do |format|
format.html { redirect_to books_url }
format.json { head :no_content }
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_book
@book = Book.find(params[:id])
end

# Never trust parameters from the scary internet, only allow the white list through.
def book_params
params.require(:book).permit(:title, :release_date, :description)
end
end

クラス名以外ほとんど同じコードですね?concernでまとめてしまいましょう!!

app/controllers/concerns/crudable.rb

module CRUDable
extend ActiveSupport::Concern

included do
before_action :set_model
before_action :set_resource, only: [:edit, :update, :destroy]

def index
instance_variable_set :"@#{model_name.tableize}", @model.all
end

def show
end

def new
instance_variable_set :"@#{model_name.underscore}", @model.new
end

def edit
end

def create
instance_variable_set :"@#{model_name.underscore}", @model.new(resource_params)

respond_to do |format|
if instance_variable_get("@#{model_name.underscore}").save
format.html { redirect_to action: :index, notice: 'Resource was successfully created.' }
format.json { render action: 'show', status: :created, location: instance_variable_get("@#{model_name.underscore}") }
else
format.html { render "concerns/admin/new" }
format.json { render json: instance_variable_get("@#{model_name.underscore}").errors, status: :unprocessable_entity }
end
end
end

def update
respond_to do |format|
if instance_variable_get("@#{model_name.underscore}").update(resource_params)
format.html { redirect_to action: :index, notice: 'Resource was successfully updated.' }
format.json { head :no_content }
else
format.html { render "concerns/admin/edit" }
format.json { render json: instance_variable_get("@#{model_name.underscore}").errors, status: :unprocessable_entity }
end
end
end

def destroy
instance_variable_get("@#{model_name.underscore}").destroy

respond_to do |format|
format.html { redirect_to action: :index, notice: 'Resource was successfully destroyed.' }
format.json { head :no_content }
end
end

private

def set_model
@model = controller_name.classify.constantize
end

def model_name
@model.to_s
end

def set_resource
instance_variable_set :"@#{model_name.underscore}", @model.find(params[:id])
end

def resource_params
params.require(model_name.underscore.intern).permit(*@model.column_names.map(&:intern))
end
end
end

app/controllers/authors_controller.rb

class AuthorsController < ApplicationController
require "crudable"
include CRUDable
end

app/controllers/books_controller.rb

class BooksController < ApplicationController
require "crudable"
include CRUDable
end

Concern moduleは〜ableという名前を付ける慣例っぽいので、ここではCRUDableとしてみました。元々のauthors_controller.rb、books_controller.rbはCRUDableをincludeするだけにまで単純化できました。いかがでしょうか?

ポイントとしてはset_modelコールバックで現在のコントローラ名を取得し、そこからモデルクラスへ@modelで参照できるようにしています。

ちなみにここではviewをそのまま使えるように@bookのようなインスタンス変数にセットするようにしていますが、ここを@itemや@resourceのように汎用的な名前にするとさらにコードがシンプルになり、viewも共通化することができます。お試しあれ。

余談ですが、bundle execはシェルの設定でbeというエイリアスで呼べるようにすると少しだけ幸せになれます。

こちらの記事はなかの人(ヤマグチ)監修のもと掲載しています。
元記事は、こちら