kuma0319のブログ

webエンジニアになるためのアウトプットブログ

Ruby on Railsチュートリアル第6章

Railsチュートリアルの第6章を進めていきます。

第6章(ユーザーのモデルを作成する)

Userモデル

モデルの作成

rails gでモデルを作成。モデルは単数形。db:migrateも実行。

$ rails generate model User name:string email:string
$ rails db:migrate

モデル生成の際には同時にマイグレーションファイルも生成される。
create_tableメソッドを呼び、テーブル名はデータの集まりなので複数形のusers。

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

      t.timestamps
    end
  end
end
カラム名 データ型
id integer
name string
email string
created_at datetime
updated_at datetime
Active Recordの操作(作成、保存、更新、削除)
  • User.newで新規ユーザーオブジェクトをメモリ上に作成。Active Recordの設計で、引数無しの場合はnil
  • validメソッドで有効性の検証が可能。有効かどうかなだけで、DBへ保存されていることを確認する訳ではない。
  • saveメソッドでデータベースへ保存。この際にモデルに対応するid属性とtimestampも変更される。戻り値は真偽値。
  • createnewsaveを兼ねたメソッド。戻り値は真偽値ではなく、オブジェクト自身を返す。
  • createの逆はdestory。削除されたオブジェクトはメモリ上には残っている。
  • saveの前であれば、データベースの情報を元にreloadでオブジェクトを再読み込み可能。
  • 更新と保存を一括でおこなう操作がupdate。更にupdate_attributeは特定の属性のみであればバリデーションにかからず更新が可能。
Active Recordの操作(検索)

検索で使用される、find、find_by、whereの違いについてまとめておきます。

  • User.findは引数に指定したidのユーザーを探す。idが分かっている場合に使用。該当するidのユーザーがいない場合は例外ActiveRecord::RecordNotFoundが発生する。
  • User.find_byは引数に単一の属性を指定してユーザーを検索する。idもしくはid以外が分かっている場合(メールアドレスなど)で使用。返ってくる結果は最初の1件のみ。該当するユーザーがいない場合はnilが返ってくる。
  • whereは引数に一つ以上の属性を指定してユーザーを検索する。また、該当する全ての結果。また、find_byと微妙に違う点として戻り値の形式が異なっており、whereの結果に対して追加でクエリ出来る。

ユーザーの検証

有効性の検証

valid?メソッドを使用。

[test/models/user_test.rb]
require "test_helper"

class UserTest < ActiveSupport::TestCase

  #@userを使えるようにsetupメソッドを実行
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  #ユーザーの有効性を検証
  test "should be valid" do
    assert @user.valid?
  end
end
存在性の検証

名前が存在していることを検証するためのテストを先に書く。

[test/models/user_test.rb]
  #ユーザーの存在性を検証
  test "name should be present" do
    @user.name = '   '
    assert_not @user.valid?
  end

ユーザーモデルにnameの存在性バリデーションを追加。
validates :name, presence: trueの書き方は、validatesメソッドに対して属性として:name、オプションとしてpresence: trueを渡している。presence: trueはハッシュであるが、最後の引数のため{}は省略が可能。

[app/models/user.rb]
class User < ApplicationRecord
  validates :name, presence: true
end

emailの存在性検証も全く同じ。

長さの検証

名前は50文字、メールアドレスはStringの上限の255文字としてバリデーションを設定。
まずはテストを記述する。

[test/models/user_test.rb]
  #ユーザー名の長さを検証
  test "name length" do
    @user.name = 'a' * 51
    assert_not @user.valid?
  end

  #メールアドレスの長さを検証
  test "email length" do
    @user.email = 'a' * 256
    assert_not @user.valid?
  end

モデルに長さのバリデーションを追加。

[app/models/user.rb]
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end
ハッシュの省略について

