0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Railsチュートリアル】第6章 ユーザーのモデルを作成する②

Posted at

【Railsチュートリアル】第6章 ユーザーのモデルを作成する

本章の目的:一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について学んでいく。

6.2 ユーザーを検証する

本節の目的:検証(Validation)を実装する。

**検証(Validation)**とは?
nameを空にしない、emailはメールアドレスのフォーマットに従う、というように何らかの制限を課すことができるActive Recordの機能。

よく使われるケース
存在性(presence)、長さ(length)、フォーマット(format)、意性(uniqueness)、確認(confirmation)

6.2.1 有効性を検証する

検証方法

  1. 有効なモデルのオブジェクトを作成
  2. その属性のうちの1つを有効でない属性に意図的に変更
  3. バリデーションで失敗するかどうかをテスト
test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  # setupメソッド内に書かれた処理は、各テストが走る直前に実行される。
  def setup
    # @userはインスタンス変数
    # setupメソッド内で宣言しておけばすべてのテスト内で使えるようになる。
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    # setupメソッド内で@userを宣言しているので@userが使える。
    assert @user.valid?
  end
end

演習 1

コンソールから、新しく生成したuserオブジェクトが有効(valid)であることを確認してみましょう。

console
>> user = User.create(name: "moutoon", email: "moutoon@example.com")
   (0.1ms)  begin transaction
   (0.2ms)  SAVEPOINT active_record_1
  User Create (5.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "moutoon"], ["email", "moutoon@example.com"], ["created_at", "2021-02-09 02:50:29.521080"], ["updated_at", "2021-02-09 02:50:29.521080"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "moutoon", email: "moutoon@example.com", created_at: "2021-02-09 02:50:29", updated_at: "2021-02-09 02:50:29">
>> user.valid?
=> true

演習 2

6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。

console
>> user = User.new(name: "Michael Hartl", email: "michael@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "michael@example.com", created_at: nil, updated_at: nil>
>> user.valid?
=> true

6.2.2 存在性を検証する

**存在性(Presence)**とは?
渡された属性が存在することを検証すること。この節では、ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証する。

test/models/user_test.rb
.
.
.
 # user.nameが存在することを検証する。
  test "name should be present" do
    @user.name = "  " # user.nameに空白を代入
    assert_not @user.valid?
		# @user.valid?がtrueなら失敗、falseなら成功
  end
end

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  # validates(:name, presence: true)
  # validates(検証したい場所, 検証したい内容)
end
console
>> user = User.new(name: "",email: "moutoon@example")
   (0.1ms)  begin transaction
	# nameが空白のUserインスタンスをuserに代入
=> #<User id: nil, name: "", email: "moutoon@example", created_at: nil, updated_at: nil>

>> user.valid?
# userを検証
=> false
# validates :name, presence: true と設定しているので失敗(false)

>> user.errors.full_messages
# errorsオブジェクトを使うと、失敗の原因を確認できる
=> ["Name can't be blank"]

>> user.save
=> false
# Userオブジェクトは有効でないので保存ができない

演習 1

新しいユーザーuを作成し、作成した時点では有効ではない(invalid)ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。

console
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]
# nameとemailが空白

演習 2

u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?

console
>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

>> u.errors.messages[:email]
=> ["can't be blank"]
# キーを指定(:email)を指定してハッシュを取り出す

6.2.3 長さを検証する

ユーザー名は50、メールアドレスは255の上限を設ける。

test/models/user_test.rb
.
.
.
  # user.nameが51文字のとき@userは有効か検証せよ
  test "name should not be too long" do
    @user.name = "a" * 51 
    assert_not @user.valid?
  end

 # user.emailが244文字+@example.comのとき@userが有効か検証せよ
 test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end
app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true length: { maximum: 255 }
  # validates(:name, presence: true)
  # validates(検証したい場所, 検証したい内容)
end

演習 1

長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

console
user = User.new(name: "moutooooooooooooooooooooooooooooooooooooooooooooooooo", email: "moutooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo@example.com")
   (0.1ms)  begin transaction
=> #<User id: nil, name: "moutoooooooooooooooooooooooooooooooooooooooooooooo...", email: "moutoooooooooooooooooooooooooooooooooooooooooooooo...", created_at: nil, updated_at: nil>
>> user.valid?
=> false

演習 2

長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

console
>> user.errors.messages
=> {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}

6.2.4 フォーマットを検証する

メールアドレスにおなじみのパターンuser@example.comに合っているかどうかも確認することを要求する。

console
# 空白で区切って配列をつくることができる記法
>> %w[foo bar baz]
=> ["foo", "bar", "baz"]

# 文字列を配列にしてaddressesに代入せよ
>> 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の値を一つずつ取り出し、出力せよ
>> addresses.each do |address|
?>   puts address
>> end

USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp
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
  .
  .
  # emailが有効か検証せよ
  test "email validation should accept valid addresses" do
    # 文字列を空白で区切って配列にし、valid_addressesに代入せよ
    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の値を一つずつ取り出し、
    valid_addresses.each do |valid_address|
      # valid_addressを@user.emailに代入せよ
      @user.email = valid_address
      # @userは有効か?
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end
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

演習 1

リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

確認だけなので省略

演習 2

先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。

確認だけなので省略

演習 3

foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

app/models/user.rb

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
  # validates(:name, presence: true)
  # validates(検証したい場所, 検証したい内容)
end

6.2.5 一意性を検証する

**一意性(uniqueness)**とは?

意味や値が一つに確定していること。つまり同じデータが無いこと。
ユーザー名として使うためにemailに一意性を求める。

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
    # @userを複製してduplicate_userに代入せよ
    duplicate_user = @user.dup
    # @userを保存せよ
    @user.save
    # duplicate_userは有効か?
    assert_not duplicate_user.valid?
  end
end
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 },
                    # emailがuniqueness: trueか検証せよ
                    uniqueness: true
