Railsチュートリアル 第６章

#ユーザオブジェクト
##ユーザオブジェクトの作成
方法は２つある。
###方法１
モデルを生成する

```
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com",
created_at: nil, updated_at: nil>
```

有効性を確認

```
>> user.valid?
true
```

保存

```
>> user.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.8ms)  INSERT INTO "users" ("name", "email", "created_at",
  "updated_at") VALUES (?, ?, ?, ?)  [["name", "Michael Hartl"],
  ["email", "mhartl@example.com"], ["created_at", 2016-05-23 19:05:58 UTC],
  ["updated_at", 2016-05-23 19:05:58 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
```

saveメソッドは、成功すればtrue、失敗すればfalseを返す。

###方法２
モデルの生成と保存の２ステップを同時に行う方法。

```
>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2016-05-23 19:18:46", updated_at: "2016-05-23 19:18:46">
```

User.createは、trueかfalseを返す代わりにユーザオブジェクト自身を返す。

##ユーザオブジェクトの削除
destroyメソッドで削除する

```
>> foo.destroy
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 3]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
```

##ユーザオブジェクトの検索
オブジェクトを検索するための、findメソッドとfind_byメソッドについて。
下記のfindとfind_byでは同じ結果が返ってくる。

```
>> User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2020-01-24 07:53:52", updated_at: "2020-01-24 07:53:52">
```

```
>> User.find_by(id: 1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2020-01-24 07:53:52", updated_at: "2020-01-24 07:53:52">
```

**しかし、検索がヒットしなかった時の返り値には違いがある。**

findメソッドの場合、指定したidのオブジェクトが存在しない場合、例外が発生する。

```
>> User.find(2)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):26
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=2)
```

それに対して、find_byメソッドの場合は、nilが返される。

```
>> User.find_by(id: 2)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
=> nil
```

##ユーザオブジェクトの更新
更新するのにも方法が２つある。
###方法１

```
>> user           # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
```

###方法２
update_attributesメソッドは、ハッシュを引数に渡して複数同時に更新することができる。
更新のタイミングでバリデーションも実行される。

```
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
```

また、特定の属性のみを更新したい場合にはupdate_attributeが使用できます。
**この場合、バリデーションをパスして更新されます。**

```
>> user.update_attribute(:name, "El Duderino")
=> true
```

##assertとassert_not
assert foo：fooが真なら成功、偽なら失敗
assert_not foo：fooが偽なら成功、真なら失敗

```
test "name should be present" do
　　@user.name = "  "　　　　　　⇨nameを空にする
　　assert_not @user.valid?　　⇨nameが空なのでバリデーションに引っ掛かり、偽となるので成功
end
```

##一意性の検証

```
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のオブジェクトをコピーしてemailが重複するユーザ作成
　　duplicate_user.email = @user.email.upcase　　⇨emailを大文字にする
    @user.save　　　　　　　　　　　　　　　　　　　　⇨@userを保存
    assert_not duplicate_user.valid?　　　　　　　⇨コピーして作られたユーザのバリデーションが有効でない事を確認
  end
end
```

###dupメソッド
オブジェクトのコピーを作成
###uniqunessオプション
emailを一意項目とする

```
validates :email, uniquness: true
```

ただし、これでは大文字小文字を区別しているため、duplicate_user.valid?はtrueになる。
しかし、メールアドレスなので大文字小文字を区別しないようにしたいので、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
```

##データベースでの一意性
上記でアプリケーション側ではメールアドレスの一意性の検証を行いましたが、
データベースレベルでも一意性の検証を行う必要があります。

emailインデックスを追加するためにマイグレーションファイルを作成する

```
$ rails generate migration add_index_to_users_email
```

作成したマイグレーションファイルにemailインデックスを追加する

```ruby: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
```

usersテーブルのemailカラムに対して、インデックスを追加しており、オプションでunique: trueを指定することで一意性を強制している。

```
$ rails db:migrate
```

##before_saveコールバック
ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換する。

```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: { case_sensitive: false }
end
```

```
self.email = self.email.downcase
```

上記のように書くこともできるが、Userモデルの中では右式のselfを省略できる。
selfは現在のユーザーを指します。


#セキュアなパスワード追加
セキュアなパスワードの実装は、has_secure_passwordメソッドを呼び出すだけでほとんど終わりです。

##has_secure_passwordで可能になること
- セキュアにハッシュ化したパスワードをデータベース内のpassword_digestという属性に保存できるようになる
- 2つのペアの仮想的な属性 (passwordとpassword_confirmation) が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される
- authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド) 

##has_secure_passwordの仕組み
ユーザーがパスワードと確認用パスワードを入力すると、一時データであるpasswordとpassword_confirmationに保持される。（Userのデータモデルにはpassword_digestしか存在しない）
これに対して、存在性と値が一致するかどうかのバリデーションが実行される。
問題なければ、ハッシュ化したパスワードはpassword_digestに保存される。


##has_secure_password導入
###Userモデルにhas_secure_passwordメソッド追加

```ruby:app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end
```

###Userのデータモデルにpassword_digestカラムを追加
####マイグレーションファイル作成

```
$ rails generate migration add_password_digest_to_users password_digest:string
```

上記のコマンドにより、下記のマイグレーションファイルが作成される

```ruby:db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :password_digest, :string
  end
end
```

データベースでマイグレーションを実行する

```
$ rails db:migrate
```

####bcryptを追加
パスワードをハッシュ化するためのハッシュ関数

```ruby:Gemfile.rb
source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bcrypt',         '3.1.12'
.
.
.
```

インストール

```
$ bundle install
```

##バリデーションを追加
has_secure_passwordメソッドは存在性のバリデーションもしてくれるが、これは新しくレコードが追加された時だけに適用されるので、ユーザーが' '(6文字空白スペース)といった文字列をパスワード欄に入力し更新すると、バリデーションが適用されず更新されてしまう。そのため、モデルにバリデーションを追加する必要がある。

```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: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }　　⇦これを追加
end
```

passswordカラムに対して、空白でないこと、最小文字数が６文字であるかをチェック

##authenticateメソッド
has_secure_passwordをUserモデルに追加したことで、オブジェクト内でauthenticateメソッドが使えるようになる。

間違ったパスワードを与えると、falseが返される

```
>> user.authenticate("foobaz")
false
```

次に正しいパスワードを与えると、ユーザーオブジェクトが返される

```
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">
```

正しかったときにUserオブジェクトではなく、返ってきた値がtrueである方が判定するときに都合がいいので、「!!」でオブジェクトが対応する論理値オブジェクトに変換してあげる。

※「!!」という演算子を使うと、そのオブジェクトを２回否定することになるので、どんなオブジェクトも強制的に論理値に変換できる

```
>> !!user.authenticate("foobar")
=> true
```

##本番環境のモデル更新
本番環境でも今回変更したUserモデルの変更を反映させるためには、Heroku上でもマイグレーションを走らせる必要がある。

```
$ heroku run rails db:migrate
```

##--sandbox
Railsコンソールのサンドボックスモードにすると、コンソール終了時に全てロールバックされるので、モデルの確認などができる。

```
$ rails console --sandbox
$ rails c -s　⇨短縮形
```