LoginSignup
6
1

More than 3 years have passed since last update.

Rails Tutorial 第6版 学習まとめ 第6章

Posted at

概要

この記事は私の知識をより確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・

出典
Railsチュートリアル第6版

この章でやること

ユーザーモデルを作成する。以降の章でもユーザーモデルを作りこんでいく。

Userモデル

Webアプリならほぼ確実に存在するユーザーのデータ
ID、名前、メールアドレス、パスワード、住所、etc...
これらのデータを作成した際保存するデータベースのデータ構造から作る。
このデータ構造をModelと呼ぶ
データベースとのやり取りをするライブラリをRailsではActiveRecordと呼ぶ。
ActiveRecordがあることによって通常データベースを操作するのに必要なSQLを意識する必要がない。
またRailsではマイグレーションという機能を使ってデータベース作成からSQLを切り離している。
おかげでRails単体でデータベース操作までできてしまう。

データベースの移行

さっそくUserモデルを作成してみる。
ここで覚えておきたいのが
コントローラ名:複数形 → Users、Micropostsなど
モデル名:単数形 → User、Micropostなど
のように命名する。
rails g model User name:string email:string
このコマンドで文字型のカラムname,emailを持つUserモデルが作成される。
ちなみにモデルをgenerateコマンドで作成すると
マイグレーションファイル、テストファイル、フィクスチャも同時生成してくれて非常に便利。

今回はname,emailの二つの属性を定義したがRails側で自動的に3つのカラムが追加される。
それがこちら

users
id integer
name string
email string
created_at datetime
updated_at datetime

Railsチュートリアル第6版より
https://railstutorial.jp/chapters/modeling_users?version=6.0#fig-user_model_initial

idはそのままの意味でレコードを一意に識別するためのユニークな値
created_atはそのレコードが作成された時間
updated_atはそのレコードのデータが更新された時間を表す。

先程のgenerateコマンドでモデルと同時にマイグレーションファイルも同時生成されたので
さっそくマイグレートしていく。
rails db:migrate

演習

1.スキーマとマイグレーションを比較してみる

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end

どちらもidを定義する行はないがtimestampsがスキーマではそれぞれ細かく定義されている。
感覚的にはマイグレーションで設計したデータベースの実行結果がスキーマといった感覚。

2.rails db:rollback

== 20200608105854 CreateUsers: reverting ======================================
-- drop_table(:users)
   -> 0.0047s
== 20200608105854 CreateUsers: reverted (0.0125s) =============================

drop_tableコマンドでテーブルが削除されている。

3.rails db:migrateで先ほどと同じようにテーブルが作成される。(結果は省略)

modelファイル

Userモデルのクラス継承構造を復習を兼ねて確認してみる。

$ rails c
Running via Spring preloader in process 7640
Loading development environment (Rails 6.0.3)
>> User.superclass
=> ApplicationRecord(abstract)
>> User.superclass.superclass
=> ActiveRecord::Base

User < ApplicationRecord < ActiveRecord::Base
このような構造で、つまりUserモデルはActiveRecord::Baseの機能をすべて継承している。
そのため、UserデータベースもActiveRecordのメソッドを使って操作することができる。

演習

1.↑でやった内容と大体同じ。