end

メールアドレスは大文字小文字が区別されないため、検証でも区別しないようにすることが重要となる。

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.emailをupcase(大文字)にして、duplicate_user.emailに代入せよ
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end
end
app/models/user.rb

# 大文字と小文字を区別せずuniquenessを検証せよ
uniqueness: { case_sensitive: false }

演習 1

リスト 6.34のように、メールアドレスを小文字にするテストをリスト 6.26に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.34のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして red になることを、また、コメントアウトを解除すると green になることを確認してみましょう。

test/models/user_test.rb
  .
  .
  .

  test "email addresses should be saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    # mixed_case_email.downcaseと@user.reload.emailの値が一致しているか確認せよ
    # @user.reload.email データベースの値に合わせて@user.emailを更新する
    assert_equal mixed_case_email.downcase, @user.reload.email
  end
end

演習 2

テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります(リスト 6.35)。

app/models/user.rb
class User < ApplicationRecord
  before_save { email.downcase! }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
end

6.3 セキュアなパスワードを追加する

本節の目的:セキュアなパスワードを実装し、ハッシュ化したパスワードをデータベースに保存する

6.3.1 ハッシュ化されたパスワード

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # この一行だけ
  has_secure_password
end

has_secure_passwordを実装すると以下の機能が使えるようになる。

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

has_secure_password機能を使えるようにするために、モデル内にpassword_digestを追加する。

console
$ rails generate migration add_password_digest_to_users password_digest:string
# rails generate migration  マイグレーションファイルを生成せよ
# add_password_digest_to_users  ファイル名(末尾をto_usersにしておく)
# password_digest:string 属性(password_digest)と型情報(:string = 文字列)を渡す

has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要

Gemfile

source 'https://rubygems.org'

gem 'rails',          '6.0.3'
gem 'bcrypt',         '3.1.13'
gem 'bootstrap-sass', '3.4.1'
.
.
.
console
$ bundle install

6.3.2 ユーザーがセキュアなパスワードを持っている

演習 1

この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。

console
>> user = User.new(name: "moutoon", email: "moutoon@example.com")
   (0.2ms)  begin transaction
=> #<User id: nil, name: "moutoon", email: "moutoon@example.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
  User Exists? (1.1ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "moutoon@example.com"], ["LIMIT", 1]]
=> false

演習 2

なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。

console
>> user.errors.full_messages
=> ["Password can't be blank"]
# Passwordが設定されていないためfalseとなる

6.3.3 パスワードの最小文字数

passwordが空でない、最小文字数6文字を設定する。

test/models/user_test.rb
.
.
.
# パスワードが空白ではないことをテストせよ
test "password should be present (nonblank)" do
  # @user.password_confirmationに空白6文字を代入し、それを@user.passwordに代入せよ
  @user.password = @user.password_confirmation = " " * 6
  assert_not @user.valid?
end

# パスワードの最小文字数が6文字であることをテストせよ
test "password should have a minimum length" do
  # @user.password_confirmationにa5文字を代入し、それを@user.passwordに代入せよ
  @user.password = @user.password_confirmation = "a" * 5
  assert_not @user.valid?
