→はじめから

”社会人”を”はじめから”した人のあしあと

【技術】rails よくある 投稿にコメントつけるときのバリデーション処理

こんにちは、技術ネタを書くのも楽しいですね。
今回も初学者向けのネタを書いていきます。

今回の内容は私が独学を初めていろんな教材(私の場合は◯ットインストール)
を見ながらやっていた時に
バーされていなかった(初学者にとってややこしいから割愛された?)
ところを初学者目線で書ければと思います。


今回は読んだままやれば
できるように書きますのでどうぞお楽しみください!
ご指摘どんどん受け付けます!

概要

ネストされたテーブルの入力フォームでエラー表示をする。
例)投稿テーブルPostsにコメントテーブルCommentsが親子関係のリレーションで定義されている状態で
投稿詳細画面にコメント入力フォームをおいた時にバリデーションエラーが出るようにする。

下記のような状態
f:id:travy:20180825180934p:plain

前提

  • rails sでローカル環境にサービスの画面が出力できる
  • 初学者のかた

よくあるオンライン学習教材で勉強されている方だと理解が早いと思います。
*今回はコメントの作成(create)に焦点を絞っています。更新(update)がしてたい人は
こちらの記事を参考に修正を加えれば実装できるはずです。

バージョン
rails 4.2.10
ruby 2.3.1

準備

まずはこれから

$rails new app_name

scaffoldで投稿モデルとコメントモデルを作っちゃいましょう
CommentモデルはPostモデルに子として定義しておきましょう

$rails g scaffold Post title:string body:text
$rails g scaffold Comment body:string post:references

マイグレート

$bin/rake db:migrate

各モデルにリレーションを定義しましょう

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
  validates :body, presence: true
end

投稿に対して複数のコメント属するようにします
今回コメントを入力した時にエラーを出したいので
commentモデルのbodyカラムにはvalidationを入れておきましょう

config/routes.rbでcommentをpostにネストしておきます

  resources :posts do
    resources :comments
  end

実装

UI(入力フォーム)

先の画像であったようなフォームを作りましょう
投稿の詳細(posts/show.html.erb)に作りたい
Point:form_forに与える引数でコメントは投稿にネストしているので
第1引数に今の投稿の情報を持った@post
第2引数に@post.comments.newで作った@commentを入れておきます
*あとでコントローラーでこの処理は書きます

<%= form_for([@post,@comment]) do |f| %>
  <% if @comment.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:</h2>

      <ul>
      <% @comment.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
 ・
 ・
 ・
<% end %>
コントローラーの処理

PostとCommentでそれぞれ処理を書いていきましょう

投稿詳細のページを表示する時に使われるshowアクションで@postに紐づいた@commentをnew
を作成
*UIで書いた@commentはこれと同じ

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
 ・
 ・
 ・
  def show
    @comment = @post.comments.new
  end
 ・
 ・
 ・

次はCommentのcreateアクション
(必要ないものは削除したので丸ごと貼っておきます)
こちらはscaffoldで作られたコードとほとんど変わりないので解説は割愛で

class CommentsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :create, :update, :destroy]

  def create
    @comment = @post.comments.new(comment_params)
    respond_to do |format|
      if @comment.save
        format.html { redirect_to @post, notice: 'Comment was successfully created.' }
      else
        format.html { render 'posts/show' }
      end
    end
  end

  private
    def set_post
      @post = Post.find(params[:post_id])
    end
    # Use callbacks to share common setup or constraints between actions.

    # Never trust parameters from the scary internet, only allow the white list through.
    def comment_params
      params.require(:comment).permit(:body)
    end
end
コメントを表示

投稿詳細画面にコメント一覧を表示しましょう
コメント入力フォームはrennderして使っています。
Point:postsのshowアクションですでにDBに保存されていなく情報が空の
コメントオブジェクトを作っているので下記の処理を入れて表示されないようにします
この処理がないと下記の画像のようになります(かっこ悪いですね)
*メソッドの意味等は調べてみましょう

  <% next unless comment.persisted? %>
・
・
・
<%= render 'comments/form' %>

<% @post.comments.each do |comment| %>
  <% next unless comment.persisted? %>
  <div class="comment-item">
    コメント<br />
    <%= comment.body %>
  </div>
<% end %>
・
・
・

f:id:travy:20180825184357p:plain

さて!これでコメント入力に対するエラーは実装できました!!

理解を深めるために

ちなみに実装方法は多岐にわたるので今回の方法だけが正解ではありません。
私が学習した時はコメント入力フォームを作る
>||form_for|

の引数に
>|ruby|[@post, @post.comments.build]|

と書いていました。
今回@post.comments.buildの代わりにコントローラーであらかじめ作成しておいた
@commentを使っていたと思います。

両者の挙動はかなり違うものになります。

それはなぜでしょうか?
自分でコードを変えながら答えを探るとデバッグの練習にもなると思います。
ヒント:両者とも入力対象のカラムが1つでvalidationがpresenceの場合にのみ
挙動が同じになります。
そしてコメントに文字制限がある場合、コメントにタイトルの入力が増えた場合は
挙動が変わるはずです。



















では回答に

form_for[@post,@post.comment.build]

で実装した方で下記を実行した場合

ヒント:両者とも入力対象のカラムが1つでvalidationがpresenceの場合にのみ
挙動が同じになります。
そしてコメントに文字制限がある場合、コメントにタイトルの入力が増えた場合は
挙動が変わるはずです。

バリデーションエラーの表示は出ないはずです。
DBに保存する際に失敗はしていますがユーザーがみる画面ではエラーが伝えられないようになってしまいます。

理由はコメントをcreateする時、失敗すると@commentの[:errors]にエラー情報が格納されてrenderした時に
viewが呼び出されてエラー表示の処理内に[:errors]があるか確認するものがあります。
しかし、viewを呼び出すときに@post.comment.buildでエラー情報のないcommentオブジェクトを新たに作成してしまうので
エラーがない状態と判断されてしまう結果、エラー表示は出ないことになります。

わかりにくかもしれませんが今回のコードはgithubに公開してあるのでcloneして遊んでみてください。

まとめ githubリポジトリ

github.com

私がこちらのバグとはかなり時間をかけて闘いました、もちろんググったりもしましたし
ログで送信パラーメーターを調べたりと初心者なりに踏み込んだデバッグ作業をしいられました笑
結果的にpresenceをチェックするバリデーションのエラー表示を実装できましたが、これがそれ以外の
バリデーションに対応できないことを知ってショックを受けました。
同時に時間をかけて闘ってきたバグをやっつけて目的の処理ができた時にもう一度それが
本当に正しい挙動かどうかを一度落ち着いて考える必要があると感じました。

ここまで読んでいただいた初学者のかたの役に立てれば幸いです。

記事に対するご指摘アドバイス等はどしどし受け付けますので
私のTwitterにDMもしくはこちらの記事にコメントいただければ幸いです。

それでは!