Rails2.1でhas_manyな関連を設定してscaffold出来たとしたら...
scaffoldはモデル同士の関連は無視して*1、最も基本的なwebアプリケーションとしての骨格を生成してくれる。自分ではそれをお手本に、少しずつ拡張していくことが多い。しかし、モデル同士が無関係という状況はほとんどなく、ほとんど例外なくモデルにhas_manyやhas_one、belongs_toと書いて、毎回同じような修正をして行くことが多い。(そして、ちょっとした間違いでエラーをもらうことが多い。)そんな時、もしscaffoldがやってくれたら、どんなコードを生成してくれるのだろう?Rails2.1でサンプルプロジェクト「Q&Aサイト」を作りながら、試行錯誤してみた。
いつもの手順
$ rails QandA $ script/generate scaffold question name:string body:text $ script/generate scaffold answer name:string body:text question_id:integer $ rake db:migrate $ script/server
- 以上で、まだ質問と答えは無関係な状態だが、動くものは出来てしまう。
migrationの書き方
- migrationファイルで外部キーを追加する時、以下の書き方でもOKになった。
class CreateAnswers < ActiveRecord::Migration def self.up create_table :answers do |t| t.string :name t.text :body # t.integer :question_id # 上記は以下と同等 t.references :question t.timestamps end end ...(中略)...
- しかし、scaffoldの引数で使うと、コントローラーやビューの中で「@answers.question」と展開されてしまい、エラーになる。(本来は「@answers.question_id」となって欲しい。)今イチな感じ。
rake db:migrateによるDB操作
- rake db:migrateのバージョン管理コードが14桁の日付と時刻*2を羅列した数字になり、以下の操作が可能になった。
# バージョン指定したマイグレーションファイルのみdownする $ rake db:migrate:down VERSION=20080720072122 # バージョン指定したマイグレーションファイルのみupする $ rake db:migrate:up VERSION=20080720072122
- これは個人的には嬉しい変更だ。その他のテーブルの保存状態には影響を与えずに、指定したテーブルだけ修正できる。
- バージョン管理コードは長ったらしいので、ファイル名をコピーしている。ファイル名そのままを指定してもOKだった。
# ファイル名に拡張子まで含まれていても正常に処理された $ rake db:migrate:up VERSION=20080720072122_create_questions.rb
- rake db:migrate:reset
- マイグレーションを最初からやり直して、DBを上書きする。(つまり、保存されているデータはすべて削除される)
- db:migrate:redo
- rake db:rollbackとrake db:migrateの合わせ技らしい。しかし、自分の環境ではdb:migrateの処理がschema.rbに反映されないようだ。(DBには反映されている。改めてrake db:migrateを実行するとschema.rbはDBの状態と一致する。これじゃredoの意味が無い。)
- 参考ページ
- マイグレーション関連のRakeタスクの再確認 - ひげろぐ(感謝です!)
モデルの修正 app/models/question.rb, answer.rb
class Question < ActiveRecord::Base has_many :answers end class Answer < ActiveRecord::Base belongs_to :question attr_protected :question_id # または # attr_accessible :name, :body end
ルートの変更 config/routes.rb
- 「質問」あっての「答え」なので、ルート設定を:has_manyな関係に変更した。
ActionController::Routing::Routes.draw do |map| # map.resources :answers map.resources :questions, :has_many=>answers # Install the default routes as the lowest priority. map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end
- これで[答え」にアクセスするときのURLは「http://localhost:3000/questions/1/answers」のようなURLになる。(常に「questions/:question_id/」が付加される)
# 以下、Answerモデルにアクセスするhtmlのルートのみ。xmlのルートは省略した。 question_answers GET /questions/:question_id/answers {:controller=>"answers", :action=>"index"} POST /questions/:question_id/answers {:controller=>"answers", :action=>"create"} new_question_answer GET /questions/:question_id/answers/new {:controller=>"answers", :action=>"new"} edit_question_answer GET /questions/:question_id/answers/:id/edit {:controller=>"answers", :action=>"edit"} question_answer GET /questions/:question_id/answers/:id {:controller=>"answers", :action=>"show"} PUT /questions/:question_id/answers/:id {:controller=>"answers", :action=>"update"} DELETE /questions/:question_id/answers/:id {:controller=>"answers", :action=>"destroy"}
answer側の変更
上記のルート設定に合わせて修正すると、以下のようになった。
コントローラー
- コメントマーク「##」二つの行が修正箇所。次の行で修正している。
- before_filterで常に@questionを取得するようにした。
- オプジェクトをそのままキーとする場合は、配列で[@question, @answer]とすることで「questions/1/answers/1」というパスを取得できた。(@question.id=1, @answer.id=1の場合)
- answersコントローラー内(同一コントローラー内)であれば、question_answers_url(@question)は、引数を省略してquestion_answers_urlでもOKだった。
- create, updateアクションではパラメーター詐称防止のため、以下のように書いている。
- 「if @question.answers << @answer」
- 「if @answer.update_attributes(params[:answer].merge(:question_id=>params[:question_id]))」
# ---------- app/controllers/answers_controller.rb ---------- class AnswersController < ApplicationController before_filter :set_up # GET /answers # GET /answers.xml def index ##@answers = Answer.find(:all) @answers = @question.answers respond_to do |format| format.html # index.html.erb format.xml { render :xml => @answers } end end # GET /answers/1 # GET /answers/1.xml def show ##@answer = Answer.find(params[:id]) @answer = @question.answers.find(params[:id]) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @answer } end end # GET /answers/new # GET /answers/new.xml def new @answer = Answer.new respond_to do |format| format.html # new.html.erb format.xml { render :xml => @answer } end end # GET /answers/1/edit def edit ##@answer = Answer.find(params[:id]) @answer = @question.answers.find(params[:id]) end # POST /answers # POST /answers.xml def create @answer = Answer.new(params[:answer]) respond_to do |format| ##if @answer.save if @question.answers << @answer flash[:notice] = 'Answer was successfully created.' ##format.html { redirect_to(@answer) } format.html { redirect_to([@question, @answer]) } ##format.xml { render :xml => @answer, :status => :created, :location => @answer } format.xml { render :xml => @answer, :status => :created, :location => [@question, @answer] } else format.html { render :action => "new" } format.xml { render :xml => @answer.errors, :status => :unprocessable_entity } end end end # PUT /answers/1 # PUT /answers/1.xml def update ##@answer = Answer.find(params[:id]) @answer = @question.answers.find(params[:id]) respond_to do |format| ##if @answer.update_attributes(params[:answer].merge(:question_id=>params[:question_id])) # attr_protected :question_idを利用することで.merge(:question_id=>params[:question_id])は不要になった if @answer.update_attributes(params[:answer]) flash[:notice] = 'Answer was successfully updated.' ##format.html { redirect_to(@answer) } format.html { redirect_to([@question, @answer]) } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @answer.errors, :status => :unprocessable_entity } end end end # DELETE /answers/1 # DELETE /answers/1.xml def destroy ##@answer = Answer.find(params[:id]) @answer = @question.answers.find(params[:id]) @answer.destroy respond_to do |format| ##format.html { redirect_to(answers_url) } format.html { redirect_to(question_answers_url) } # 引数(@question)は省略してOK format.xml { head :ok } end end private def set_up @question = Question.find(params[:question_id]) end end
ビュー
- link_toのリンク先を、has_manyなルート設定に合わせて修正した。
<%# --------- app/views/answers/index.html.erb ---------- %> <h1>Listing answers</h1> <table> ...(中略)... <% for answer in @answers %> <tr> <td><%=h answer.name %></td> <td><%=h answer.body %></td> <td><%=h answer.question_id %></td> <%#=link_to 'Show', answer %> <td><%= link_to 'Show', [@question, answer] %></td> <%#=link_to 'Edit', edit_answer_path(answer) %> <td><%= link_to 'Edit', edit_question_answer_path(@question, answer) %></td> <%#=link_to 'Destroy', answer, :confirm => 'Are you sure?', :method => :delete %> <td><%= link_to 'Destroy', [@question, answer], :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %> </table> <br /> <%#=link_to 'New answer', new_answer_path %> <%= link_to 'New answer', new_question_answer_path %><%# 引数(@question)は省略してOK %>
- form_forでも配列を使って[@question, @answer]と書ける。
<%# --------- app/views/answers/edit.html.erb ---------- %> <h1>Editing answer</h1> <%#form_for(@answer) do |f| %> <% form_for([@question, @answer]) do |f| %> ...(中略)... <% end %> <%#=link_to 'Show', @answer %> <%= link_to 'Show', [@question, @answer] %> | <%#=link_to 'Back', answers_path %> <%= link_to 'Back', question_answers_path %><%# 引数(@question)は省略してOK %>
- new.html.erbは、上記のedit.html.erbとほとんど同じなので、サンプルコードは省略。
<%# --------- app/views/answers/show.html.erb ---------- %> ...(中略)... <%#=link_to 'Edit', edit_answer_path(@answer) %> <%= link_to 'Edit', edit_question_answer_path(@question, @answer) %> | <%#=link_to 'Back', answers_path %> <%= link_to 'Back', question_answers_path %><%# 引数(@question)は省略してOK %>
question側の変更
「質問」から「答え」へのリンクを作成してみた。
<%# ---------- app/views/questions/index.html.erb ---------- %> <h1>Listing questions</h1> ...(中略)... <% for question in @questions %> <tr> <td><%=h question.name %></td> <td><%=h question.body %></td> <td><%= link_to 'Answers', question_answers_path(question) %></td><%# <---追記 %> <td><%= link_to 'Show', question %></td> <td><%= link_to 'Edit', edit_question_path(question) %></td> <td><%= link_to 'Destroy', question, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %> ...(中略)...
- questionsコントローラーからanswersコントローラーへのパス指定(異なるコントローラーへのパス指定)については、引数を省略することは出来なかった。
<%# ---------- app/views/questions/show.html.erb ---------- %> ...(中略)... <%= link_to 'Edit', edit_question_path(@question) %> | <%= link_to 'Back', questions_path %> | <%= link_to 'Answers', question_answers_path(@question) %>
以上で完了。全く実用的じゃないけど、:has_manyなルート設定のパスの指定方法は分かってきた!