Rails Tutorial「第6章 ユーザーのモデルを作成する」の内容になります
Userモデル
- Railsでは、データモデルとして扱うデフォルトのデータ構造のことをモデル (Model) と呼ぶ
- データベースとやりとりをするデフォルトのRailsライブラリはActive Recordと呼ぶ
- Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っている
- これらのメソッドを使うのに、リレーショナルデータベースで使うSQLを意識する必要がない
- Railsにはマイグレーションという機能があり、データの定義をRubyで記述することができのでSQLのDDLを新たに学ぶ必要がありません
Userモデルを生成する
- コントローラ名には複数形を使い、モデル名には単数形を用いる(コントローラはUsersでモデルはUser)
- 属性の型情報も一緒に渡す。
$ rails generate model User name:string email:string
カラム | 型 |
---|---|
id | integer |
name | string |
string |
マイグレーションファイル
generateコマンドの結果のひとつとしてマイグレーションと呼ばれる新しいファイルが生成される
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
- マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まり
- changeメソッドはcreate_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成
- モデル名は単数形 (User) ですが、テーブル名は複数形 (users)
- .timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム (Magic Columns)」を作成(あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプ)
カラム | 型 |
---|---|
id | integer |
name | string |
string | |
created_at | datetime |
updated_at | datetime |
マイグレーションの適用
$ rails db:migrate
マイグレーション適用でdevelopment.sqlite3
が更新される。
また、app/models/user.rb
が作成される
マイグレーションを元に戻す
$ rails db:rollback
- drop_tableが適用されるので
db/schema.rb
が空になる - あるカラムを削除する場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する
ユーザーオブジェクトを作成する
Railsコンソールでモデルの操作が可能
$ rails console --sandbox
sandboxをつけることで「データベースへの変更をコンソールの終了時にすべて “ロールバック” (取り消し) する」設定になる
User.new
User.newで新しいオブジェクトを生成
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com",
created_at: nil, updated_at: nil>
userオブジェクトが有効かどうかを確認
>> user.valid?
true
User.save
現時点ではまだデータベースにデータは格納されていません。
つまり、User.newはメモリ上でオブジェクトを作成しただけです。
データベースにUserオブジェクトを保存するためには、userオブジェクトからsaveメソッドを呼び出す必要があります。
>> 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", "mhartl@example.com"], ["created_at", 2016-05-23 19:05:58 UTC],
["updated_at", 2016-05-23 19:05:58 UTC]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
saveメソッドは、成功すればtrueを、失敗すればfalseを返す。
>> 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">
saveメソッド後、id
、created_at
、updated_at
に値が入る。
User.create
Active RecordではUser.createでモデルの生成と保存を同時におこなう方法も提供されている
>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2016-05-23 19:18:46", updated_at: "2016-05-23 19:18:46">
User.createは、trueかfalseを返す代わりに、ユーザーオブジェクト自身を返す
User.destroy
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
>> 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: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
- createと同じようにdestroyはそのオブジェクト自身を返しますが、その戻り値を使ってもう一度destroyを呼ぶことはできません。
- 削除されたオブジェクトはまだメモリ上には残っている(
>> foo
)
検索
.find
>> User.find(1)
=> #<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">
.find_by
>> User.find_by(email: "mhartl@example.com")
=> #<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">
.first
>> User.first
=> #<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">
.all
>> User.all
=> #<ActiveRecord::Relation [#<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 id: 2, name: "A Nother",
email: "another@example.org", created_at: "2016-05-23 19:18:46",
updated_at: "2016-05-23 19:18:46">]>
その他、オブジェクトなので.length
も使える
>> User.all.length
=> 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をしないと更新されない
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"
.update_attributes
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
>> user.update_attribute(:name, "El Duderino")
=> true
>> user.name
=> "El Duderino"
validationとテスト駆動開発
Userオブジェクトが有効か
- setupは各テストが走る直前で実行される
- @userはインスタンス変数
- valid?は有効かを判定
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
test実行
$ rails test:models
validate presence
ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証する場合
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
test "email should be present" do
@user.email = ""
assert_not @user.valid?
end
end
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true
end
error messageを表示
>> user.errors.full_messages
=> ["Name can't be blank"]
>> user.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> user.errors.messages[:email]
=> ["can't be blank"]
validate length
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 lond" 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
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
validates format mail-address
有効なメールフォーマットと無効なメールフォーマットをテスト
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
,
test "email validation should accept valid addresses" do
valid_addresses = %w[uesr@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[uesr@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
end
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
メールアドレスが有効かを調べ正規表現
正規表現 | 意味 |
---|---|
/\A[\w+-.]+@[a-z\d-.]+.[a-z]+\z/i | (完全な正規表現) |
/ | 正規表現の開始を示す |
\A | 文字列の先頭 |
[\w+-.]+ | 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す |
@ | アットマーク |
[a-z\d-.]+ | 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す |
. | ドット |
[a-z]+ | 英小文字を少なくとも1文字以上繰り返す |
\z | 文字列の末尾 |
/ | 正規表現の終わりを示す |
i | 大文字小文字を無視するオプション |
validates unique
dupメソッド
メールアドレスの一意性を検証するために、メモリ上だけでなく実際にレコードをデータベースに登録する必要がある。
まずは重複したメールアドレスからテストする。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
test "email addresses shuold be unique" do
duplicate_user = @user.dup
duplicate_user.email = @user.email.upcase
@user.save
assert_not duplicate_user.valid?
end
end
- @userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテスト
- dupは同じ属性を持つデータを複製するためのメソッド
- @userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
- メールアドレスでは大文字小文字が区別されなし。したがって大文字を区別しないように
@user.email.upcase
と記述
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: { case_sensitive: false }
end
- メールアドレスの大文字小文字を区別した書き方
uniqueness: true
- メールアドレスの大文字小文字を無視した書き方
uniqueness: { case_sensitive: false }
emailにindexを追加で重複を回避
emailにindexを追加
$ rails generate migration add_index_to_users_email
メールアドレスの一意性のマイグレーションを定義
- usersテーブルのemailカラムにインデックスを追加する
add_index
- オプション
のunique: true
を指定することで一意性を強制
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
def change
add_index :users, :email, unique: true
end
end
データベースをマイグレート
$ rails db:migrate
fixture内のサンプルデータを削除
# 空にする (既存のコードは削除する)
email属性を小文字に変換する
- 文字列は同一であると解釈されるべきなので、データベースに保存される直前にすべての文字列を小文字に変換
- データベースに保存する前にemail属性を
before_save
で強制的に小文字に変換 - selfは現在のユーザーを指す
- こちらでも可。
before_save { email.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: { case_sensitive: false }
end
メールアドレスの小文字化に対するテスト
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 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
end
パスワードのセキュア化
- ユーザーにパスワードとパスワードの確認を入力させ、ハッシュ化したものをデータベースに保存
- ハッシュ化とはハッシュ関数を使って入力されたデータを元に戻せないデータにする
- ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較の手順
has_secure_password
- セキュアなパスワードの実装は
has_secure_password
というRailsのメソッドを使用 - セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存する
- 2つのペアの仮想的な属性 (passwordとpassword_confirmation) が使えるようになる
- 存在性と値が一致するかどうかのバリデーションも追加される
- authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド)
- モデル内にpassword_digestという属性がないと使えない
password_digestの追加
$ rails generate migration add_password_digest_to_users password_digest:string
※to_usersにすることで、usersテーブルにカラムを追加するマイグレーションがRailsによって自動的に作成される
マイグレーションを実行
$ rails db:migrate
bcryptのinstall
- has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数である
bcrypt
が必要 - パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされない
''' ruby:Gemfile
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
``` terminal
$ bundle install
Userモデルにhas_secure_passwordを追加
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: { case_sensitive: false }
has_secure_password
end
passwordとpassword_confirmationをテストに追加
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
.
.
.
end
パスワードの最小文字数設定
- パスワードとパスワード確認に対して同時に代入
- 空のパスワードを入力させないために存在性のバリデーション追加
- has_secure_passwordメソッドは存在性のバリデーションもするが、新しくレコードが追加されたときだけに適用される
- そのためパスワード更新用に
presence: true
を追加
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の作成と認証
データベースに新規ユーザーを1人作成
>> User.create(name: "Michael Hartl", email: "mhartl@example.com", password: "foobar", password_confirmation: "foobar")
データベースに保存されたかを確認
has_secure_passwordの効果を確認
>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3yOcVFzb6oK"
authenticateメソッドの確認
>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">
# ログインするときは論理値で判定
>> !!user.authenticate("foobar")
=> true