Chapter 20

4-1. Post モデル作成

Masuyama
Masuyama
2022.10.15に更新

Post モデル作成

いよいよ、TechLog のメイン機能である学習ログを投稿・閲覧する機能を作成していきます。

本カリキュラムは Rails の基礎は学んでいることを前提としており、
基本的な機能の実装については細かい解説はしていませんのでご注意ください。

それよりもむしろ、どうやって機能を正確にテストするかという「実践的な開発」部分に着目していただければと思います。

モデル作成

TechLog では、学習ログの投稿一つひとつを Post というモデルオブジェクトとして扱うことにします。

rails g コマンドで Post モデル関連のファイルを作成しましょう。

$ bundle exec rails g model Post
      invoke  active_record
      create    db/migrate/20220727120112_create_posts.rb
      create    app/models/post.rb
      invoke    rspec
      create      spec/models/post_spec.rb
      invoke      factory_bot
      create        spec/factories/posts.rb

次は、作られたファイルからテーブルを準備(マイグレーション)します。

マイグレーション

マイグレーションファイルの編集

Post モデル、もとい posts テーブルに持たせたいカラム名をマイグレーションファイル内で定義します。

先ほど作成されたファイルのうち db/migrate/xxxxxxxxxx_create_posts.rb を開き、以下のように編集してください。

class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title, null: false # タイトルカラム
      t.string :content, null: false # 本文カラム
      t.references :user, foreign_key: true, null: false # 外部キー (User)
      t.timestamps
    end
  end
end

Post はユーザーが作るものですので、必ずいずれかの User に紐づきます。
そのため、User を外部キーとして設定しています。

後ほど、Post モデルに User との紐付けを定義することで、Post に紐づく User、そして User に紐づく Post 情報を取得できるようにします。

マイグレーション実行

では、先ほど編集したマイグレーションファイルの内容をデータベースに反映していきましょう。
まずは development からです。

$ bundle exec rails db:migrate
== 20220727120112 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0015s
== 20220727120112 CreatePosts: migrated (0.0015s) =============================

同じく test の方もマイグレーションしましょう。

$ RAILS_ENV=test bundle exec rails db:migrate
== 20220727120112 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0007s
== 20220727120112 CreatePosts: migrated (0.0008s) =============================

エラーなく、マイグレーションが完了することを確認してください。

テスト作成

では、テスト駆動開発スタイルに従い、あるべき仕様を先にテストで定義しましょう。

以下のような動作を仕様として定義し、それぞれテストを追加していきます。

  • Post モデル
    • バリデーションの検証
      • 正常系
        • 正しいパラメータを渡せば
      • 異常系
        • title が空の場合は無効
        • title が 100 文字を超える場合は無効
        • content が空の場合は無効
        • content が 1000 文字を超える場合は無効
        • user_idが空の場合
    • Post が持つ情報の検証
      • 作成した Post が title を持つこと
      • 作成した Post が content を持つこと
      • 作成した Post から紐づく User 情報を取得できること
  • User モデル
    • 紐づく Post 情報を取得できること

では、テストを作成していきます。

FactoryBot の準備

Post モデルに関するテストをスムーズに作成するため、FactoryBot を用いてテスト内で Post を準備しやすくしておきます。
rails g コマンドで作成された spec/factories/post.rb を以下のように編集してください。

spec/factories/post.rb

FactoryBot.define do
  factory :post do
    title { 'タイトル1' }
    content { '本文1' }

    association :user, factory: :user
  end
end

association :user, factory: :user の1行により、同時に Post に紐づく User も作成できるように設定していることに注意してください。

これで、RSpec ファイル内で create(:post) という1行だけで Post を作成できるようになりました。
次は、実際に Post モデルのテストを準備しましょう。

Post モデルのテスト

spec/models/post_spec.rb

require 'rails_helper'

describe Post do
  before { @user = create(:user) } # 事前にユーザーを作成

  let(:title) { 'テストタイトル' }
  let(:content) { 'テスト本文' }
  let(:user_id) { @user.id } # 作成したユーザーのIDを外部キーに設定

  describe 'バリデーションの検証' do
    let(:post) { Post.new(title: title, content: content, user_id: user_id) }

    context '正常系' do
      it '有効である' do
        expect(post.valid?).to be(true)
      end
    end

    context '異常系' do
      context 'titleが空の場合' do
        let(:title) { nil }
        it '無効である' do
          expect(post.valid?).to be(false)
          expect(post.errors[:title]).to include('が入力されていません。')
        end
      end

      context 'titleが100文字を超える場合' do
        let(:title) { 'あ' * 101 }
        it '無効である' do
          expect(post.valid?).to be(false)
        end
      end

      context 'contentが空の場合' do
        let(:content) { nil }
        it '無効である' do
          expect(post.valid?).to be(false)
          expect(post.errors[:content]).to include('が入力されていません。')
        end
      end

      context 'contentが1000文字を超える場合' do
        let(:content) { 'あ' * 1001 }
        it '無効である' do
          expect(post.valid?).to be(false)
        end
      end

      context 'user_idが空の場合' do
        let(:user_id) { nil }
        it '無効である' do
          expect(post.valid?).to be(false)
          expect(post.errors[:user]).to include('が入力されていません。')
        end
      end
    end
  end

  describe 'Postが持つ情報の検証' do
    before { create(:post, title: title, content: content, user_id: user_id) } # Post を作成

    subject { described_class.first }

    it 'Postの属性値を返す' do
      expect(subject.title).to eq('テストタイトル')
      expect(subject.content).to eq('テスト本文')
      expect(subject.user_id).to eq(@user.id)
    end
  end