ここの書き方がだいぶ混乱したのでまとめておきます。
ハッシュには、最後の引数である場合はハッシュの{}を省略可能という法則があるが、validates :name, presence: true, length: { maximum: 50 }は、第1引数が:name、第2引数がpresence: true、第3引数がlength: { maximum: 50 }となるため、第2引数のpresence: trueには{}が必要なのでは無いかと思いました。
ただ、よく考えるとvalidates( :name, {presence: true, length: { maximum: 50 }}という記述になるので、第1引数は:nameの属性で、第2引数がオプションハッシュの{presence: true, length: { maximum: 50 }}になるということですね。(多分)
この記述は4章のstylesheet_link_tag "application", "data-turbo-track": "reload"の書き方と全く同じとなりますね。特にこれまで気にせず読んでいませんでしたが、改めて考えるとrubyの省略記法には慣れが必要そうです。

フォーマットの検証

まずはメールアドレスに対するテストを記述。

[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

  #メールアドレスのフォーマット検証、不正なメールアドレス
  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

メールフォーマットに対するバリデーションを追加。正規表現を使用。VALID_EMAIL_REGEXは定数。
正規表現を試せるwebサイトのRubular(https://rubular.com/)を使用するとよい。

[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
一意性の検証

メールアドレスの一意性検証に関する前提
一意性をテストする際は、メモリ上だけでなく、実際にデータベースへレコードを登録している必要がある。そのためメソッドにはsaveupdateを加える。 メールアドレスは大文字と小文字は区別せず、全て小文字として扱うことが通例。

メールアドレスの重複についてテスト。dupメソッドではオブジェクトのコピーを作成可能。

  test "email addresses should be unique" do
    duplicate_user = @user.dup    #dupメソッドでコピーを作成。
    @user.save                    #@userをDBへ保存。
    assert_not duplicate_user.valid?
  end

モデルに一意性のバリデーションを追加。uniqueness: { case_sensitive: false }とすることで、大文字と小文字を区別しない検証が可能。

[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カラムにインデックス(index)を追加し、そのインデックスに一意性の制約を付けるだけで解決可能。
更にindexは索引機能のようなもので、データベースに対する全表スキャンの対策にもなる。

マイグレーションファイルを生成(rails g migration)し、add_indexメソッドでユーザーのemailに対して一意性を制約する。

[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

これまでのテストの時点では、データベース間での一意性検証は無かったためテストパスしていたが、テスト用データfixtureを編集しておく。
また、実質的にデータベースレベルでメールアドレスの一意性を保つためには、そもそもメールアドレスは全て小文字として扱う方が理にかなっている。そのため、before_savedowncase化しておく。

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

セキュアなパスワード

ユーザーの認証は、パスワードの送信→ハッシュ化→データベース内のハッシュ化された値との比較、という手順でおこなわれる。
ハッシュ化は、暗号化とが類似しているが明確に異なる。暗号化は元に戻す複合化が可能であるが、ハッシュ化は元に戻せず不可逆的な処理(∵ソルト化(ランダム)ハッシュを追加している)。

has_secure_password
  • ハッシュ化したパスワードを、password_digest属性に保存できる。
  • 2つの仮想的な属性(passwordとpassword_confirmation)が使える。(データベースカラムに存在はしていない)
  • authenticateメソッド(引数のパスワードをハッシュ化した値とpassword_digestを比較)が使える。真偽値を返し、trueであればオブジェクトも返す。 *パスワードのハッシュ化にはbcryptgemを使用する必要がある。

password_digestをカラムに追加するマイグレーションを生成する。末尾をto_usersとすることでテーブルが認識出来る。

$ rails generate migration add_password_digest_to_users password_digest:string

モデルに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

has_secure_passwordの仮想的なデータ属性であるpasswordとpassword_confirmationをtest userに追加しておく。

[test/models/user_test.rb]
  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
パスワードの最小文字数設定

空で無いことと、6文字以上であることをテスト。ここの空白はただの空パスワードでは無く、空文字が数文字分ある。(has_secure_password自体が完全な空文字に対する存在性のバリデーションが付いている。)

 #パスワードが空白でないことを検証
  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  #パスワードが空6文字以上であることを検証
  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end

emailのときと同様に、存在性と文字数のバリデーションをモデルに追加。

[app/models/user.rb]
validates :password, presence: true, length: { minimum: 6 }

第6章ー演習ー

6.1.1

Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはスキーマ(schema)と呼ばれるデータベースの構造を追跡するために使われます。そこで、自分の環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル(リスト 6.2)の内容を比べてみてください。 (解答) マイグレーションファイルの中身と対応している。force: :cascadeの部分は既に存在するテーブルがあった場合は、それを削除して再度テーブル生成するオプション。

[db/schema.rb]
ActiveRecord::Schema[7.0].define(version: 2023_04_14_080953) do
  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

②ほぼすべてのマイグレーションは、元に戻すことが可能です(少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック(rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。 $ rails db:rollback 上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください(コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。なお上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。これがうまくいくのは、drop_tableとcreate_tableがそれぞれ対応していることをchangeメソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。 (解答) 確かにロールバックされた。

[db/schema.rb]
ActiveRecord::Schema[7.0].define(version: 0) do
end

③もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。 (解答) 再度schemaの内容が戻った。

6.1.2

Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください。(ヒント: 4.4.4で紹介したテクニックを使ってみてください。)

②同様の方法で、ApplicationRecordがActiveRecord::Baseを継承していることも確認してみてください。 (解答) ①と②の解答

irb(main):001:0> user = User.new
=> #<User:0x00007f014e8145d8 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
irb(main):003:0> user.class.superclass
=> ApplicationRecord(abstract)
irb(main):004:0> user.class.superclass.superclass
=> ActiveRecord::Base

6.1.3

①user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。 (解答)

irb(main):005:0> user.name.class
=> String
irb(main):007:0> user.email.class
=> String

②created_atとupdated_atは、どのクラスのインスタンスでしょうか? (解答) ActiveSupport::TimeWithZone

irb(main):008:0> user.created_at.class
=> ActiveSupport::TimeWithZone
irb(main):009:0> user.updated_at.class
=> ActiveSupport::TimeWithZone

6.1.4

①nameを使ってユーザーオブジェクトを検索してみてください。また、find_by_nameメソッドが使えることも確認してみてください(これはfind_byの古い書き方で、古いRailsアプリケーションでよく見かけられます)。 (解答)

irb(main):012:0> User.find_by(name: 'Taro Yamda')
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Taro Yamda"], ["LIMIT", 1]]                                  
=> nil                                                          
irb(main):013:0> User.find_by_name('Taro Yamda')
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Taro Yamda"], ["LIMIT", 1]]                                  
=> nil         

②実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。 (解答)

irb(main):015:0> User.all.class
=> User::ActiveRecord_Relation

③User.allに対してlengthメソッドを呼び出すと、その長さ(データの件数)を求められることを確認してみてください(4.2.2)。なおRubyには、そのクラスを詳しく知らなくてもオブジェクトをどう扱えば良いか何となく見当がつく、という特徴があります。これはダックタイピング(duck typing)と呼ばれ、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」10 。 (解答) ユーザーを1件しか登録していなため、lengthは1。

irb(main):016:0> User.all.length
  User Load (0.2ms)  SELECT "users".* FROM "users"
=> 1    

6.1.5

①userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
(解答)

irb(main):017:0> user.name = 'Ichiro Sato'
=> "Ichiro Sato"
irb(main):018:0> user
=> 
#<User:0x00007fba0cbe4858                                       
 id: 1,                                                         
 name: "Ichiro Sato",                                           
 email: "t.yamada@example.com",                                 
 created_at: Fri, 14 Apr 2023 08:40:55.614896000 UTC +00:00,    
 updated_at: Fri, 14 Apr 2023 08:40:55.614896000 UTC +00:00>    
irb(main):019:0> user.save
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Update (0.3ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Ichiro Sato"], ["updated_at", "2023-04-14 09:17:36.928435"], ["id", 1]]
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1        
=> true           

②今度はupdateを使って、email属性を更新および保存してみてください。
(解答)

irb(main):020:0> user.update(email: 'i.sato@example.com')
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  User Update (0.3ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "i.sato@example.com"], ["updated_at", "2023-04-14 09:18:59.317175"], ["id", 1]]
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1        
=> true                                                         
irb(main):021:0> user
=> 
#<User:0x00007fba0cbe4858                                       
 id: 1,                                                         
 name: "Ichiro Sato",                                           
 email: "i.sato@example.com",                                   
 created_at: Fri, 14 Apr 2023 08:40:55.614896000 UTC +00:00,    
 updated_at: Fri, 14 Apr 2023 09:18:59.317175000 UTC +00:00> 

③同様に、マジックカラムであるcreated_atも直接更新できることを確認してみてください。(ヒント: 1.year.agoで更新すると便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の日時を算出してくれます。)
(解答)確かに1年前になった。

irb(main):022:0> user.update(created_at: 1.years.ago)
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Update (0.2ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2022-04-14 09:20:09.696234"], ["updated_at", "2023-04-14 09:20:09.703575"], ["id", 1]]                                                         
  TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_1                   
=> true                                                                    
irb(main):023:0> user
=> 
#<User:0x00007fba0cbe4858                                                  
 id: 1,                                                                    
 name: "Ichiro Sato",                                                      
 email: "i.sato@example.com",                                              
 created_at: Thu, 14 Apr 2022 09:20:09.696234000 UTC +00:00,               
 updated_at: Fri, 14 Apr 2023 09:20:09.703575000 UTC +00:00>  

6.2.1

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

irb(main):024:0> user.valid?
=> true

②6.1.3で生成したuserオブジェクトについても、有効かどうかを確認してみましょう。
(解答) 作っていないのでパス。

6.2.2

①nameもemailも空の新しいユーザーuを作成し、作成した時点では有効ではない(invalid)ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
(解答) nameもemailも空だとバリデーションエラー。

irb(main):001:0> u = User.new
  TRANSACTION (0.9ms)  begin transaction
=> #<User:0x00007f3b35fdf3c8 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
irb(main):002:0> u.valid?
=> false
irb(main):003:0> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

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

irb(main):003:0> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
irb(main):004:0> u.errors.messages[:email]
=> ["can't be blank"]

6.2.3

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

irb(main):001:0> u = User.new
  TRANSACTION (0.1ms)  begin transaction
=> #<User:0x00007fdbca71a608 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
irb(main):002:0> u.name = 'a' * 55
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
irb(main):003:0> u.email = 'a' * 260
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
irb(main):004:0> u.valid?
=> false

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

irb(main):005:0> u.errors.messages
=> {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}

6.2.4

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

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

 FAIL UserTest#test_email_validation_should_reject_invalid_addresses (0.06s)
        "foo@bar..com" should be invalid
        test/models/user_test.rb:55:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:53:in `each'
        test/models/user_test.rb:53:in `block in <class:UserTest>'

正規表現VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/iを追加するとパスした。

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

6.2.5

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

 FAIL UserTest#test_email_addresses_should_be_saved_as_lowercase (0.07s)
        Expected: "foo@example.com"
          Actual: "Foo@ExAMPle.CoM"
        test/models/user_test.rb:71:in `block in <class:UserTest>'

②テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう(リスト 6.35)。(ヒント: メソッドの末尾に!を追加すると、email属性が直接変更されます。)
(解答) この記述でも変わらずうまくいく。

6.3.2

①この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。
②なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
(解答) ①、②合わせて。パスワードが未設定のため失敗する。

irb(main):001:0> user = User.new(name: 'Jiro Tanaka', email: 'j.tanaka@example.com')
  TRANSACTION (0.5ms)  begin transaction
=>                                                                     
#<User:0x00007fb861334a70                                              
...                                                                    
irb(main):002:0> user.valid?
  User Exists? (2.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "j.tanaka@example.com"], ["LIMIT", 1]]                                                
=> false                                                               
irb(main):003:0> user.errors.messages
=> {:password=>["can't be blank"]}

6.3.3

①名前とメールアドレスは有効でも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
②上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
(解答) パスワード短すぎ。

irb(main):001:0> user = User.new( name: 'Jiro Tanaka', email: 'j.ranaka@example.com', password: 'foo')
  TRANSACTION (0.1ms)  begin transaction
=>                                                                                                                
#<User:0x00007f5f50f93678                                                                                         
...                                                                                                               
irb(main):002:0> user.valid?
  User Exists? (1.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "j.ranaka@example.com"], ["LIMIT", 1]]                                                                                           
=> false                                                                                                          
irb(main):003:0> user.errors.messages
=> {:password=>["is too short (minimum is 6 characters)"]}

6.3.4

①userオブジェクトを消去するためにコンソールを一度再起動し、本節で作ったuserオブジェクトを検索してみてください。
(解答)

irb(main):001:0> User.all
  User Load (0.2ms)  SELECT "users".* FROM "users"
=>                                                              
[#<User:0x00007fe5bbbcaae0                                      
  id: 1,                                                        
  name: "Michael Hartl",                                        
  email: "michael@example.com",                                 
  created_at: Fri, 14 Apr 2023 16:18:32.698774000 UTC +00:00,   
  updated_at: Fri, 14 Apr 2023 16:18:32.698774000 UTC +00:00,   
  password_digest: "[FILTERED]">]  

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

irb(main):003:0> user.name = 'Taro Yamada'
=> "Taro Yamada"
irb(main):004:0> user.save
  TRANSACTION (0.2ms)  begin transaction
  User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "michael@example.com"], ["id", 1], ["LIMIT", 1]]                
  TRANSACTION (0.1ms)  rollback transaction                            
=> false                                                               
irb(main):005:0> user.errors.messages
=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

③今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
(解答) これならtrueで変更出来た。

irb(main):007:0> user.update_attribute(:name, 'Taro Yamada')
  TRANSACTION (0.2ms)  begin transaction
  User Update (1.8ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Taro Yamada"], ["updated_at", "2023-04-14 16:30:58.847096"], ["id", 1]] 
  TRANSACTION (13.6ms)  commit transaction                      
=> true     

第6章ーまとめー

  • ActiveRecordモデルのnew操作はあくまで新しいオブジェクトをメモリ上に作成するだけ。データベースには保存されない。
  • ActiveRecordの検索は、findfind_bywhereの3種類。
  • メールアドレスは大文字と小文字は区別せず、全て小文字として扱うのが通例。
  • ActiveRecordは通常データベースのレベルでの一意性保証はしていない。そのため、indexを付けてindexに一意性を持たせる。indexは索引機能としても役立つ。
  • has_secure_passwordで安全なパスワード(不可逆的にハッシュ化されたパスワード)が追加される。

2周目時点では曖昧理解で飛ばしていたデータベースの一意性制約などがやっとクリアになった。。。

第6章終わり🐻