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.

1周目 railsチュートリアル第6章

Last updated at Posted at 2021-08-30

#目次
1.前回
2.概要
3.内容
4.用語のまとめ
5.感想
6.おわりに

#1. 前回

#2. 概要
ユーザーのモデルを作成する。
ユーザー用のデータモデルの作成と、データを保存する手段の確保を行う。

#3. 内容

Userモデル

完成図
6章の完成図.jpg

MVCのM=モデルデータモデルを扱うデフォルトのデータ構造
データの永続化=データベースを使う
RailsのライブラリではActiveRecordという。
ActiveRacordはデータオブジェクトの作成、保存、検索のメソッドを持っている。
Railsはデータベースの細部を見えなくするのであまり意識することはない。

データベースの移行

コンソール上でexitをして閉じても消えないユーザーモデルを構築する。
name,emailと属性をつけたUserモデルを生成する。

$rails g model User name:string email:string

コントローラー名:複数形
ユーザー名:単数形

Userモデルのマイグレーション

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

      t.timestamps
    end
  end
end

db/migrate/[timestamp]_create_users.rb
ファイル名の先頭にタイムスタンプが追加される。

モデル名:単数形
テーブル名:複数形

rails db:migrate
マイグレーションの適用

rails db:rollback
元に戻すこと

演習
  • ~environment/sample_app/db/schema.rbの
    内容とマイグレーションファイルの内容を見比べる。

  • マイグレーションを元に戻すコマンドを実施。ロールバックコマンドを使う(rails db:rollback)
    db/schema.rbを見比べる。
    特定のカラムを削除するなどする場合はup,downのメソッドを定義する必要があるがマイグレーションを別途まとめる必要がある。

  • 元に戻すため再度、rails db:migrateを実行

modelファイル

usersテーブルを作成するとdevelopment.sqlite3のファイルが更新。id,name,email...を作成。
user.rbも作成される。(app/models/user.rb)

class User < ApplicationRecord
end
演習
  • rails c でUser.newを実行してApplicationRecordを継承しているか確認する。
