Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #5 Userモデル編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#4 System spec導入編](https://qiita.com/aokyo17/items/356b7628194937851848)
次回：[#6 ユーザー登録画面, エラー日本語化編](https://qiita.com/aokyo17/items/6ee827c240907514681b)

# 今回の流れ
1. 完成のイメージを理解する
2. Userモデルを作る
3. Userモデルのテストを作る
4. Herokuのエラーを解決する
5. showビューを仮実装する

※ この記事は、ポートフォリオを作る理由を**web系自社開発企業に転職するため**とします。
※ 2020年3月30日、記事を大幅に更新しました。

# 完成のイメージを理解する
まずは、登録機能・ログイン機能を実装するためのUserモデルを作ります。
続いて、UserモデルのテストをModel specで作ります。
その後、マイグレーションの際のHerokuエラーを解決します。
最後に、次回以降に必要な仮ビューを作ります。

以上です。

# Userモデルを作る
ユーザー情報を管理する、Userモデルを作ります。
ここでの手順は以下の通りです。

* Userモデルを生成する
* バリデーションを追加する
* インデックスを追加する
* パスワードを追加する

## Userモデルを生成する
ジェネレーターでUserモデルを生成します。
現段階では、Userモデルにユーザーネームとアドレスの属性を与えます。

```shell:shell
$ rails g model User name:string email:string
```

この際、自動的にID属性も付与されます。
このIDは、showビューを仮実装にも使われます。

## バリデーションを追加する
このままでは、どんな情報でもUser登録を受け付けてしまいます。
よって、以下のバリデーションを加えます。

* ① ユーザーネームを必須にする
* ② アドレスを必須にする
* ③ 51文字以上の名前を無効にする
* ④ 256文字以上のアドレスを無効にする
* ⑤ アドレスでない文字列を無効にする
* ⑥ 一意性のないアドレスを無効にする
* ⑦ 一意性のないアドレスの大文字小文字を区別しないようにする
* ⑧ アドレスを全て小文字にする

```rb:app/models/user.rb
class User < ApplicationRecord
  before_save :downcase_email #⑧
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  
  validates :name,
    presence: true, #①
    length: { maximum: 50 } #③
  validates :email,
    presence: true, #②
    length: { maximum: 255 }, #④
    format: { with: VALID_EMAIL_REGEX }, #⑤
    uniqueness: { case_sensitive: false } #⑥⑦

  private
    def downcase_email #⑧のメソッド
      email.downcase!
    end
end
```

downcase_emailメソッドは、user.rb内でのみ使います。
よって、プライベートにします。

## インデックスを追加する
アドレスを検索する際は、ユーザーとの紐付けがあると高速です。
そのためのインデックスをマイグレーションに追加します。

```shell:shell
$ rails g migration add_index_to_users_email
```

```rb:db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
  def change
    add_index :users, :email, unique: true
  end
end
```

データベースレベルでの一意性も確保するために、uniqueをtrueにしています。
最後に、データベースを更新します。

```shell:shell
$ rails db:migrate
```

# パスワードを追加する
Userモデルにパスワードを追加します。
ここでの手順は以下の通りです。

* has_secure_passwordを理解する
* 必要なGemを入れる
* password_digest属性を追加する
* has_secure_passwordとバリデーションを追加する

## has_secure_passwordを理解する
パスワードの実装は、モデルにhas_secure_passwordメソッドを加えることで可能になります。
これによって、以下の機能が使えます。

* passwordとpassword_confirmation（再入力）の仮属性が作られる
* セキュア化したパスワードをpassword_digest属性に保存する
* パスワードの正誤を表すauthenticateメソッドが使える

has_secure_passwordを使うには、Gemとpassword_digest属性が必要です。

## 必要なGemを入れる
bcryptというGemを加えます。

```diff:Gemfile
+ gem 'bcrypt', '3.1.12'
```

```shell:shell
$ bundle install
```

## password_digest属性を追加する
password_digest属性の追加には、同じくマイグレーションを使います。

```shell:shell
$ rails g migration add_password_digest_to_users password_digest:string
$ rails db:migrate
```

## has_secure_passwordとバリデーションを追加する
Userモデルにhas_secure_passwordメソッドを追加します。
パスワードの一意性と5文字以下を無効にするバリデーションも追加します。

```rb:app/models/user.rb
class User < ApplicationRecord
  before_save :downcase_email
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  
  validates :name,
    presence: true,
    length: { maximum: 50 }
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
  # 追加
  has_secure_password
  validates :password,
    presence: true,
    length: { minimum: 6 }

  private
    def downcase_email
      email.downcase!
    end
end
```

以上でモデルの作成が完了です。

# Userモデルのテストを作る
Model specでUserモデルのテストを作ります。
[Rails Tutorial 6.2](https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-user_validations)のテスト内容とほぼ同じです。

```rb:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  
  let(:user) { User.new(
    name: "Example User",
    email: "user@example.com",
    password: "foobar",
    password_confirmation: "foobar"
  ) }
  
  describe "User" do
    it "should be valid" do
      expect(user).to be_valid
    end
  end
  
  describe "name" do
    it "gives presence" do
      user.name = "  "
      expect(user).to be_invalid
    end
    
    context "50 characters" do
      it "is not too long" do
        user.name = "a" * 50
        expect(user).to be_valid
      end
    end
    
    context "51 characters" do
      it "is too long" do
        user.name = "a" * 51
        expect(user).to be_invalid
      end
    end
  end
  
  describe "email" do
    it "gives presence" do
      user.email = "  "
      expect(user).to be_invalid
    end
    
    context "254 characters" do
      it "is not too long" do
        user.email = "a" * 243 + "@example.com"
        expect(user).to be_valid
      end
    end
    
    context "255 characters" do
      it "is too long" do
        user.email = "a" * 244 + "@example.com"
        expect(user).to be_invalid
      end
    end
    
    it "should accept valid addresses" do
      user.email = "user@example.com"
      expect(user).to be_valid
      
      user.email = "USER@foo.COM"
      expect(user).to be_valid
      
      user.email = "A_US-ER@foo.bar.org"
      expect(user).to be_valid
      
      user.email = "first.last@foo.jp"
      expect(user).to be_valid
      
      user.email = "alice+bob@baz.cn"
      expect(user).to be_valid
    end
    
    it "should reject invalid addresses" do
      user.email = "user@example,com"
      expect(user).to be_invalid
      
      user.email = "user_at_foo.org"
      expect(user).to be_invalid
      
      user.email = "user.name@example."
      expect(user).to be_invalid
      
      user.email = "foo@bar_baz.com"
      expect(user).to be_invalid
      
      user.email = "foo@bar+baz.com"
      expect(user).to be_invalid
      
      user.email = "foo@bar..com"
      expect(user).to be_invalid
    end
    
    it "should be unique" do
      duplicate_user = user.dup
      duplicate_user.email = user.email.upcase
      user.save!
      expect(duplicate_user).to be_invalid
    end
    
    it "should be saved as lower-case" do
      user.email = "Foo@ExAMPle.CoM"
      user.save!
      expect(user.reload.email).to eq 'foo@example.com'
    end
  end
  
  describe "password and password_confirmation" do
    it "should be present (nonblank)" do
      user.password = user.password_confirmation = " " * 6
      expect(user).to be_invalid
    end
    
    context "5 characters" do
      it "is too short" do
        user.password = user.password_confirmation = "a" * 5
        expect(user).to be_invalid
      end
    end
      
    context "6 characters" do
      it "is not too short" do
        user.password = user.password_confirmation = "a" * 6
        expect(user).to be_valid
      end
    end
  end
end
```

テストのUserモデルにはインスタンス変数を使わず、letを使います。
望ましいテストにするため、しきい値を確認しています。

参考になりました↓
[RSpec入門 - Dotinstall](https://dotinstall.com/lessons/basic_rspec)
[RSpecで書かれたRailsチュートリアル 第6章のテストコードをレビューしてみた](https://www.youtube.com/watch?v=qpiKb0mdbr0)

# Herokuのエラーを解決する
gitのpushを済ませ、Herokuを起動しようとすると以下のエラーが発生します。

```shell:shell
$ heroku run rails db:migrate
Running rails db:migrate on ⬢ lantern-lantern-app... up, run.2705 (Free)
rails aborted!
PG::ConnectionBad: could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?
```

これはPostgreSQLの問題です。アドオンを足すことで使えます。

```shell:shell
$ heroku addons:add heroku-postgresql
$ heroku run rails db:migrate
```

参考になりました↓
[【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】](https://qiita.com/kazukimatsumoto/items/a0daa7281a3948701c39)
[【Rails】「heroku run rake db:migrate」を実行しようとすると発生するエラーについて](https://qiita.com/suzuki-x/items/b878723080aea1a673ed)

# showビューを仮実装する
次回以降に必要な、ログイン・登録後に遷移するshowビューを仮実装します。
ここでの手順は以下の通りです。

* RESTfulにルーティングを行う
* showアクションを編集する
* showビューを作る

## RESTfulにルーティングを行う
まずはルーティングを行います。
もっと後になりますが、各ユーザーには様々なページを用意します。
各ユーザーのページURLをいい感じ（RESTful）に作るには、resourcesを使います。

```config/routes.rb
Rails.application.routes.draw do
# 中略
  resources :users
end
```

何が生成されたかを、[Rails Tutorial 表7.1](https://railstutorial.jp/chapters/sign_up?version=5.1#table-RESTful_users)で確認します。
そのうちの一つである、showビューを仮実装します。

## showアクションを編集する
Usersコントローラーのshowアクションを編集します。
showビューは、ログイン・登録画面から送信されたIDでユーザーを特定します。
それを実装したのが、以下の記述です。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
  end
end
```

以下の部分を解説します。

```rb:
@user = User.find(params[:id])
```

変数userに各Userモデル入れることで、各ユーザーの情報を画面に与えます。

例えば、初めての登録者のIDは1になります。
その際、ログイン・登録画面からshowビューに向けてIDが送信されます。
よって、ユーザーのshowビューのURLは/users/1になります。
ここの、/1というパラメーター部分が、/:idに当たります。

それを、railsで取得する際の書き方が、params[:id]になります。
各ユーザーを特定するには、UserモデルからIDが1のものを探します。
それを、Userモデルから探すのが、User.find(引数)になります。

## showビューを作る
仮でshowビューを作ります。
先ほどshowアクションに、インスタンス変数@userを追加しました。
@user.nameのように属性をドットで繋ぐことで、ユーザーネームが呼び出せます。

```erb:app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="container">
  <div class="row">
    <div class="col bg-primary">
      form
    </div>
    <div class="col bg-secondary">
      figure
    </div>
  </div>
  <div class="row">
    <div class="col bg-warning">
      log
    </div>
  </div>
</div>
```

![lantern_lantern_show_page_provisional.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/8f66b212-5dd0-4267-9fa3-5607a5039801.png)

プレビューにあるデバッグを実装したい方は、[Rails Tutorial 7.1.3](https://railstutorial.jp/chapters/sign_up?version=5.1#sec-debugger)をご覧ください。
（この記事では、言及しません。）

今回は以上です。

---
前回：[#4 System spec導入編](https://qiita.com/aokyo17/items/356b7628194937851848)
次回：[#6 ユーザー登録画面, エラー日本語化編](https://qiita.com/aokyo17/items/6ee827c240907514681b)