>> user = User.new
   (0.4ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.class.superclass
=> ApplicationRecord(abstract)

2.これは↑でやっていることと全く同じなので省略。

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

試しにユーザオブジェクトを作成してみる。
データベースを変更したくない(今回のような試しの時)はsandboxモードを使うよい。
rails console --sandboxrails c -s
このモードはコンソール終了時に変更をすべてロールバック(取り消し)してくれるので試しの時は便利。

ここでの要点をまとめる。
・UserオブジェクトはUser.newで作成可能。

・User.newで作成したUserオブジェクトはUser.saveで保存できる。(saveしないとデータベースに格納されない)

・作成したUserオブジェクトuserが有効かどうかはuser.valid?で返る論理値でわかる。

・Userオブジェクトをnewメソッドで作成した段階でcreated_at,updated_atはnilになっているがsaveした時の時刻
が代入される。

・User.createを使えばオブジェクトの作成と保存が同時に行える。

演習

1.nameとemailに何も入れていないとnilとなりNilClassとなってしまうので注意。

>> user.email.class
=> String
>> user.name.class
=> String

2.ActiveSupport::TimeWithZone

>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

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

要点をまとめる。
・User.find(ユーザーのid)で該当するユーザーを返す。

・User.find_by(属性: 検索値)で該当するユーザを返す。

・User.firstでデータベースの最初のユーザを返す

・User.allでデータベースに登録されているすべてのレコードを配列のような形式
(ActiveRecord_Relation)として返す。

演習

1.find_by_nameも同じ動作をするが基本find_byの引数にハッシュを渡す使い方を用いる。

>> user = User.find_by(name: "take")
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "take"], ["LIMIT", 1]]
=> #<User id: 2, name: "take", email: "engineer@gmail.com", created_at: "2020-06-08 12:38:02", updated_at: "2020-06-08 12:38:02">
>> user = User.find_by_name("take")
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "take"], ["LIMIT", 1]]
=> #<User id: 2, name: "take", email: "engineer@gmail.com", created_at: "2020-06-08 12:38:02", updated_at: "2020-06-08 12:38:02">
>> user  = User.find_by_email("engineer@gmail.com")

2.

>> user = User.all
  User Load (0.2ms)  SELECT "users".* FROM "users" LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: nil, email: nil, created_at: "2020-06-08 12:32:30", updated_at: "2020-06-08 12:32:30">, #<User id: 2, name: "take", email: "engineer@gmail.com", created_at: "2020-06-08 12:38:02", updated_at: "2020-06-08 12:38:02">]>
>> user.class
=> User::ActiveRecord_Relation

3.Userの要素数(数)を知りたい→lengthで数えられそう。→メソッドチェーンでつなげれば数えられる。
このような詳しく知らないクラスも何となく扱いがわかることをダックタイピングと呼ぶ。

>> User.all.length
  User Load (0.2ms)  SELECT "users".* FROM "users"
=> 2

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

ユーザーオブジェクトの内容を更新する方法は2通り
・直接代入してsave

>> user.name = "takemo"
=> "takemo"
>> user.save

・updateメソッドで即更新

>> user.update(name: "take",email: "taro.kono@gmail.com")

1つの属性だけを更新するときはupdate_attribute(:name,"take")のようにハッシュではなく属性名、値という渡し方をする。
2つ以上の属性を更新するときはupdate(name:"take",email:"tutorial@gmail.com")といったようにハッシュを渡す。

ちなみにupdate_attribute(:name,"take")update(name:"take")のようにどちらも一つの属性を更新するが
引数にハッシュを使わない書き方(前者)だとこれから学習するデータの「検証」を回避するという効果もある。

演習

1.

