rails beginner

→Railsビギナー 第0章はこちら

→Railsビギナー 第1章はこちら

→Railsビギナー 第2章はこちら

 

→Railsビギナー 免責事項 に同意いただける方のみ、教材の利用をお願いいたします。

 

今のままでは質問があるだけで、質問に対する回答がありません。

そこで次は回答機能を作りましょう。

 

まずはanswerモデルを作ってください。

ターミナルに下記のコマンドを入力。

rails g model answer

作成されたマイグレーションファイルを開いて、下記のコードを追加してください。

class CreateAnswers < ActiveRecord::Migration[6.0]
  def change
    create_table :answers do |t|
      t.references :user, foreign_key: true
      t.references :question, foreign_key: true
      t.text :body
      t.timestamps
    end
  end
end

第2章ではquestionsテーブルに、後からuser_idカラムを付け加えました。

今回は最初からuser_idカラムを作ります。

そしてuser_idカラムだけでなく、question_idカラムも持たせます。

 

1人のユーザーは多くの回答を持ちますね?

そして1つの質問も多くの回答を持ちます。

ユーザーと回答は1対多の関係になり、質問と回答も1対多の関係になります。

そのためanswersテーブルには、user_idカラムとquestion_idカラム、そして本文であるbodyカラムを持たせました。

※今回は回答なのでtitleカラムは不要です。

 

書けたら下記コマンドをターミナルに入力してください。

rails db:migrate

次はモデルの関連付けを行いましょう。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  has_many :questions, dependent: :destroy
  has_many :answers, dependent: :destroy
end
class Question < ApplicationRecord
  has_many :answers, dependent: :destroy
  belongs_to :user

  validates :title, presence: true, length: { maximum: 100 }
  validates :body, length: { maximum: 3000 }
end
class Answer < ApplicationRecord
  belongs_to :user
  belongs_to :question
end

バリデーションも書いておきます。

class Answer < ApplicationRecord
  belongs_to :user
  belongs_to :question

  validates :body, presence: true, length: { maximum: 1000 }
end

次にコントローラーを作成。

rails g controller answers

ルーティングも忘れず書いておきましょう。

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

  get "/users/:id", to: "users#show"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: "questions#index"
  resources :questions
  resources :answers
end

それでは回答するためのフォームを作ります。

 

今回は質問詳細ページに回答できるフォームを作ります。

イメージはこんな感じ。

railsビギナー

質問のタイトルと本文の下に、回答フォームがある感じです。

 

app/views/questions/show.html.erbに、フォームを作るコードを追加します。

<p><%= @question.title %></p>
<p><%= @question.body %></p>
<% if @question.user_id == current_user.id %>
  <%= link_to("編集", "/questions/#{@question.id}/edit") %>
  <%= link_to("削除", "/questions/#{@question.id}", method: :delete, data: {confirm: "本当に削除しますか?"}) %>
<% end %>
<div>
  <%= form_with(model: @answer, local: true) do |f| %>
    <div>
      <%= f.label :body, "回答" %><br>
      <%= f.text_area :body %>
    </div>
    <%= f.submit %>
  <% end %>
</div>

今回作ったフォームは回答の新規作成なので、Answer.newが代入された@answerを使います。

しかしながらapp/views/questions/show.html,erbに対応する、questionsコントローラーのshowアクションには、Answer.newを代入した@answerは存在しません。

そのためにはquestionコントローラーのshowアクションに下記のコードを追加します。

class QuestionsController < ApplicationController
  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
    @answer = Answer.new
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/new")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "編集しました"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  def destroy
    @question = Question.find(params[:id])
    @question.destroy
    flash[:notice] = "成功!"
    redirect_to("/questions")
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end

    def ensure_correct_user
      @question = Question.find_by(id: params[:id])
      if @question.user_id != current_user.id
        flash[:alert] = "権限がありません"
        redirect_to("/questions/#{@question.id}")
      end
    end
end

これでフォームはできます。

それでは質問の新規作成と同じく、フォームに入力された値を、データベースに保存する処理を書きましょう。

answersコントローラーのcreateアクションを作ります。

それではapp/controllers/answers_controller.rbを開いて、下記のコードを追加してください。

class AnswersController < ApplicationController
  before_action :authenticate_user!

  def create
    @answer = Answer.new(answer_params)
    @answer.user_id = current_user.id
    if @answer.save
      flash[:notice] = "成功!"
      redirect_to("/questions/???")
    else
      flash.now[:alert] = "失敗!"
      render "questions/show"
    end
  end

  private
    def answer_params
      params.require(:answer).permit(:body)
    end
end

ログインしているユーザーのみ回答OKにしています。

そしてここで問題発生です。

answersテーブルは、bodyカラムと、user_idカラム、question_idカラムで構成されています。

つまりquestion_idも必要となりますが、questionのidは、どこからとってきたらいいのでしょうか?

URLを見ても、フォームから送られてくる値を見ても、question_idの手掛かりになりそうなものはありません。

 

またquestionのidが分からないので、redirect_toで、質問の詳細ページに飛ばすことができません。

 

そこでURLをネストしてあげます。

ネスト?

 

今のcreateアクションのURLは下記の形式ですよね。

localhost:3000/answers

 

これを下記のような形式にしてあげます。

localhost:3000/questions/2/answer

 

