Railsチュートリアルまとめ6 ユーザーのモデルを作成する

# 個人的リマインド用
参考
[Ruby on Rails チュートリアル プロダクト開発の０→１を学ぼう](https://railstutorial.jp/)

## ユーザーのモデルを作成する
### Userモデルの作成
データモデルとして扱うデフォルトのデータ構造のことを**モデル**(Model)という。データを永続化するために、**データベース**を使ってデータを保存し、データベースとやりとりするためにRailsライブラリの**Active Record**を使う。
さらにRailsには**マイグレーション**(Migration)という機能があり、データの定義をRubyで記述することができるため、SQLのDDL(Data Definition Language)を学ぶ必要がない。

### データベースのマイグレーション
まずモデルを作成する
```
rails g model User name:string email:string
```
モデル名の後にオプションをつけることで、データベースで使いたい2つの属性をRailsに伝える。
そしてgenerateを実行すると**マイグレーション**と呼ばれるファイルも新しく生成される。マイグレーションは、データベースの構造を段階的に変更する手段を提供してくれる。
```ruby
db/migrate/[timestamp]_create_users.rb

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
```
マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まり。changeメソッドはcreate_tableというメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成する。自分で設定していたnameやemail以外にも、created_atやupdate_atという2つの**マジックカラム**を生成してくれる。
ちなみにモデル名は単数系だが、テーブル名は複数形。

上記のマイグレーションが生成された後は、**マイグレーションを適用**する作業に入る。
```
rails db:migrate
```
### モデルファイル
rails g modelでつくられるファイルは2つあり、1つがマイグレーションファイル、もう1つがモデルファイルである。
```ruby
app/models/user.rb

class User < ApplicationRecord
end
```
### ユーザーオブジェクトを作成する
rails console --sandboxで実行
```
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
```
引数なしで読んだ場合は、全てがnil。オブジェクトの属性を設定する**初期化ハッシュ**を引数として受け渡すと
```
>> user = User.new(name: "Michael Hartl", email: "michael@example.com")
>> user
=> #<User id: nil, name: "Michael Hartl", email: "michael@example.com",
created_at: nil, updated_at: nil>
```
この設計は、同様の方法でオブジェクトを初期化するActive Recordの設計に基づいている。
またActive Recordを理解する上で、**「有効性(Validity)」**という概念も重要。
```
>>user.valid?
true
```
現時点でデータベースにデータは格納されていない。つまりUser.newはメモリ上でオブジェクトを作成しただけで、user.valid?もオブジェクトが有効かどうかを確認しただけだから、データベースにデータが存在するかどうかは有効性と関係ない。
```
user.save
```
データベースにデータを保存。ここでマジックカラムにデータが入る。
```
>> foo = User.create(name: "Foo", email: "foo@bar.com")
```
User.createを行うとモデルの生成と保存を同時に行える。
```
foo.destroy
```
destroyはデータベースから削除するが、メモリ上にはまだデータが残る。
### ユーザーオブジェクトを検索する
findは見つかったらユーザーを返してくる。見つからなかったら**例外**を発生する。
```
User.find(1) ←idを渡す
```
**find_by**を使うと属性を指定してユーザーを検索できる。
```
User.find_by(email: "michael@example.com"
```
また、
```
User.find
User.all
```
こんなのもある。

### ユーザーオブジェクトの更新
基本的な方法は2つ。
1つ目は属性を個別に代入する。
```
user.email = "mhartl@example.net"
user.save
```
saveを忘れずに。reloadするとデータベースの情報を元にオブジェクトを再読み込みするので、次のように変更が取り消される。
```
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"
```
2つ目はupdateする方法
```
>> user.update(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
```
成功した時は更新と保存を同時にする。ただし、バリデーションに1つでも引っかかると失敗する。
特定の属性のみを更新したい時は、update_attributeを使う。これにはバリデーションを回避するといった特徴もある。
```
user.update_attribute(:name, "El Duderino")
user.save
```
### 有効性を検証する
バリデーションとテスト駆動開発の相性は最高。
手順に関しては、まず**有効**なモデルのオブジェクトを作成し、その属性のうち1つを有効でない属性に変更する。そして、バリデーションが失敗するかどうかをテストする。念の為、最初に作成時の状態に対してもテストを書いておき、最初のモデルが有効かどうかも確認しておく。このようにすればバリデーションのテストが失敗した時、バリデーションの実装かオブジェクトか、責任の所在がはっきりする。

rails g modelの時に、User用テストの原型ができている。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end
```
有効なオブジェクトに対してテストを書くために、setupという特殊なメソッドを使って有効なUserオブジェクト(@user)を作成する。
setupメソッド内に書かれた処理は、各テストが走る直前に実行される。@userはインスタンス変数だが、setupメソッド内で宣言しておけば、全てのテスト内でこのインスタンス変数が使えるようになる。
```ruby
# 有効なUserかを判断
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end
end
```
ちなみに下記コードでmodelに対してのみテストを行える。
```
rails test:model
```
### 存在性を検証する
最も基本的なバリデーションは「存在性(Presence)」。これは単に、渡された属性が存在することを検証する。テストの方法は、まず属性に対して空白の文字列をセットし、assert_notメソッドを使ってUserオブジェクトが有効でなくなったことを確認する。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do # ←ここ この時点でテストはred
    @user.name = "     "
    assert_not @user.valid?
  end
end
```
name属性の存在を検査する方法はvalidatesメソッドにpresence: trueを与える。これは要素を1つ持つ**オプションハッシュ**である。
```ruby
app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true # ←この文でテストはgreenに
end
```
結果として
```ruby
>> user = User.new(name: "", email: "michael@example.com")
>> user.valid?
=> false
```
なぜ失敗したかerrorsオブジェクトを使って確認できる。
```
user.errors.full_message
=> ["Name can't be blank"]
```
emailの方も同じ処理を
### 長さを検証する
今回は名前を50文字まで、メールアドレスを255文字まで。
超過する文字を入れて有効性を確認する。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "name should not be too long" do # ←こっから2つの文でテストはredに
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end
```
長さに関するバリデーションの追加
```ruby
app/models/user.rb

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 } # ←greenになる
  validates :email, presence: true, length: { maximum: 255 }
