cloudpack の ヤマグチです。
Railsネタです。
RailsにはConcernという仕組みがあります。ActiveSupportが提供する機能のひとつなのですが、一言でいうと複数モデル間で共通のロジックを1つのモジュールにまとめてDRYにできる機能で、Railsの開発者であるDHHもこれを推しているようです。
- Put chubby models on a diet with concerns by David of Basecamp
- 翻訳 ふとっちょのRails modelをconcernsでダイエットしよう | ntcncp.net
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というエイリアスで呼べるようにすると少しだけ幸せになれます。