>> User.new
   (2.3ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

継承の確認
>> User.superclass
=> ApplicationRecord(abstract)
  • ApplicationRecordがActiveRecord::Baseを継承しているか確認。
>> User.superclass.superclass
=> ActiveRecord::Base

ユーザーオブジェクトを作成する

コンソールをサンドボックスモードで起動、exitで終了時のロールバックされるため
実際のDBには登録されない。

$ rails c --sandbox
Running via Spring preloader in process 3642
Loading development environment in sandbox (Rails 6.0.3)
Any modifications you make will be rolled back on exit

サンドボックスモードであればUser.newで作成してuser=User.new(name:~~~)を作成しても保存されない。
ActiveRecordの操作でvalidメソッドを使うとそのオブジェクトが有効かが確認できる。

>> user.valid?
true

saveメソッドでDBに登録。User.createの逆destoroyも使える。
しかし、destoryコマンドを実行してもオブジェクトはまだ存在し続ける。

演習
  • user.name,user.emailがどちらもStringクラスのインスタンスであるか確認。
>> user.name.class
=> String
>> user.email.class
=> String
  • created_at,updated_atはどのクラスのインスタンスであるか
>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

ユーザーオブジェクトを検索する

ActiveRecordのオブジェクト検索方法
User.find(1)
idが1番のデータを検索する、先程消したid情報を入力すると存在しないと結果が返ってくる

>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3

User.find_by(email:"michael@example.com")
特定の属性でユーザーを検索する方法もある。ユーザーをサイトにログインさせるときに役立つ。

User.first
最初のユーザーを結果として表示させる。

User.all
全てのUserオブジェクトを表示させる。返ってきたオブジェクトクラスはActiveRecord::Relationとなっている。
各オブジェクトを配列として効率的にまとめてくれるクラスである。

演習
  • nameを使ってユーザーオブジェクトを検索する。find_by_nameも利用する。
>> User.find_by(name:"Michael Hartl")
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-08-31 15:06:58", updated_at: "2021-08-31 15:06:58">
>> 
find_by_nameを実行
>> User.find_by_name("Michael Hartl")               
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-08-31 15:06:58", updated_at: "2021-08-31 15:06:58">
>> 
  • User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認する。
>> User.all.class
=> User::ActiveRecord_Relation
  • User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認する。
>> User.all.length
  User Load (0.1ms)  SELECT "users".* FROM "users"
=> 2

ユーザーオブジェクトを更新する

更新方法は2つ。

saveメソッドを使った更新。

>> 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

saveを行うと更新される。
save前にreloadされると取り消されてしまう。
user.reload.email

user.saveを実行したことでユーザー更新が行われたため以下の2つも更新される。

  • user.created_at
  • user.updated_at

updateメソッドを使った更新。

>> user.update(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
>> user.name
=> "El Duderino"
演習
  • userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存。
>> user
=> #<User id: 1, name: "The Dude", email: "dude@abides.org", created_at: "2020-09-01 02:59:48", updated_at: "2021-09-01 03:10:02">
>> 
>> user.name = "usertest"
=> "usertest"
  • updateメソッドを使い、email属性を更新、保存する。
>> user.update(email: "test@test.com")   (0.1ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "email" = ?, "updated_at" = ?, "name" = ? WHERE "users"."id" = ?  [["email", "test@test.com"], ["updated_at", "2021-09-01 03:12:04.216320"], ["name", "usertest"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
>> 
  • created_atも更新すること、更新は「1.year.ago」を使う。現在の時刻から1年前の時間を算出してくれる。
>> user.update_attribute(:created_at,1.year.ago)
   (0.1ms)  SAVEPOINT active_record_1
  User Update (1.1ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2020-09-01 03:14:59.659028"], ["updated_at", "2021-09-01 03:14:59.659468"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
>> 

updateとupdate_attributeの違い
特定の属性のみを更新するため、検証を回避するためにある。

ユーザーを検証する

Userモデルにname,emailの属性を与えた。しかし今のままでは制限をかけていないため
空白文字や重複文字が登録されてしまう。
ActiveRecordでそのようなことを起こさないように
validationとして以下の検証を行う。

  • presence:存在性
  • length:長さ
  • format:フォーマット
  • uniqueness:一意性
  • confirmation:確認

有効性を検証する

テスト方法、有効なモデルのオブジェクトを作成、その属性のうち1つを有効でない属性に意図的に変更する。
バリデーションで弾かれるかを確認するテストを行う。

$ rails generate model User name:string email:string

このコマンドで作成された以下のファイルにテストコードを記述していく。

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

有効なオブジェクトに対してテストを書くために、
setupという特殊なメソッドを使い有効なUserオブジェクト(@user)を作成。
setupメソッド内に書かれた処理は、各テストが走る直前に実行される。
valid?メソッドを使いUserオブジェクトの有効性をテストできる。

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

assertメソッドを使いテストを行い、trueであれば成功、falseであれば失敗。
まだバリデーションがないため成功する。

ubuntu:~/environment/sample_app (modeling-users) $ rails test:models
Started with run options --seed 18121

  1/1: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.02371s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
演習
  • コンソールから、新しく生成したuserオブジェクトが有効(Valid)であることを確認。
>> user = User.new(name:"test user")                                          
=> #<User id: nil, name: "test user", email: nil, created_at: nil, updated_at: nil>
>> 
>> user.valid?
=> true
  • 前回生成したuserオブジェクトも有効であるか確認。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> 
>> 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

存在性を検証する

基本的なバリデーションはpersence存在です。渡された属性が存在するか否かを検証する。
ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証する。

name属性の存在をテスト
@user変数のname属性に対して空白文字をセット。
assert_notメソッドを使い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

  test "name should be present" do
    @user.name = "     "
    assert_not @user.valid?
  end
end

この時点でテストしてもエラーになるのでapp/models/user.rbに
name属性が存在していることをテストする記述が必要。

class User < ApplicationRecord
  validates :name, presence: true
end

validatesメソッドにpresence: trueという引数を与えて使う。
presence: true という引数は、要素が1つのオプションハッシュである。
コンソール起動してUserモデルに検証を追加した効果を見る。

$ rails console --sandbox
>> user = User.new(name: "", email: "michael@example.com")
>> user.valid?
=> false

user変数が有効であるかはvalid?メソッドで確認できる。
もしオブジェクトが1つ以上の検証(Validation)に失敗するとfalseを返す。
通ればtrueを返す。
なぜfalseになるかはerrorsを使えばよい。

>> user.errors.full_messages
=> ["Name can't be blank"]

今の状態でuser.saveをしてもfalseと返ってきて保存ができない。

保存ができないことが正常であるためrails test:modelsをすると
テストが通る。

ubuntu:~/environment/sample_app (modeling-users) $ rails test:models
Started with run options --seed 9721

  3/3: [================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.04484s
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

emailについても同じような記述を行いテストをする。

演習
  • 新しいユーザーuを作成、有効ではない(invalid)であるか確認。なぜ有効ではないか
    エラーメッセージを確認する。
>> u = User.new
   (1.3ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]
>> 
  • u.errors.messageを実行するとハッシュ形式でエラーが取得できることを確認する。
    emailに関するエラー情報のみ必要な場合どのような方法で取得したほうが良いか。
>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors.messages[:email]
=> ["can't be blank"]

長さを検証する

ここまでの制限で、空白だと弾かれるため名前を持つことが強制されることになった。
しかし、長さまでの制限はされていないためここではその設定を行う。
nameに51文字、emailに256文字の長さ制限を設けるようValidationの設定を行う。

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
    @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

rails cで簡単にuserのnameとemailの文字列を作ることができる

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
>> ("a" * 244 + "@example.com").length
=> 256

今の状態でrails tをしてもapp/models/user.rbに長さ制限
Validationの設定をしていないので弾かれるので設定を行う。

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end
演習
  • 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、valid?で弾かれるか確認。
>> user = User.new(name:"a"*51,email:"b"*256)
=> #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb...", created_at: nil, updated_at: nil>
>> user.valid?
=> false
  • 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるか
>> user.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]

フォーマットを検証する

name属性の検証は空白文字ではく、名前が51文字未満であるのみの制限のみしか与えていない。
email属性の検証は、メールアドレスであるか判定するため更に制限をかける。
%w[]を入れると簡単に文字列の配列を入れることができる。

>> %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メソッドは繰り返し処理を行う際に記述するメソッド。
railsではよく使うメソッドである。

登録できるメールアドレスとできないメールアドレスを複数用意してテストを行う。
登録できる:user@example.com
登録できない:user@example,com (.ではなく,であるため登録できない)

登録できるフォーマット
test/models/user_test.rb

  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
assert @user.valid?, "#{valid_address.inspect} should be valid"

.inspectメソッドを使うことによりどのアドレスが失敗したかを判別できる。

登録できないフォーマット
test/models/user_test.rb

  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

user@example,comと「,」カンマであるので登録ができないはずであるが
現時点で登録できないような設定をしていないので、そのままメールアドレスとして登録されてしまう。
rails t を実行してもREDで弾かれてしまう。

「、」がついているものなど明らかにメールアドレスではないものなどを
登録できないような設定を行うには正規表現を取り
条件に当てはまるもののみを登録できるような仕組みを記述します。
メールアドレスについては有効、無効のValidationは正規表現を使い判別する。

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 }
end

正規表現の確認方法はRubularを使い
冒頭の\Aと末尾の\zを含めずに試すこと。

Your regular expression:
正規表現

Your test string:
テストしたい項目を記述

正規表現 意味
/\A[\w+-.]+@[a-z\d-.]+.[a-z]+\z/i (完全な正規表現)
/ 正規表現の開始を示す
\A 文字列の先頭
[\w+-.]+ 英数字、アンダースコア(_)、プラス(+)、ハイフン(-)、ドット(.)のいずれかを少なくとも1文字以上繰り返す
@ アットマーク
[a-z\d-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す
. ドット
[a-z]+ 英小文字を少なくとも1文字以上繰り返す
\z 文字列の末尾
/ 正規表現の終わりを示す
i 大文字小文字を無視するオプション

emailのフォーマット検証した結果
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 }
end

大文字で始まる名前はRubyでは定数である。
rails tでテストをすると成功するがfoo@bar..comとドットを連続すると登録されてしまうが、
演習課題で解決させる。

演習
  • foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubular確認する。有効なメールアドレスがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認する。
  # 無効なメールフォーマット
  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 foo@bar..com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end
2つの連続したドットはマッチさせないようにする
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 }
end

ここまで設定できれば、foo@bar..comのメールアドレスを登録しようとすると
弾かれるので正常テスト終了。

一意性を検証する

メールアドレスの一意性を矯正するためvalidatesメソッドの:uniquenessオプションを使う。
一意性のテストはメモリ上だけではなく、実際にレコードをデータベースに登録する必要がある。
そのため重複したメールアドレスからテストする。

test/models/user_test.rb

 #メールアドレスを重複させるためのメソッドを追記
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end

@userと同じメールアドレスのユーザーは作成できないこと、@user.dupを使ってテストをする。
dupは同じ属性を持つデータを複製するためのメソッドである。
@userを保存後、複製されたユーザーのメールアドレスが既にデータベース内の存在するため、
ユーザー作成は無効になる。

上記のテストをパス(成功させる)ためにemailのValidationに
uniqueness:trueというオプションを追加する。

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

しかしこの構文だとメールアドレスの大文字小文字の区別はされない。
foo@bar.comFOO@BAR.COMと書いても同じ扱いになる。
大文字を区別せずにテストさせるようにする必要がある。

test/models/user_test.rb

  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

upcaseメソッドで登録したメールアドレスを重複させて、大文字に変換させる。
Railsコンソールで確認。

$ rails console --sandbox
>> 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

これ以上のまとめは難しい。

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

before_save→保存する前にコールバック
downcaseで入力されたemaiの引数を小文字に変換する。

演習
  • メールアドレスを小文字にするテストを追加する。そのテストコードではデータベースの値に合わせて
    更新するreloadメソッドと値が一致しているかどうか確認するassert_equalメソッドを使う。
    テストが動く確認するためにもbefore_saveの行をコメントアウトして弾かれるかテスト。
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
    assert_equal mixed_case_email.downcase, @user.reload.email
  end
app/models/user.rb

コメントアウト
  #before_save { self.email = email.downcase }
        Expected: "foo@example.com"
          Actual: "Foo@ExAMPle.CoM"
        test/models/user_test.rb:73:in `block in <class:UserTest>'

  9/9: [=======================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.09588s
9 tests, 18 assertions, 1 failures, 0 errors, 0 skips
  • before_saveコールバックをemail.downcase!に書き換えてみる。
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]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end

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

セキュアパスワードとはパスワードとパスワードの確認を入力させ、
それをハッシュ化したものをデータベースへ保存させること。
今回のハッシュはハッシュ関数を用いて入力されたデータを元に戻せない不可逆なデータにする処理を指す。
ユーザー認証(authenticate)する際に利用する。

ユーザー認証はパスワード送信、ハッシュ化、データベース内のハッシュ化された値との比較を行い
比較結果が一致すれば送信されたパスワードは正しいと認識されそのユーザーは認証される。
ハッシュ化された者同士を比較しているのでパスワードが盗まれたり覗き見されても安全性は保たれている。

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

セキュアなパスワード実装は、has_secure_passwordというRailsメソッドを呼び出すだけで終わる。
モデルにこのメソッドを追加すると

  • セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
  • 2つのペアの仮想的な属性(password,password_confirmation)が使え、存在性と値が一致するかのValidationも追加される。
  • authenticateメソッドが使える。
    このメソッドの機能を使うためにモデル内にpassword_digestの属性が含まれていること。

以下のマイグレーションを作成する必要がある。

$ rails generate migration add_password_digest_to_users password_digest:string

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

Userモデルにpassword_digest属性を追加し、Gemfileにbcryptを追加したのでUserモデル内で
has_secure_passwordが使えるようになる

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
演習
  • userオブジェクトに有効な名前とメールアドレスを作りvalid?を実行する。
>> user = User.new(name:"test",email:"test@test.com")
   (1.6ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: "test", email: "test@test.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
  User Exists? (0.8ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "test@test.com"], ["LIMIT", 1]]
=> false
  • 失敗したときのメッセージを表示させる。
>> user.errors.full_messages
=> ["Password can't be blank"]

パスワードの最小文字数

パスワードを簡単に当てられないようにするため、パスワードの最小文字数を設定する。
最小を6文字で、パスワードの長さが6文字以上であることを検証するテストを実施。

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

多重代入が使われている。パスワードとパスワード確認に対して同時に代入をしている。
user.rbではmaximumを使いユーザー名の最大数を制限したが、
似たようにminimumというオプションを使い最小限のValidationを実装することができる。

validates :password, length: { minimum: 6 }

空パスワード入力させないように存在性のValidationも使うようにする。

  validates :password, presence: true, length: { minimum: 6 }
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
演習
  • 有効な名前とメールアドレスでも、パスワードが短いとuserオブジェクトが有効にならないことを確認。
>> user = User.new(name:"test",email:"test@test.com",password:"1111")
   (0.4ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: "test", email: "test@test.com", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
 
>> user
=> #<User id: nil, name: "test", email: "test@test.com", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
>> user.valid?
  User Exists? (1.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "test@test.com"], ["LIMIT", 1]]
=> false
  • エラーメッセージの確認
>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]

ユーザーの作成と認証

Userモデルの基本部分が完了。
ユーザー情報表示ページを作成するときに備えて、データベースに新規ユーザーを一人作成する。
Userモデルにhas_secure_passwordを追加した効果も見る。
現時点ではwebからのユーザー登録はできない。
実際にユーザーを登録するためsandboxモードではなくrails consoleを使う。

$ rails console
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2019-08-22 03:15:38", updated_at: "20**-08-22 03:15:38",
password_digest: [FILTERED]>

cloud9の場合は都度ダウンロードする必要があるが、データベースに反映されているか
/sample_app/db/development.sqlite3をダウンロードしてBrowser for SQLiteを開きデータベースを確認する。

コンソール上でpassword_digestを確認する。

>> user = User.find_by(email: "michael@example.com")
>> user.password_digest
=> "$2a$12$V4TEcxCwzcq6mRQZixos/e5bOrw5u1cmQpzDUC48UBzOMiSJKkq7G"

foobarというパスワード文字列をハッシュ化した結果が表示されている。
authenticateメソッドも使えるため比較する。

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

間違ったパスワードを入れるとfalseを返す。

>> user.authenticate("foobar")
=> #<User id: 1, name: "yamada", email: "michael@example.com", created_at: "2021-09-13 02:04:26", updated_at: "2021-09-13 04:12:07", password_digest: [FILTERED]>
>> 

正しいパスワードを入れると対象となるユーザー情報が表示される。

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

先頭に!!を入れることでtrueを返す。

演習
  • コンソール再起動を行いuserオブジェクトを検索。
>> user = User.find_by(email: "michael@example.com")
  User Load (1.5ms)  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-09-13 02:04:26", updated_at: "2021-09-13 02:04:26", password_digest: [FILTERED]>
  • 名前を新しい文字列に置き換え、saveメソッドで更新してみる。失敗した内容を確認する。
>> user.name = "yamada"
=> "yamada"
>> 
>> user.save
   (0.1ms)  begin transaction
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "michael@example.com"], ["id", 1], ["LIMIT", 1]]
   (0.1ms)  rollback transaction
=> false

>> user.errors.full_messages
=> ["Password can't be blank", "Password is too short (minimum is 6 characters)"]

ユーザー名のみを変更しようとするが、パスワード、パスワード確認も更新するようエラーで弾かれている。
ここではユーザー名のみ更新したいため、下記のコマンドを実行となる。

  • userの名前を更新する。
>> user.update_attribute(:name,"yamada")
   (0.1ms)  begin transaction
  User Update (5.9ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "yamada"], ["updated_at", "2021-09-13 04:12:07.503505"], ["id", 1]]
   (9.7ms)  commit transaction
=> true

最後

この章はUserモデルを作成、name属性、email属性、パスワード属性を加えた。
それぞれの値を制限するバリデーションを追加。
パスワードをセキュアに認証できる機能も実装。

まとめ

  • マイグレーション使い、アプリケーションのデータモデルを修正
  • ActiveRecordを使うと、データモデルを作成、操作するための多数のメソッドを使える。
  • よくあるバリデーションは存在性(presence)、長さ(length)、フォーマット(format)、一意性(uniqueness)がある。
  • データベースにインデックスを追加することで検索効率が上がる。データベースレベルでの一意性を保証するためにも使われる。
  • has_secure_passwordメソッドを使うことで、モデルに対してセキュアなパスワードを追加できる。

項(subsection)

目(演習)(subsubsection)

#4. 用語のまとめ

用語 意味
モデル(Model) データモデルとして扱うデフォルトのデータ構造のこと(MVCのMである)
Active Record データベースとやり取りするデフォルトのRailsライブラリ、データオブジェクトの作成、保存、検索のメソッドも持っている。
マイグレーション データベースに与える変更を定義したchangeメソッドの集まり
%w[] %w[user@foo.com test@user.com]と簡単に配列を入れることができる。
.dupメソッド 同じ属性を持つデータを複製するメソッド
セキュアパスワード パスワードとパスワードの確認を入力させ、
それをハッシュ化したものをデータベースへ保存させること。
#5. 感想
  • 節の感想

#6. おわりに

章を改めて振り返っての感想
次の目標

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?