end
```
### フォーマットを検証する
最初に有効なメールアドレスと無効なメールアドレスのコレクションに対するテストを行う。
```
>> %w[foo bar baz]
=> ["foo", "bar", "baz"]

>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]

>> addresses.each do |address|
?>   puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp
```
eachメソッドを使ってadresses配列の各要素を繰り返し取り出す。

現在は空ではないメールアドレスなら全てパスするようになっている。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
                         first.last@foo.jp alice+bob@baz.cn]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end
```
assertの第2引数にエラーメッセージを追加している。これにより、どのメールアドレスでテストが失敗したかがわかるようになる。ちなみに詳細な文字列を調べるために.inspectが使われている。

次に@がなかったり、.が,になっているアドレスなどの「無効性」についてもテストをする。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end
end
```
この時点でテストはred。
フォーマット検証のバリデーションオプションはformatである。
```
validates :email, format: { with: /<regular expression>/ }
↑正規表現を使っている
```
正規表現の説明に関して、かなり難しいので本文の第6章、真ん中あたりを都度確認。
```ruby
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }
end
```
正規表現は定数に保存。
### 一意性の検証
一意性を強制するために、validatesメソッドの:uniquenessオプションを使う。
今まではUser.newで、ただ単にメモリ上にオブジェクトを作っていただけ。しかし、一意性のテストにはオブジェクトをデータベースに登録する必要がある。まずは重複したメールアドレスからテストしていく。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
end
```
dupは同じ属性を持つデータを複製するためのメソッド。@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザーの作成は無効になるはず。
```ruby
app/models/user.rb

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true # ←追加
end
```
**補足**：通常メールアドレスでは大文字小文字が区別されない。したがってメールアドレスの検証ではこのような場合も考慮する必要がある。この性質のため、大文字と小文字を区別しないでテストすることが重要。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase # ←追加
    @user.save
    assert_not duplicate_user.valid?
  end
end
```
具体的に見ると
```
>> user = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true
```
これはfalseにならないといけない。これはcase_sensitiveをfalseにすれば解決する。
```ruby
app/models/user.rb

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false } #←ここ
end
```
テストはgreenになった。
しかしまだ問題が1つ残っている。それはActive Recordはデータベースのレベルでは一意性を保持していないこと。
高速で登録ボタンを2回押した時に、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまう。
この問題はデータベース上のemaikカラムに**インデックス**を追加することで、一意性を示し解決することができる。またインデックスは検索にも役立ち、インデックスがなければ全表スキャンしなければいけないところを、本の索引のようにパッと見つけることができる。

emailインデックスを追加するには、データモデリングが必要。Railsではマイグレーションでインデックスを追加する。
```
rails g migration add_index_to_users_email
```
ユーザー用のマイグレーションとことなり、メールアドレスの一意性のマイグレーションは未定義になっているので、定義を記述する必要がある。
```ruby
db/migrate/[timestamp]_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email, unique: true
  end