>> user.name = "Saori Yoshida"
=> "Saori Yoshida"
>> user.save
   (0.1ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Saori Yoshida"], ["updated_at", "2020-06-08 13:50:14.777986"], ["id", 2]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

2.

>> user.update_attribute(:email,"saori.yoshida@gmail.com")
   (0.1ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "saori.yoshida@gmail.com"], ["updated_at", "2020-06-08 13:56:20.532390"], ["id", 2]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

3.

>> user.update_attribute(:created_at,1.year.ago)
   (0.1ms)  SAVEPOINT active_record_1
  User Update (0.2ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2019-06-08 13:57:32.050811"], ["updated_at", "2020-06-08 13:57:32.051252"], ["id", 2]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

ユーザーを検証する

前の演習などで何回か検証という言葉が出ているが
ようはデータの制限のこと。名前のないユーザや間違った形式のメールアドレス、短すぎるパスワードが
データベースに登録されてはいけない。
それを防ぐのが検証(Validation)である。

有効性を検証する

ここの部分はテスト駆動開発で進めていく
最初のユーザーモデルのテストはこんな感じ

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

ここでは各テスト(現時点では一つだけだが)を行う前に実行されるsetupメソッドで
テスト用のユーザーを@user変数に定義し、

should be validテストで@userが有効なデータであることをテストしている。

今の段階ではバリデーションを追加しておらずどんなデータも有効になっているのでテストは必ずパスする。

rails test:models
でmodelに関係するテストだけ走らせる、
がどうせguardで自動でテストしてるし、手動で走らせるときもすべてのテストを流しているので
自分は使ったことはない…
テストそのものが増えて重くなったら使うのもありかも。

演習

1.

>> new_user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> new_user.valid?
=> true

2.

>> user.valid?
=> true

当たり前だがバリデーションがないためどんなデータだろうが有効になる。

存在性を検証する

一番単純で基本的なバリデーション、存在性(presense)を追加してみる
これを追加することで要素が存在する場合のみ有効とすることができる。

さっそく先にテストを書いてみる。

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

今回追加したこのテストではname属性に空白(存在しない)を入れ
assert_not(引数がfalseの時にパスする)で@user.valid?
とすることでuserが有効でないときテストにパスする。
つまり
空白を入れ、nameが存在しない場合はuserが有効でないことをテストする。

現段階でテストは失敗するので
さっそくバリデーションを追加してテストをパスさせることにする。

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

ここで書き方に突っかかる人がかなり多いと思うが、
この行を省略せずに書くと
validates(:name,{presence: true})
validatesメソッドへ第一引数に:name、第二引数(オプション)にハッシュを渡している。
Railsは効率化のために様々な省略記法を用意している。
ここではメソッドの()は省略できる、メソッドの引数の最後のハッシュは{}を省略できる、
という二つの省略記法を使っている。
今後も当たり前のようにこのような省略記法がでてくるので
可能な限り解説していくが、今のうちに慣れておくといい。

バリデーションを追加したので
nameが存在しないと無効になる。

コンソールでバリデーションが効いているか確かめてみる。
```

user = User.new(name: "",email:"user@gmail.com")
(0.1ms) begin transaction
=> #
user.valid?
=> false
```

ちなみに無効の状態ではデータベースに保存はできない

>> user.save
=> false

このように失敗するとエラーオブジェクトが作られエラーメッセージが保存される。

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

コンソールを抜けテストを流してみるとテストはパスする。
email属性もテストを書いてからバリデーションを追加してみる。(テスト駆動開発)

※豆知識>>>valid?でそのオブジェクトが有効かどうかわかるがinvalid?で無効かどうかもわかる。
テストの書き方を統一するためにすべてvalid?で書き進める。

emailの存在性のテスト

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

ここではテストは失敗する。
emailのバリデーションを追加する
validates :email, presence: true

テストがパスする。

演習

1.nameとemailの存在性のバリデーションにひっかかっているから 

>> u = User.new
   (0.1ms)  begin transaction
=> #<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"]

2.messagesがハッシュ形式になっているので[:email]と指定して取り出せばよい。

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

長さを検証する

名前はとりあえず手ごろな文字数で50文字以内という制限を与える。
メールアドレスも標準的な文字数制限の255文字以内という制限を与えることにする。
バリデーションを追加する前にテストを書いてみる。

  test "name should not be 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

aを大量に入れて文字数を稼いでいる。
この時点ではテストは通らない。

  validates :name, presence: true, length:{maximum: 50}
  validates :email, presence: true, length:{maximum: 255}

これで文字数でバリデーションがかかりテストがパスする。

演習

1.

>> user = User.new(name:"a"*51,email:"a"*256)
   (0.1ms)  begin transaction
=> #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", created_at: nil, updated_at: nil>
>> user.valid?
=> false

2.is too long(長すぎる)と怒られる。

>> user.errors.messages
=> {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}
フォーマットを検証する

様々なサイトでよく見かける、アカウントの新規作成時にメールアドレスに適当な文字を入れると
「正しいメールアドレスを入力してください」と怒られることが多々ある。
このように文字数など以外にメールアドレスとしての所定の形式を満たしているかどうかを検証していく。

%w[]を使うと文字列の配列を簡単に作れるのでこれを使って無効なメールアドレスリストを作成していく。

有効なメールアドレスがバリデーションに引っかからないことを確認するテスト

  test "email validation should accept valid address" do
    valid_addresses = %w[user@example.com User@foo.COM A_US-ER@foo.bar.org]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid? , "#{valid_address.inspect} should be valid"
    end
  end

どのメールアドレスでテストが失敗したかわかるように第二引数でエラーメッセージを追加している。

次は無効なメールアドレスがしっかりバリデーションに引っかかることを確認するテスト

  test "email validation should reject invalid address" 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

この時点ではテストは通らない。
テストをパスさせるため、
メールアドレスのバリデーションを追加していく。
メールアドレスのフォーマットを検証するためには正規表現を書かなければならない。
とても分かりやすくまとめているサイトがあったので紹介する。
https://murashun.jp/blog/20190215-01.html
もちろんチュートリアル上で紹介しているRubularも便利。

Emailの正規表現を定義していく。Rubyでは定数は大文字で始める。

user.rb
class User < ApplicationRecord
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[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}

end

チュートリアル上に説明がなかったので補足。
[\w+-.]の部分は英数字、アンダースコア、プラス、ハイフン、ドットを表している。
\wは英数字、アンダースコア +-.はそれぞれに対応しているが余計な\が一つ多い感じがする
これは直後の文字をエスケープする効果で、直後のハイフンは[]内では特別な意味を持っており、
範囲指定の効果a-zをもっている。したがってエスケープして本来の-として表示するために
\でエスケープするからこのような表現になる。

この時点でテストがパスする。

この正規表現だとsample@gmail..comのようなドットの連続をはじけないので次の演習で修正していく。

演習

1.image.png

こういった感じで正しいものはMatchする。

2.foo@bar..comのようなアドレスをはじきたい。
しかし、ドットは1つまで、といったような指定だと@softbank.ne.jpのようなアドレスまではじかれてしまう。
そこでこの表現
/\A[\w+\-.]+@[a-z\d\-.]+(\.[a-z\d\-.]+)*\.[a-z]+\z/i
これは(\.[a-z\d\-.]+)で.〇〇〇がもう一つあるパターンを追加している。

3.1と大体同じ 
省略

一意性を検証する

メールアドレスはユーザー名として使われる(ログイン時など)ためユーザーどうしで重複してはならない。
ここでは一意性のバリデーションを追加する。

User.newだとメモリ上にオブジェクトを作成するだけでデータベースを照会しても重複が発生しない。
つまりデータベースにデータAを入れておき、全く同じデータBをテストすればいい。

  test "email address should be unique" do
    duplicate_user = @user.dup 
    @user.save
    assert_not duplicate_user.valid?
  end

ここで修正
メールアドレスは通常大文字小文字が区別されないので
example@email.comEXAMPLE@EMAIL.COMが同じ扱いになる。
これを同時にテストするためには

  test "email address should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email.upcase!
    @user.save
    assert_not duplicate_user.valid?
  end

コピーしたアドレスをupcaseして比較する。
バリデーションはuniquenessオプションにcase_sensitive: falseを指定するだけで
そのメールアドレスは一意でなければならないが大文字小文字は考えない(どちらも同じもの)というバリデーションになる。

しかしまだ欠陥がある。
今追加しているバリデーションはデータベースに登録する前のデータを作成した段階で検証しているため
連続で登録リクエストを送信した際に万が一トラフィックが多く重い状態の時に検証を潜り抜けてしまった際、
そのままデータベースに登録されてしまう。
これを防ぐにはデータ作成時だけでなく、データ登録時もユニークであることを強制すればいい。

データベースのemailカラムにインデックスを追加し、インデックスに一意性を強制することで実現する。
※インデックスとはデータベースの索引のようなもので追加することでデータの検索速度の向上などにつながる。
ここでは詳しく説明しないがすべてのデータに対してインデックスを追加すればいいということではないので注意

インデックスを追加するためにマイグレーションファイルを作る
$ rails g migration add_index_to_users_email

マイグレートしてインデックスを追加するとデータベースのemailインデックスに一意性を強制したのに対して
テストDBのサンプルデータであるfixtureのemail属性が被っているため、エラーをはきまくる。
このfixtureも現時点は使わないので中身を削除しておく
今までfixtureの値が間違っているのにもかかわらずテストがパスしていたのはデータベースに登録する前の
データの検証に対するテストだったから(fixtureはテストDBにあらかじめ登録しておくデータ)

しかしこれでもまだ足りない。
データベースによってはインデックスの大文字小文字を区別する場合とそうでない場合がある。
ユーザーモデルのバリデーションではuniqueness:{case_sensitive:false}で対応できたが
このような場合はデータベース登録前にすべてのデータを小文字に変換してしまう手法をとる。

ActiveRecordのbefore_saveメソッドを使って、データのセーブ前にすべて小文字に変換するように指定する。
before_save { self.email.downcase! }
これによりsave時にコールバックが呼ばれemailはすべて小文字に変換される。
よって、uniqueness:{case_sensitive: false}
uniqueness: trueに戻してしまってOK
これは保存時に必ず小文字にするので問題ないから。

uniqueness: trueに戻すとテストでエラーをはく。
バリデーションで大文字小文字が別者扱いされるように戻ったから。
したがって、テストも大文字変換している部分を元に戻す。

  test "email address should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
演習

1.コメントアウトした時の結果だけとりあえず乗せる。

 FAIL["test_email_address_should_be_saved_as_lower-case", #<Minitest::Reporters::Suite:0x00007f75a0e7b310 @name="UserTest">, 0.06644777600013185]
 test_email_address_should_be_saved_as_lower-case#UserTest (0.07s)
        Expected: "sample.app@gmile.com"
          Actual: "SAMple.ApP@GmIlE.cOm"
        test/models/user_test.rb:58:in `block in <class:UserTest>'

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

Finished in 0.07422s
9 tests, 15 assertions, 1 failures, 0 errors, 0 skips

期待値と実際の値といった形でエラーがでていてわかりやすい。
コメント解除でコールバックが呼ばれてすべて小文字変換されてから保存されるようになるので
テストもパスする。

2.もとからemail.downcase!のほうで書いていたため、省略。
いちおう説明すると!をつけることで「破壊的メソッド」になり、今回の場合はemailをdowncaseしてその結果で「上書き」されるようになる。

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

ユーザのパスワードを実装していく。
ただデータベースにパスワードを登録するのではなくハッシュ関数でハッシュ化したものを登録する。
そのまま登録するよりもリスクがかなり低くなるので必須。

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

パスワードの実装には便利なメソッドがある
has_secure_passwordを追加するだけで8割終わる。
このメソッドをモデル内で呼び出すことで
・ハッシュ化したパスワードをpassword_digest属性に保存できるようになる。」
・仮想的な属性password,password_confirmationが使えるようになると同時に
存在性とこの2属性の値が一致するかのバリデーションも追加される。
authenticateメソッドが使えるようになる。

もちろん他にも準備があって、password_digestは当たり前だがモデルに含まれてなければならない。
そのためpassword_digest追加用マイグレーションファイルを作成する。

rails g migration add_password_digest_to_users password_digest:string

マイグレーションファイルのファイル名末尾をto_usersとすることでusersテーブルへの変更だとRailsが解釈してくれる。
ファイル名の後にpassword_digest:stringも書けば、usersテーブルにstring型のpassword_digestを追加するという
マイグレーションを自動作成してくれる。ぐう便利。

マイグレーションファイルは前述のとおり変更の必要がないのでそのままマイグレートしてしまおう。

またパスワードのハッシュ化のために最先端のハッシュ関数であるbcryptが必要になる。
bcryptを使うためにGemfileにbcryptを追加する。

gem 'bcrypt', '3.1.13'

追加したらbundleをたたく。

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

has_secure_passwordを使う準備が整ったのでuserモデルにhas_secure_passwordを追加する。
仮想的な属性password,password_confirmationの存在性と値が同じかどうかのバリデーションが引っかかっている
テストの@userの属性にこれら二つの属性の値も渡してやることでパスさせる。

演習

1.

>> user1 = User.new(name:"takemo",email:"take@gmail.com")
=> #<User id: nil, name: "takemo", email: "take@gmail.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user1.valid?
  User Exists? (0.1ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "take@gmail.com"], ["LIMIT", 1]]
=> false

2.passwordの存在性のバリデーションで引っかかる。

>> user1.errors.full_messages
=> ["Password can't be blank"]

パスワードの最小文字数

パスワードに最小文字数のバリデーション(6文字)をかける。
まずはテストを先に書く。
パスワードが空白もしくは6文字未満の時エラーをはくようにテストを書く。

  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

ここで使っている@user.password = @user.password_confirmation = " " * 6
は多重代入を使って、同時に二つの要素に代入している。

テストを書いたのでバリデーションを実装する。
最小文字数なのでmaximumの逆のminimumオプションを使う。

validates :password, presence: true, length:{minimum: 6}

バリデーションを追加した段階でテストはパスする。

演習

1.

>> user1 = User.new(name:"takemo",email:"take@gmail.com",password:"foo",password_confirmation:"foo")
   (0.1ms)  begin transaction
=> #<User id: nil, name: "takemo", email: "take@gmail.com", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
>> user1.valid?
  User Exists? (0.6ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "take@gmail.com"], ["LIMIT", 1]]
=> false

2.

>> user1.errors.messages
=> {:password=>["is too short (minimum is 6 characters)"]}

ユーザーの作成と認証

今後のインデックスページ作成に備えてdevelopment環境でも新規ユーザーを一人作成してみる。
webからのユーザー作成機能はまだ未実装なのでrails consoleを使って追加する。

>> User.create(name:"take",email:"take@gmail.com",password:"foobar",password_confirmation:"foobar")
   (0.4ms)  SELECT sqlite_version(*)
   (0.1ms)  begin transaction
  User Exists? (0.8ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "take@gmail.com"], ["LIMIT", 1]]
  User Create (3.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?)  [["name", "take"], ["email", "take@gmail.com"], ["created_at", "2020-06-10 12:51:23.959140"], ["updated_at", "2020-06-10 12:51:23.959140"], ["password_digest", "$2a$12$xkZDNGfs2Dnjzbm5XxfCfu.sB3I4ug4PFtTHukKdp0EF9YLLsE5Sm"]]
   (6.7ms)  commit transaction
=> #<User id: 1, name: "take", email: "take@gmail.com", created_at: "2020-06-10 12:51:23", updated_at: "2020-06-10 12:51:23", password_digest: [FILTERED]>

password_digestをのぞいてみるとハッシュ化されたパスワードが格納されている。

>> user.password_digest
=> "$2a$12$xkZDNGfs2Dnjzbm5XxfCfu.sB3I4ug4PFtTHukKdp0EF9YLLsE5Sm"

また以前の説明通りhas_secure_passwordメソッドによりauthenticateメソッドが有効化されているので
試しに使ってみる。

>> user.authenticate("foo!baka")
=> false

間違ったパスワードに対してはfalseを返している。

>> user.authenticate("foobar")
=> #<User id: 1, name: "take", email: "take@gmail.com", created_at: "2020-06-10 12:51:23", updated_at: "2020-06-10 12:51:23", password_digest: [FILTERED]>

正しいパスワードを与えるとユーザーオブジェクトを返す。(true)
!!を使えば論理値に変換して返すこともできる→!!user.authenticate("foobar")

演習

1.

>> u = User.find_by(email:"take@gmail.com")
   (0.4ms)  SELECT sqlite_version(*)
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "take@gmail.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "take@gmail.com", created_at: "2020-06-10 12:51:23", updated_at: "2020-06-10 12:51:23", password_digest: [FILTERED]>

2.仮想的な属性passwordが空なためバリデーションに引っかかってしまう。
以前入力されたパスワードはハッシュ化されてpassword_digestとしてDBに保存されるのでパスワードは毎度入力する必要がある。

>> u.name = "taketake"
=> "taketake"
>> u.save
   (0.1ms)  begin transaction
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "take@gmail.com"], ["id", 1], ["LIMIT", 1]]
   (0.1ms)  rollback transaction
=> false

3.update_attributeメソッドを使えば検証を回避して直接値を更新できる。

>> u.update_attribute(:name,"taketake")
   (0.1ms)  begin transaction
  User Update (2.1ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "taketake"], ["updated_at", "2020-06-10 13:09:09.579353"], ["id", 1]]
   (6.4ms)  commit transaction
=> true

最後に

ここまでの変更をGitにコミットし
push、デプロイまで行っておく。
本番環境データベースのマイグレートも忘れず行う。

前の章へ

次の章へ

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