[Rails] 掲示板の一覧機能の作成

学習記録

はじめに

掲示板一覧機能を実装していきましょう。

作業の流れ

  • Boardモデルを作成
  • Fakerを使ってダミーデータを作成する
  • 掲示板一覧のルーティングを設定
  • 掲示板一覧のコントローラーを作成
  • 掲示板一覧画面のビューファイルを作成
  • ログインしていなければ掲示板一覧を表示できなくする
  • ログイン時の処理を修正する
  • タイムゾーンを日本時刻に設定する

Boardモデルを作成

掲示板のモデルを生成しましょう。今回、「title」「body」「userと紐づけるための外部キー」の3つのカラムが欲しいとします。

references型

user has_many bords の関係を実現するにはbords テーブルに user_id が必要です。

外部キーを設定する際は下記の書き方をします。

$ bundle exec rails g model board title:string body:text user:references

なぜなら、普通のrails g model board title:string body:text user_id:integer という書き方では、

  • インデックスが貼られない。
  • 外部キー制約が貼られない。

などの問題が生じるからです。

外部キーを作るときは基本的にはreferences型を使うということを肝に銘じておきましょう!

マイグレーションファイルを確認し、NOT NULL制約を追記

マイグレーションファイルと、modelファイルが作成されます。

ついでにNOT NULL制約を追加しておきます。

class CreateBoards < ActiveRecord::Migration[5.2]
  def change
    create_table :boards do |t|
      t.references :user, foreign_key: true
      t.string :title, null: false
      t.text :body, null: false

      t.timestamps
    end
  end
end

references型を指定して、rails g modelを実行するとモデルの他に、マイグレーションファイルにも特別な記述が入ります。

それが、t.referencesforeign_key: true です。

t.references :テーブル名,  foreign_key: true
  • t.references カラムが他のテーブルを参照していることを示します。カラム名は、指定したテーブルの前に id_ がつきます。(t.references :client であれば、カラム名は id_client になる)
  • foreign_key: true 外部キーであることを示しています。親テーブルに対象が存在しない場合、子テーブルへの保存ができないようにします。

また、reference型で作成すると、インデックスを自動で貼ってくれます。

BoardモデルにUserモデルとのアソシエーションと、バリデーションを設定

掲示板を作成したユーザーとの関連を、belongs_to で設定します。

また、タイトルと本文の必須入力と、文字数制限のバリデーションを設定します。

class Board < ApplocationRecord
	belongs_to :user

	validates :title, presence: true, length: { maximum: 255 }
	validates :body, presence: true, length: { maximum: 65_535 }
end

マイグレーションします。

$ bundle exec rails db:migrate

アソシエーションの使い方

このようにモデルを関連付けした事で、必要なオブジェクトを呼び出すメソッドが使えるようになります。具体的には、あるユーザーの投稿一覧が欲しい時には、

user.boardsというメソッドを使えば掲示板オブジェクトを取得できます。

また、この投稿に紐づいてるユーザをオブジェクトとして欲しい時には、boards.userというメソッドで取得することができるようになります。

Fakerを使ってダミーデータを作成する

10.times do
	User.create(
		first_name: Faker::Name.first_name,
		last_name: Faker::Name.last_name,
		email: Faker::Internet.email,
		password: '12345678',
		password_confirmation: '12345678'
	)
end

20.times do |index|
	Board.create(
		user: User.offset(rand(User.count)).first,
		title: "タイトル#{index}",
		body: "本文#{index}"
	)
end

こちら別記事にしましたのでご参照ください。

掲示板一覧画面のルーティングを設定

掲示板一覧画面のルーティングを追記します。

Rails.application.routes.draw do
	root 'static_pages#top'

	get 'login', to: 'user_sessions#new'
	post 'login', to: 'user_sessions#create'
	delete 'logout', to: 'user_sessions#destroy'

	resources :users, only: %i[new create]
	resources :boards, only: %i[index]
end

掲示板一覧のコントローラーを作成

$ bundle exec rails g controller boards

生成されたBoardsControllerに、掲示板の一覧をDBから取得する処理を追加します。

class BoardController < ApplicationController
	def index
		@boards = Board.all.includes(:user).order(created_at: :desc)
	end
end

includes メソッドは、関連付いたモデルのデータを先に取得するメソッドです。このメソッドを使うことで、Boardモデルからデータを取得する際に、関連するUserモデルのデータもまとめて取得してくれます。なぜこのような書き方をするのかというと、N+1問題を起こさないためです。N+1問題については別記事で説明しています。

掲示板一覧画面のビューファイルを作成

掲示板一覧画面の表示方法は、少々複雑なことをします。大本のindex.html.erbを作成して、単一の掲示板表示部分は_board.html.erbというパーシャルファイルを作成し、index.html.erbから繰り返し呼び出して掲示板一覧を表示する設計にします。

このときN +1問題が起きないように注意して実装していきましょう。

N+1問題を回避する具体的な方法は別の記事にまとめましたので、こちらを参照してください。

大本の掲示板一覧画面を作成

