初心者向け : Railsログイン機能をつけてQAサイトを作る 4 -タグ付け機能-

「初心者向け : Railsログイン機能をつけてQAサイトを作る 4 -タグ付け機能-」のアイキャッチ画像

※ rails6に対応させてあります

初心者向け : Railsログイン機能をつけてQAサイトを作る 1 -ログイン機能+質問機能-
初心者向け : Railsログイン機能をつけてQAサイトを作る 2 -Bootstrap+UI修正-
初心者向け : Railsログイン機能をつけてQAサイトを作る 3 -回答機能+リアクション機能+ベストアンサー機能-
初心者向け : Railsログイン機能をつけてQAサイトを作る 4 -タグ付け機能-
初心者向け : Railsログイン機能をつけてQAサイトを作る 5 -管理画面機能-
初心者向け : Railsログイン機能をつけてQAサイトを作る 6 -検索機能-

今回は質問に対してタグをつけることができるようにします

質問内容が

  • 動物
  • 食べ物
  • スポーツ

などわかりやすくするための機能です

データベース関係性

データベースの関係性はこのような形となります

通常であれば、Question modelからTag modelをhas_manyにして
たくさんのタグを作成したいところですが、
タグはQuestionを作成するユーザーごとに違うものを適用するか、
それとも全てのユーザーが同じタグを利用して管理をしやすくする、
どちらが良いでしょうか?

保守面で考えると後者が間違いなく便利です
なのでほとんどのシステムでは同じタグを質問ごとに紐づける
多対多というデータベースのリレーションが行われています

今まではQuestionに紐づくAnswersを全て取得して表示していました
これは一対多です。

多対多とはTagとQuestionがそれぞれ複数要素を持ち合っている状態です
複数持ち合うには、デーブルとテーブルの間にさらにテーブルを作成して
そこへTagのidとQuestionのidを入れておくことで関係性を持たせることができます

※テーブルとテーブルの間のテーブルのことを中間テーブルと呼びます
慣習として、テーブル(Question)とテーブル(Tag)の名前をつけることがあり、
今回はQuestionTagと名前をつけました。

TagとQuestionTag modelを作成する

いつも通りscaffoldで作りたいところですが、
コントローラーやhtml等は必要ないので、modelのみで行きます

$ rails g model Tag name
$ rails g model QuestionTag tag:references question:references

次はモデルにリレーションを与えていきます

question.rb

class Question < ApplicationRecord
  belongs_to :user
  has_many :answers, dependent: :destroy
  # question_tagsをたくさん持っている
  has_many :question_tags
  # question_tagsをたくさん持っていて、question_tagsを介してtagsをたくさん持っている
  has_many :tags , through: :question_tags
end

tag.rb

class Tag < ApplicationRecord
  # question_tagsをたくさん持っている
  has_many :question_tags, dependent: :destroy
  # question_tagsをたくさん持っていて、question_tagsを介してquestionsをたくさん持っている
  has_many :questions , through: :question_tags
end

question_tag.rb

class QuestionTag < ApplicationRecord
  belongs_to :tag
  belongs_to :question
end

最後にデータベースを適用させます

$ rails db:migrate

Tagにデータを追加する

コンソールでデータを追加します
結果の部分は見にくくなってしまうので削除しました

$ rails c
irb(main):001:0> Tag.create(name:"動物")
irb(main):001:0> Tag.create(name:"スポーツ")
irb(main):001:0> Tag.create(name:"ご飯")
irb(main):001:0> Tag.create(name:"その他")

こんな感じでデータを4つぐらい登録して、最後に確認を行いましょう

irb(main):010:0> Tag.all
  Tag Load (0.2ms)  SELECT  "tags".* FROM "tags" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Tag id: 1, name: "動物", created_at: "2019-07-27 03:38:38", updated_at: "2019-07-27 03:38:38">, #<Tag id: 2, name: "スポーツ", created_at: 07-27 03:39:08", updated_at: "2019-07-27 03:39:08">, #<Tag id: 3, name: "ご飯", created_at: "2019-07-27 03:39:17", updated_at: "2019-07-27 03:39:17">, #<Tag id: 4, name: の他", created_at: "2019-07-27 03:39:23", updated_at: "2019-07-27 03:39:23">]>

無事にデータが入ってます

Questionコントローラーとviewsに適用する

new, edit question_paramsを修正

class QuestionsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_question, only: [:show, :edit, :update, :destroy]

  def index
    # ユーザータイプによって取得内容を変更
    @questions = current_user.questions if current_user.role == '質問者'
    @questions = Question.all if current_user.role == '回答者'
  end

  def index
    @questions = current_user.questions.all
  end

  def show
  end

  def new
    # 新規作成画面でタグを表示するため
    @tags = Tag.all
    @question = Question.new
  end

  def edit
    # 修正画面でタグを表示するため
    @tags = Tag.all
  end

  def create
    @question = current_user.questions.build(question_params)

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

  def update
    respond_to do |format|
      if @question.update(question_params)
        format.html { redirect_to @question, notice: 'Question was successfully updated.' }
        format.json { render :show, status: :ok, location: @question }
      else
        format.html { render :edit }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @question.destroy
    respond_to do |format|
      format.html { redirect_to questions_url, notice: 'Question was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

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

  def question_params
    # ここを修正
    params.require(:question).permit(:user_id, :title, :body, :best_answer_id, {:tag_ids => []})
  end
end

views/questions/_form.html.erbをこのように修正

<%= form_with(model: question, local: true) do |form| %>
  <% if question.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>

      <ul>
        <% question.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <!--ここを追加-->
  <div class="field">
    <% @tags.each do |t| %>
      <%= form.label t.name %>
      <%= check_box_tag "question[tag_ids][]", t.id, @question.tags.include?(t) %>
      <br/>
    <% end %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

修正後、質問画面ではタグを選択できるようになっています

それでは適当にデータを作成して保存してみましょう
※必ずどれかのタグにチェックを入れてください

次はタグを確認できるように修正します

views/questions/show.html.erb
全部表示すると長くなるので、header部分のみ表示しています
これ以外には修正はありません

<header class="jumbotron my-4">
  <h2 class="card-title"><%= @question.title %></h2>
  <!--ここにボタン形式でタグを表示-->
  <% @question.tags.each do |tag| %>
    <button class="btn btn-info"><%= tag.name %></button>
  <% end %>
  <!--ここまで-->
  <% if current_user.role == '回答者' %>
    <%= link_to '回答する!', question_answers_path(@question.id), class: 'btn btn-primary btn-lg' %>
  <% end %>
</header>

それでは確認しましょう

タグ追加後はタイトルのすぐ下にタグが表示されています!

ちなみにこのタグですが、複数追加すれば複数で表示されます

タグは検索できるとより便利になるので、次の次ぐらいからやります