end

多重代入(Multiple Assignment)

パスワードとパスワード確認に対して同時に代入をしている。

test/models/user_test.rb
@user.password = @user.password_confirmation = "a" * 5

has_secure_passwordメソッドは存在性のバリデーション機能を持っているものの、新しくレコードが追加されたときだけに適用される性質のため、更新のときはバリデーションが適用されずに更新されない問題がある。空のパスワードを入力させないために、存在性のバリデーション(6.2.2)も一緒に追加する。

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

演習 1

有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。

console
>> user = User.new(name: "moutoon", email: "moutoon@example.com", password: "m")
   (0.2ms)  begin transaction
=> #<User id: nil, name: "moutoon", email: "moutoon@example.com", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
>> user.valid?
  User Exists? (1.0ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "moutoon@example.com"], ["LIMIT", 1]]
=> false

演習 2

上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。

console
>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]
# パスワードが短すぎるためエラーになっている

6.3.4 ユーザーの作成と認証

データベースに新規ユーザーを1人追加する。

console
# ユーザーを作成
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?> password: "foobar", password_confirmation: "foobar")
   (3.2ms)  SELECT sqlite_version(*)
   (0.1ms)  begin transaction
  User Exists? (1.4ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "michael@example.com"], ["LIMIT", 1]]
  User Create (6.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?)  [["name", "Michael Hartl"], ["email", "michael@example.com"], ["created_at", "2021-02-09 06:34:49.836041"], ["updated_at", "2021-02-09 06:34:49.836041"], ["password_digest", "$2a$12$c6TWZz21VWw7m8bDorgz9uR3XMXaWbMdtzmV.r6krzwzy59OmF7gO"]]
   (5.6ms)  commit transaction
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-02-09 06:34:49", updated_at: "2021-02-09 06:34:49", password_digest: [FILTERED]>

# emailがmichael@example.comのユーザーを検索
>> user = User.find_by(email: "michael@example.com")
  User Load (1.2ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "michael@example.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-02-09 06:34:49", updated_at: "2021-02-09 06:34:49", password_digest: [FILTERED]>

# ユーザーのpassword_digestを参照
# has_secure_passwordによって、password: "foobar"がハッシュ化されている
>> user.password_digest
=> "$2a$12$c6TWZz21VWw7m8bDorgz9uR3XMXaWbMdtzmV.r6krzwzy59OmF7gO"

authenticateメソッド

引数に渡された文字列(パスワード)をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較する。

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false
# 違うパスワードを渡すとfalseを返す
console
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-02-09 06:34:49", updated_at: "2021-02-09 06:34:49", password_digest: [FILTERED]>
# 正しいパスワードを渡すとユーザーオブジェクトを返す
# Userオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrueであることが重要

演習 1

コンソールを一度再起動して(userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。

console
>> user = User.find_by(email: "moutoon@example.com")
   (3.5ms)  SELECT sqlite_version(*)
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "moutoon@example.com"], ["LIMIT", 1]]
=> #<User id: 2, name: "moutoon", email: "moutoon@example.com", created_at: "2021-02-09 07:13:31", updated_at: "2021-02-09 07:13:31", password_digest: [FILTERED]>
>>

演習 2

オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?

console
>> user.name = "sheep"
=> "sheep"
>> user.save
   (0.1ms)  begin transaction
  User Exists? (0.8ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "moutoon@example.com"], ["id", 2], ["LIMIT", 1]]
   (0.1ms)  rollback transaction
=> false

演習 3

今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。

console
>> user.update_attribute(:name, "sheep")
   (0.1ms)  begin transaction
  User Update (6.0ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "sheep"], ["updated_at", "2021-02-09 07:21:20.791999"], ["id", 2]]
   (8.9ms)  commit transaction
=> true

>> user = User.find_by(email: "moutoon@example.com")
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "moutoon@example.com"], ["LIMIT", 1]]
=> #<User id: 2, name: "sheep", email: "moutoon@example.com", created_at: "2021-02-09 07:13:31", updated_at: "2021-02-09 07:21:20", password_digest: [FILTERED]>

さいごに

  • Userモデルの作成と属性(name, email, password)を追加
  • バリデーションの実装
  • セキュア認証機能の実装

長かったですが、楽しくやり切れました!
早い段階からローカル環境に慣れておきたいため、この回からCloud9ではなく、Virtual Box+Vagrantで学習を進めています。
次から難しくなってくるようなので気合入れて楽しみます!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?