<div class="container pt-3">
	<div class="row">
		<div class="col-lg-10 offset-lg-1">
			<!-- 検索フォーム --!>
			<form>
				<div class="input-group mb-3">
				<input class="form-control" placeholder="検索ワード" type="search"/>
				<div class="input-group-append">
					<input type="submit" value="検索" class="btn btn-primary"/>
				</div>
			</div>
		</form>
	</div>
</div>

<!-- 掲示板一覧 --!>
<div class="row">
	<div class="col-12">
		<div class="row">
			<% if @boards.present? %>
				<%= render @boards %>
			<% else %>
				<p><% t('.no_result') %></p>
			<% end %>
		</div>
	</div>
</div>
</div>

コードを紐解く

<% if @boards.present? %>
  <%= render @boards %>
<% else %>
  <p><% t('.no_result') %></p>
<% end %>

掲示板が存在しない場合「掲示板がありません。」と表示させています。

単一の掲示板を表示するパーシャルを作成

<div class="col-sm-12 col-lg-4 mb-3">
  <div id="board-id-<%= board.id %>">
    <div class="card">
      <%= image_tag 'board_placeholder.png', class: 'card-img-top', size: '300x200' %>
      <div class="card-body">
        <h4 class="card-title">
          <a href="#">
            <%= board.title %>
          </a>
        </h4>
        <div class='mr10 float-right'>
          <a href="#"><%= icon 'fas', 'trash', class: 'pr-1' %></a>
          <a href="#"><%= icon 'fa', 'pen' %></a>
        </div>
        <ul class="list-inline">
          <li class="list-inline-item">
            <%= icon 'far', 'user' %>
            <%= board.user.decorate.full_name %>
          </li>
          <li class="list-inline-item">
            <%= icon 'far', 'calendar' %>
            <%= l board.created_at, format: :long %>
          </li>
        </ul>
        <p class="card-text"><%= board.body %></p>
      </div>
    </div>
  </div>
</div>

ヘッダーに掲示板一覧画面へのリンクを追記

<div class="dropdown-menu dropdown-menu-right">
  <%= link_to t('boards.index.title'), boards_path, class: 'dropdown-item' %>
  <%= link_to '掲示板作成', '#', class: 'dropdown-item' %>
</div>

ロケールファイルに追記

ja:
  activerecord:
    attributes:
      board:
        title: 'タイトル'
        body: '本文'

ログインしていなければ掲示板一覧を表示できなくする

ユーザーがログインしていなければ、掲示板一覧を表示できないようにして、ログイン画面へリダイレクトさせましょう。

ここでは、require_loginというメソッドをアクションの前に呼ばれるフィルタとして登録します。アクションの前に呼ばれるフィルタを登録するには、before_actionメソッドを使います。

class ApplicationController < ActionController::Base
  add_flash_types :success, :info, :warning, :danger
  before_action :require_login

  private

  def not_authenticated
    redirect_to login_path, warning: t('defaults.message.require_login')
  end
end
  • require_loginメソッド

sorceryで定義されています。ログイン状態を判定し、ログインしていなければ結果的にnot_authenticated メソッドを実行します。

  • not_authenticated メソッド

このメソッドもsorceryで定義されているメソッドです。

デフォルトではroot_pathへリダイレクトさせているので、login_pathへと設定し直しています。

また、warning を使って「ログインしてください」というフラッシュメッセージが出るようにします。

未ログイン状態でもトップページ、ユーザー新規登録画面、ログイン画面を表示できるようにする

ApplicationControllerbefore_action で、require_login のフィルタを登録したため、未ログインでもアクセス可能なアクションでは、require_action をskipするように設定します。

トップページのフィルタをスキップする

class StaticPagesController < ApplicationController
  skip_before_action :require_login, only: %i[top]

  def top; end
end

ログイン画面のフィルタをスキップする

ログイン後の遷移先を掲示板一覧に変更しておきます。

class UserSessionsController < ApplicationController
   skip_before_action :require_login, only: %i[new create]
 
   def new; end
 
   def create
     @user = login(params[:email], params[:password])
     if @user
       redirect_back_or_to boards_path, success: t('.success')
     else

新規登録画面のフィルタをスキップする

class UsersController < ApplicationController
  skip_before_action :require_login, only: %i[new create]

これでログイン成功後は掲示板一覧ページに遷移し、非ログイン時はログイン画面にダイレクトされるようになりました。

タイムゾーンを日本時刻に設定する

config.time_zone = "Tokyo"
config.active_record.default_timezone = :local

config.time_zone Rails自体のアプリケーションの時刻の設定

config.active_record.default_timezone DBを読み書きする際に、DBに記録されている時間をどのタイムゾーンで読み込むかの設定

終わりに

非常に実装料が多かったですね。今回のポイントは以下3点です。お疲れさまです。

  • Fakerの使い方
  • N+1問題
  • renderの省略記法

参考記事

Railsのタイムゾーン設定について
https://github.com/Sorcery/sorcery/blob/6fdc703416b3ff8d05708b05d5a8228ab39032a5/lib/sorcery/controller.rb

コメント

タイトルとURLをコピーしました