end

以上が Post モデルのテストです。
バリデーションのエラーメッセージは日本語ですので、後ほど翻訳ファイル (config/locales/devise.views.ja.yml) も修正する必要があります。

User モデルのテスト

次は User モデルに紐づく Post を取得できる仕様を満たすテストを追加します。

User モデルのテストは作成済みですので、その中にテストを追加・修正してあげます。

spec/models/user_spec.rb

...
  describe '.first' do
    before do
      @user = create(:user, nickname: nickname, email: email) # 修正
      @post = create(:post, title: 'タイトル', content: '本文', user_id: @user.id) # 修正
    end

    subject { described_class.first }

    it '事前に作成した通りのUserを返す' do
      expect(subject.nickname).to eq('テスト太郎')
      expect(subject.email).to eq('test@example.com')
    end

    ####### ここから追加 #######
    it '紐づくPostの情報を取得できる' do
      expect(subject.posts.size).to eq(1)
      expect(subject.posts.first.title).to eq('タイトル')
      expect(subject.posts.first.content).to eq('本文')
      expect(subject.posts.first.user_id).to eq(@user.id)
    end
    ####### ここまで追加 #######
  end
...

before ブロックの中で、User に紐づく Post を作成しています。

また、User は複数の Post を持つ場合があるので
User.post ではなく User.posts と、複数の Post を取得することに注意してください。

モデルの修正

では、テストが通るように Post モデルと User モデルを修正していきましょう.

Post モデルの修正

Post モデルを定義するファイルを以下のように修正します。

app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user

  validates :title, presence: true, length: { maximum: 100 }
  validates :content, presence: true, length: { maximum: 1000 }
end

belongs_to により、Post に対して単一の User を紐付けています。

User モデルの修正

次に User モデルを定義するファイルを以下のように修正します。

app/models/user.rb

class User < ApplicationRecord
  has_many :posts # 追加

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :nickname, presence: true, length: { maximum: 20 }
end

has_many により、User に対して複数の Post を紐付けています。

これでモデル自体の修正は完了です。

翻訳ファイル修正

先ほどテスト修正時に伝えた通り、バリデーション時のエラー内容に対する日本語訳を定義する必要があります。

翻訳ファイル config/locales/devise.views.ja.yml を開き、ja.activerecord.errors.models.post配下の日本語訳を定義します。

ja:
  activerecord:
    errors:
      models:
        user:
          attributes:
            email:
              taken: "は既に使用されています。"
              blank: "が入力されていません。"
              invalid: "は有効でありません。"
            nickname:
              blank: "が入力されていません。"
              too_long: "は%{count}文字以下に設定して下さい。"
            password:
              blank: "が入力されていません。"
              too_short: "は%{count}文字以上に設定して下さい。"
              too_long: "は%{count}文字以下に設定して下さい。"
              invalid: "は有効でありません。"
            password_confirmation:
              confirmation: "が一致していません。"
        post: # ここから追加
          attributes:
            title:
              blank: "が入力されていません。"
              too_long: "は%{count}文字以下に設定して下さい。"
            content:
              blank: "が入力されていません。"
              too_long: "は%{count}文字以下に設定して下さい。"
            user:
              required: "が入力されていません。"
...

何かモデルを増やすたびに、このように user と同じ階層レベルで各属性のバリデーションメッセージを定義してあげる必要があります。

テスト実行

これで Post モデル、User モデル、そして翻訳ファイルの修正が完了したので仕様を満たしたはずです。
すべてのテストを実行し、コケるテストがないことを確認しましょう。

$ bin/rspec
...
Finished in 10.46 seconds (files took 0.49612 seconds to load)
37 examples, 0 failures

変更をコミット

これから学習ログを管理していく準備が完了しました。

ここまでの変更をコミットしておきましょう。

$ git add .
$ git commit -m "Postモデルを作成"
$ git push

宿題

アソシエーション

Post と User 間の関連付けには「アソシエーション」という仕組みを利用しています。

Post のマイグレーションファイル内で user に foreign_key: true を含む1行を設定していましたが、
これがアソシエーションを利用することの宣言となります。

モデル間(テーブル間)の結びつけは中級者でも混乱しやすい分野ですので、整理して理解しておきましょう。

外部キーを含む FactoryBot

Post の Factory を定義した際、association を設定することで Post に紐づく User も同時に準備できるようになっていました。
余裕があれば、association の仕組みについても把握しておきましょう。