end
```
add_indexを使う。また、インデックス自体は一意性を強制しないが、オプションでunique: trueを指定することで強制できるようになる。
```
rails db:migrate
```
この時点でのテストはred。rails g modelの時にユーザー用のfixtureが自動的に生成されているが、ここのメールアドレスが一意になっているのが問題なので全部消す。
```
text/fixtures/users.yml

#全部空
```
もう1つやらなければならないことがあり、それは一部のデータベースアダプタが、大文字小文字を区別するインデックスを常に使っているとは限らない問題への対処。Foo@ExAMPle.Comとfoo@example.comを別々の文字列として解釈するデータベースがあるが、このアプリケーションではこれらを同一の文字列として扱わなければならない。
対抗策としては、データベースに保存される直前に、全ての文字列を小文字に変換するということ。これはActive Recordの**コールバック(callback)**で実装できる。
今回の場合は、オブジェクトが保存されるタイミングで処理を実行したいので、before_saveというコールバックを使う。
```ruby
app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true # ←注目！！
end
```
uniquenessがtrueに戻っている。メールアドレスが全部小文字になるなら、大文字小文字を区別するマッチが問題なく動作するから。
テストも修正が入る。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
end
```
### セキュアなパスワードを追加する
セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それをそのままではなく、**ハッシュ化**したものをデータベースに保存する。ハッシュ化とは、ハッシュ関数と呼ばれるものを使って、入力されたデータを不可逆データ(復元不可能なデータ)に変換する処理を行う。ハッシュ化されたパスワードは認証する時に使う。
### ハッシュ化されたパスワード
セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけで、ほぼ完了。Userモデルで次のように呼び出せる。
```ruby
class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end
```
このメソッドを追加すると、次のような機能が使えるようになる。
・セキュアにハッシュ化したパスワードを、データベース内のpassword_digest属性に保存できるようになる。
・2つの仮想的な属性(passwordとpassword_confirmation)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
・authenticateメソッドが使えるようになる(引数の文字列がパスワードと一致するとUserオブジェクトを返し、一致しない場合はfalseを返すメソッド)

このhas_secure_password機能を使うためには、**モデル内にpassword_digestという属性が含まれている**ことが条件。そのために適切なマイグレーションを生成する。
ちなみにマイグレーション名は自由に指定できるが、**末尾にto_usersをつける**ことがおすすめ。これをRailsが認識すると、usersテーブルにカラムを追加するマイグレーションが自動的に作成されるから。
```
rails g migration add_password_digest_to_users password_digest:string
```
```ruby
db/migrate/[timestamp]_add_password_digest_to_users.rb

class AddPasswordDigestToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :password_digest, :string
  end
end
```
今回はadd_columnメソッドを使って、usersテーブルにpassword_digestカラムを追加する。
```
rails db:migrate
```

また、has_secure_passwordを使ってパスワードをハッシュ化するためには、bcryptライブラリが必要。
```
Gemfile

.
.
.
gem "rails",           "7.0.4"
gem "bcrypt",          "3.1.18"
gem "bootstrap-sass",  "3.4.1"
.
.
.
```
```
bundle _2.3.14_ install
```
### ユーザーがセキュアなパスワードを持っている
ようやくUserモデルないでhas_secure_passwordが使えるようになった。
```ruby
app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password # ←ここ！
end
```
この時点でテストはred。理由は、has_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も追加されているから。
テストをパスさせるにはパスワードとパスワード確認の値を追加する。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end                # ↑ここ！
  .
  .
  .
end
```
これでテストはgreenになる。
### パスワードの最小文字数
最大文字数の時とほぼ同じ
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "password should be present (nonblank)" do # ←ここと
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do # ←ここ！
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end
```
違う部分といえば**多重挿入**。まとめて代入できる。
```
@user.password = @user.password_confirmation = "a" * 5
```
その後バリデーションを追加。
```ruby
app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 } # ←ここ！
end
```
この時点でテストはgreen
### ユーザーの作成と認証
今回はRails consoleで作る
```
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>             password: "foobar", password_confirmation: "foobar")
```
ハッシュ化の確認
```
>> user = User.find_by(email: "michael@example.com")
>> user.password_digest
=> "$2a$12$WgjER5ovLFjC2hmCItmbTe6nAXzT3bO66GiAQ83Ev03eVp32zyNYG"
```
また、authenticateメソッドも使えるようになっているかの確認。このメソッドは引数に渡された文字列(パスワード)をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較する。
間違った値の場合
```
>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false
```
正しい場合
```
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 03:15:38", updated_at: "2022-03-11 03:15:38",
password_digest: [FILTERED]>
```
ユーザーオブジェクトを返す。
またオブジェクトを返すのではなく、返される値が論理値であることが重要なので、
```
!!user.authenticate("foobar")
=> true
```
強制的に論理値を返させる。