目的
ユーザー登録ページを作成する。
一番重要なステップであるユーザー用のデータモデルの作成と、
データを保存する手段の確保について学ぶ。
6.1ユーザーモデル
・Active Record
データベースとやりとりをするデフォルトのRailsライブラリ
・マイグレーション(Migration)
直接SQLを使わずにデータベースのテーブルやカラムなどの構造を変更できる仕組み
6.1.1 データベースのマイグレーション
簡単に消えることのないユーザーのモデルを構築を行う
・Usersコントローラーを作成
rails generate controller Users new
・Userモデルを作成
generate modelとするとモデルを作成できる
rails generate model User name:string email:string
※コントローラ名には複数形を使い、モデル名には単数形を用いる
・マイグレーションファイル
generateコマンドを実行するとマイグレーションファイルも新しく生成される
内容はデータベースに与える変更を定義したchangeメソッドの集まり
・changeメソッド
create_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成している
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
・マイグレーションの適用(migrating up)
rails db:migrateで実行できる
6.1.3 ユーザーオブジェクトを作成する
・サンドボックスモード
rails console --sandbox
データベースへの変更をコンソールの終了時にすべて “ロールバック”(取り消し)してくれる。
4章ではUser.newで新しいユーザーオブジェクトを生成していたが、
example_userファイルを明示的にrequireするまでこのオブジェクトにアクセスできなかった。
モデルを使うとRailsコンソールは起動時にRailsの環境を自動的に読み込み、その環境にモデルも含まれているため、新しいユーザーオブジェクトを作成するときに余分な作業を行わずに済む。
今まで
>> require './example_user' # example_userのコードを読み込む方法
=> true
>> example = User.new
モデルを使う場合
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
・有効性(Validity)
オブジェクトが有効かどうか確認できる
データベースにデータが存在するかどうかは確認できない
>> user.valid?
true
・save
データベースにUserオブジェクトを保存する
データベースにデータが存在するかどうかできる
>> 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", "michael@example.com"], ["created_at", "2022-03-11 01:51:03.453035"],
["updated_at", "2022-03-11 01:51:03.453035"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
・create
モデルの生成と保存を同時におこなうことができる
>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2022-03-11 01:53:22", updated_at: "2022-03-11 01:53:22">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2022-03-11
01:54:03", updated_at: "2022-03-11 01:54:03">
・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: "2022-03-11
01:54:03", updated_at: "2022-03-11 01:54:03">
6.1.4ユーザーオブジェクトを検索する
・find
User.find(1)
ユーザーのidを渡して、そのidを持つユーザーを返す。
・find_by
属性を指定してユーザーを検索する
>> User.find_by(email: "michael@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 01:51:03", updated_at: "2022-03-11 01:51:03">
・first
データベースの最初のユーザーを返す
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 01:51:03", updated_at: "2022-03-11 01:51:03">
・all
データベースのすべてのUserオブジェクトを返す
>> User.all
=> [#<User id: 1, name: "Michael Hartl", email:
"michael@example.com", created_at: "2022-03-11 01:51:03", updated_at:
"2022-03-11 01:51:03">, #<User id: 2, name: "A Nother", email:
"another@example.org", created_at: "2022-03-11 01:53:22", updated_at:
"2022-03-11 01:53:22">]>
6.1.5 ユーザーオブジェクトを更新する
・属性を個別に代入する
user.email = "mhartl@example.net"
user.save
最後にsaveを実行して変更をデータベースに保存する
保存を行わずにreloadを実行すると、データベースの情報を元にオブジェクトを再読み込みするため、変更が取り消される
>> user # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 01:51:03", updated_at: "2022-03-11 01:51:03">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
・updateを使い更新する
user.update(name: "The Dude", email: "dude@abides.org")
・update_attribute
特定の属性のみを更新
user.update_attribute(:name, "El Duderino")
6.2 ユーザーを検証する
nameとemailにあらゆる文字列を許さないようにする
6.2.1
有効性を検証する
・モデルの有効性をテストする手順
有効なモデルのオブジェクトを作成
↓
その属性のうちの1つを有効でない属性に意図的に変更
↓
そして、バリデーションで失敗するかどうかをテストする
テストコード
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オブジェクト(@user)を作成する
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
6.2.2 存在性を検証する
・存在性(Presence)
渡された属性が存在することを検証する
章の内容では
ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証する
・name属性の存在性に関するテスト
テストコード
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
・validates メソッドにpresence: trueという引数を与えて使うことで
name属性の存在を検査できる
class User < ApplicationRecord
validates :name, presence: true
end
↓
class User < ApplicationRecord
validates(:name, presence: true)
end
と同義
実際の動き
$ rails console --sandbox
>> user = User.new(name: "", email: "michael@example.com")
>> user.valid?
=> false
・失敗したときに作られるerrorsオブジェクトを使って確認すれば
どのバリデーションが失敗したかわかる
user.errors.full_messages
>> user.errors.full_messages
=> ["Name can't be blank"]
6.2.3 長さを検証する
ユーザーの名前の長さに制限を与える
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
{ maximum: }で上限を決めている
6.2.4 フォーマットを検証する
メールアドレスにおなじみのパターンuser@example.comに合っているかどうかの確認をする
user@example,comのような無効なメールアドレスが弾かれるようにする
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
上のコードを使うと
このパターンに一致するメールアドレスだけが有効であることをチェックできるようになる
6.2.5 一意性を検証する
重複したメールアドレスのチェック
@user.dupで
@userと同じメールアドレスのユーザーは作成できないことをチェックしている
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.save
assert_not duplicate_user.valid?
end
end
・メールアドレスの一意性を有効にする
uniqueness: true
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.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
そのためテストでメールアドレスを一度大文字に変換してから確認する
duplicate_user.email = @user.email.upcase
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
duplicate_user.email = @user.email.upcase
@user.save
assert_not duplicate_user.valid?
end
end
メールアドレスの大文字小文字を無視した一意性の検証
uniqueness: { case_sensitive: false }に置き換える
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)を追加し、そのインデックスが一意であるようにする
Emailにカラムを追加
rails generate migration add_index_to_users_email
add_indexというRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制できるようになります。
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
def change
add_index :users, :email, unique: true
end
end
・大文字小文字を区別する
before_saveコールバックにブロックを渡し、downcaseという文字列メソッドでメールアドレスを小文字に変換している
→データベースに保存される直前にすべての文字列を小文字に変換している
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
6.3セキュアなパスワードを追加する
・セキュアパスワード
各ユーザーにパスワードとパスワードの確認を入力させ、それをそのままではなくハッシュ化したものをデータベースに保存する。
6.3.1 ハッシュ化されたパスワード
・has_secure_password
セキュアなパスワードの実装が行えるRailsのメソッド
- セキュアにハッシュ化したパスワードを、データベース内のpassword_digest属性に保存できるようになる。
- 2つの仮想的な属性(passwordとpassword_confirmation)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される2。
- authenticateメソッドが使えるようになる(引数の文字列がパスワードと一致するとUserオブジェクトを返し、一致しない場合はfalseを返すメソッド)。
has_secure_password機能を使えるようにするには
モデル内にpassword_digestという属性が含まれている必要がある
↓
rails generate migration add_password_digest_to_users password_digest:string
でpassword_digest:stringという引数を渡せば指定できる
6.3.3 パスワードの最小文字数
パスワードが空でないことと最小文字数(6文字)の2つを設定する
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
・多重代入(Multiple Assignment)
@user.password = @user.password_confirmation = "a" * 5
パスワードとパスワード確認に対して同時に代入を行っている
・minimumオプション
validates :password, length: { minimum: 6 }で
最小文字数のバリデーションを実装できる
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
6.3.4 ユーザーの認証と作成
・Railsコンソールを使ってユーザーを手動で作成する
User.create(name: "Michael Hartl", email: "michael@example.com",password: "foobar123456, password_confirmation: "foobar123456”)
User.create(name: "Michael Hartl", email: "michael@example.com",
password: "foobar", password_confirmation: "foobar")
感想
今回はユーザーモデルの作成方法とデータの保存の仕方を学ぶことができました。
普段自分がログインする際にデータがどのようにチェックされて保管されているのかが、今回の学習でイメージできるようになりました。
普段何気なく使っている機能にも目を凝らして学習していけたらと思います!