これでidが2のquestionに関連したanswerだと、URLを見れば分かるようになります。

 

このようなURLを作るために、config/routes.rbを下記のように書き換えます。

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

  get "/users/:id", to: "users#show"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  resources :questions do
    resources :answers, only: [:create]
  end
end

今回、answersコントローラーには、createアクションしか作られないので、only: [:create]をつけています。

ターミナルにrails routesを入力して、URLを確認してみましょう。

rails routes

注意深く見ると、/questions/:question_id/answersというURLが生成されているはずです。

 

createアクションを下記のように書き換えてあげます。

class AnswersController < ApplicationController
  before_action :authenticate_user!

  def create
    @answer = Answer.new(answer_params)
    @answer.user_id = current_user.id
    @answer.question_id = params[:question_id]
    if @answer.save
      flash[:notice] = "成功!"
      redirect_to("/questions/#{params[:question_id]}")
    else
      @question = Question.find(params[:question_id])
      flash.now[:alert] = "失敗!"
      render "questions/show"
    end
  end

  private
    def answer_params
      params.require(:answer).permit(:body)
    end
end

まずこの回答に関連する質問のidは、params[:question_id]で、URLから取得できます。

いつも使っているparams[:id]ではなく、なぜparams[:question_id]なのか?

もう一度、rails routesで、answersコントローラーのcreateアクションに対応するURLを確認してみましょう。

/questions/:question_id/answers(.:format)となっていますね。

 

このURLのquestionのidが入る部分は、:question_idというキーだと指定されています。

つまり {question_id: 1} という形で保持されます。

そのためparams[:question_id]と書くことで、questionのidが取得できるという訳です。

 

質問のidが分かったので、redirect先も指定できます。

あと @question = Question.find(params[:question_id]) という記述も付け加えました。

これは一体何か?

このcreateアクションは、renderで app/views/questions/show.html.erb を呼び出していますね?

app/views/questions/show.html.erbでは、@questionを使っています。

しかしcreateアクションには、@questionがありませんでした。

そのためこのままrenderで app/views/questions/show.html.erb を呼び出すとエラーが発生します。

そこでエラーが出ないように、@questionを定義してあげたわけです。

 

次はフォームも書き換えましょう。

<p><%= @question.title %></p>
<p><%= @question.body %></p>
<% if @question.user_id == current_user.id %>
  <%= link_to("編集", "/questions/#{@question.id}/edit") %>
  <%= link_to("削除", "/questions/#{@question.id}", method: :delete, data: {confirm: "本当に削除しますか?"}) %>
<% end %>
<div>
  <%= form_with(model: [@question, @answer], local: true) do |f| %>
    <div>
      <%= f.label :body, "回答" %><br>
      <%= f.text_area :body %>
    </div>
    <%= f.submit %>
  <% end %>
</div>

どの質問に対応する回答なのかを指定するために、form_with(model: [@question, @answer])と記述します。

これでネストしたURLに対応したフォームができました。

しかし今のままでは作成した回答が表示されません。

app/views/questions/show.html.erbに回答を表示してあげましょう。

<p><%= @question.title %></p>
<p><%= @question.body %></p>
<% if @question.user_id == current_user.id %>
  <%= link_to("編集", "/questions/#{@question.id}/edit") %>
  <%= link_to("削除", "/questions/#{@question.id}", method: :delete, data: {confirm: "本当に削除しますか?"}) %>
<% end %>
<div>
  <%= form_with(model: [@question, @answer], local: true) do |f| %>
    <div>
      <%= f.label :body, "回答" %><br>
      <%= f.text_area :body %>
    </div>
    <%= f.submit %>
  <% end %>
</div>
<hr>
<div>
  <% @question.answers.each do |answer| %>
    <p><%= answer.body %></p>
  <% end %>
</div>

これでこの質問に関連している回答がすべて表示されます。

 

ブラウザを開いて「http://localhost:3000/questions/2」にアクセスして、回答を作成してみましょう。

railsビギナー

作成すると…

railsビギナー

このような表示になったと思います!

回答が表示されています。

質問サイトっぽい雰囲気が出てきましたね!

Railsビギナー 第3章「回答機能作成」まとめ

rails beginner

お疲れ様でした!

第3章終了です。

ここまでこなせたあなたは質問サイトの、基本的な作り方が分かったはずです。

かなりレベルアップした感じもあるのでは?

ぜひここで学習を終わらせず、さらにハイレベルな教材にも挑戦してくださいね。

おすすめはやはり「Railsチュートリアル」です。

→Railsチュートリアル

 

現在、第4章も作成中です。

作成できたらTwitterで報告するので、ぜひフォローしてくださいね!

 

フリーランスとして仕事をとりたい方は、ぜひ下記noteをチェックしてください。

5~6年しっかりとフリーランスを経験した、僕のリアルなノウハウが詰まっています。

 

けっこう頑張って作ったので、ぜひ熱い感想をお聞かせください!

Twitterで「#Railsビギナー」をつけて、感想をつぶやいていただくと、見に行けるので励みになります。

「いずれはプログラミング初心者の学習王道ルートが、『プロゲート』 → 『Railsビギナー』 → 『Railsチュートリアル』になったらいいなぁ…」という願いも抱いています!

それではここまでお付き合いいただき、ありがとうございました!