やること
- サイトのユーザー用のデータモデルの作成
- データを保存する手段の確保
ブランチを切る
$ git checkout master
$ git checkout -b modeling-users
Userモデル
- モデル
- Railsの場合、データモデルで使用するデフォルトのデータ構造をさす
- データを永続化する方法
- データベースを使用してデータを長期間保存
- データベースとやり取りするRailsライブラリはデフォルトで
Active Record
という
-
Active Record
- データオブジェクトの作成/保存/検索のためのメソッドを保持している
- 上記メソッドを使用する際にSQLを意識する必要はない
- マイグレーション
- データの定義をRubyで記述
- DDLを使う必要がない
- データの定義をRubyで記述
データベースの移行
-
generate model
- モデルを作成するコマンド
- モデル名には単数形を用いる
- コントローラ名は複数形
Userモデルの作成
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/20140814160532_create_user.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
- マイグレーション
- データベースの構造をインクリメンタルに変更する手段を提供
- 要求が変更された場合にデータモデルを適合させることができる
- 上記の例の場合、
db/migrate/20140814160532_create_users.rb
にあたる- タイムスタンプを名前の中につけることで、複数開発者が生成しても競合しないようにしている
db/migrate/20140814160532_create_user.db:Userモデルのマイグレーション
class CreateUsers < ActiveRecord::Migration
def change
create_table :user do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
-
マイグレーション
- データベースに与える変更を定義した
change
メソッドの集合-
create_table
メソッドを呼び、ユーザーを保持するためのテーブルをデータベースに作成する- モデルは単数形(
user
)だが、テーブル名は複数形(users
)
- モデルは単数形(
- ブロックの中で、
name
とemail
というカラムをデータベースに作成 -
t.timestamp
でcreated_at
とupdated_at
というカラムを作成- レコード作成/更新時に自動的に更新されるカラム
-
- データベースに与える変更を定義した
-
ユーザーデータモデル
-
id
は各行を一意に識別するためのカラムで自動生成される
-
カラム名 | 型 |
---|---|
id | integer |
name | string |
string | |
created_at | datetime |
updated_at | datetime |
- マイグレーションの実行
bundle exec rake db:migrate
- マイグレーションの適用(migrating up)
- 内部的に
create_table
を呼び出している -
db/development.sqlite3
というDDLのファイルが作成される- SQLite Database Browserでデータベースの構造を参照できる
- マイグレーションの取り消し
bundle exec rake db:rollback
- マイグレーションの取り消し(migrating down)
- 内部的に
drop_table
を呼び出している
マイグレーションの適用
$ bundle exec rake db:migrate
== 20140814160532 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0077s
== 20140814160532 CreateUsers: migrated (0.0079s) =============================
modelファイル
- Userモデル
-
ActiveRecord::Base
の全ての機能を持つ
-
app/models/user.rb:Userモデル
class User < ActiveRecord::Base
end
ユーザーオブジェクトの作成
-
rails console
のサンドボックスモード- コンソール上でDBの変更を行ってもロールバックされる
Userオブジェクトの操作:作成・保存と削除
$ rails c --sandbox
Loading development environment in sandbox (Rails 4.0.4)
Any modifications you make will be rolled back on exit
# Userオブジェクトの作成
> User.new
# Userオブジェクトを初期化してuser変数に代入
> user = User.new(name: "Michael Hartl", email: "mhart@example.com")
# user変数をDBに保存
> user.save
# user変数の属性を確認
> user.name
=> "Michael Hartl"
> user.email
=> "mhart@example.com"
> user.updated_at
=> Sat, 23 Aug 2014 13:24:40 UTC +00:00
# Userオブジェクトの作成と同時に保存
> User.create(name: "A Nother", email: "another@example.org")
(0.2ms) SAVEPOINT active_record_1
SQL (0.9ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Sat, 23 Aug 2014 14:09:14 UTC +00:00], ["email", "another@example.org"], ["name", "A Nother"], ["updated_at", Sat, 23 Aug 2014 14:09:14 UTC +00:00]]
(0.4ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 4, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:09:14", updated_at: "2014-08-23 14:09:14">
# userの破棄
> user = user.destroy
(0.4ms) SAVEPOINT active_record_1
SQL (0.6ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]
(0.8ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Michael Hartl", email: "mhart@example.com", created_at: "2014-08-23 13:24:40", updated_at: "2014-08-23 13:24:40">
ユーザーオブジェクトの検索
Userオブジェクトの検索
# idで検索
> User.find(5)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 5]]
=> #<User id: 5, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:30:53", updated_at: "2014-08-23 14:30:53">
# Emailで検索
> User.find_by_email("another@example.org")
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'another@example.org' LIMIT 1
=> #<User id: 4, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:09:14", updated_at: "2014-08-23 14:09:14">
# 上記よりもハッシュで指定するfind_byメソッドを使うべき
> User.find_by(email: "another@example.org")
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'another@example.org' LIMIT 1
=> #<User id: 4, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:09:14", updated_at: "2014-08-23 14:09:14">
# 最初の1件目を取得
> User.first
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
=> #<User id: 2, name: "テストユーザー", email: "admin@localhost", created_at: "2014-08-02 06:36:09", updated_at: "2014-08-02 06:36:09">
# 全てのデータを返す
> User.all
ユーザーオブジェクトの更新
- オブジェクトの更新方法
- 属性を個別に代入
-
save
メソッド
-
- 属性の更新と保存を同時に行う
-
update_attributes
メソッド
-
- 属性を個別に代入
属性の更新
# userの登録
> user = User.create(name: "Michael Hartl", email: "mhart@exampl
e.net")
(0.2ms) SAVEPOINT active_record_1
SQL (0.6ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Sat, 23 Aug 2014 14:39:04 UTC +00:00], ["email", "mhart@example.net"], ["name", "Michael Hartl"], ["updated_at", Sat, 23 Aug 2014 14:39:04 UTC +00:00]]
(1.4ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 7, name: "Michael Hartl", email: "mhart@example.net", created_at: "2014-08-23 14:39:04", updated_at: "2014-08-23 14:39:04">
# 属性の変更
> user.email = "mhartl@example.com"
=> "mhartl@example.com"
# 属性の更新
> user.save
(0.2ms) SAVEPOINT active_record_1
SQL (0.8ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = 7 [["email", "mhartl@example.com"], ["updated_at", Sat, 23 Aug 2014 14:39:30 UTC +00:00]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> true
# 属性の更新+保存
> user.update_attributes(name: "The Dude", email: "dude@abides.org")
(0.2ms) SAVEPOINT active_record_1
SQL (1.0ms) UPDATE "users" SET "name" = ?, "email" = ?, "updated_at" = ? WHERE "users"."id" = 7 [["name", "The Dude"], ["email", "dude@abides.org"], ["updated_at", Sat, 23 Aug 2014 14:43:01 UTC +00:00]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
ユーザの検証
-
Active Record
の検証(バリデーション)- 存在の検証(presence)
- 長さの検証(length)
- フォーマットの検証(format)
- 一意性の検証(uniqueness)
最初のユーザテスト
- 初期spec
-
pending
メソッドを呼び出して、黄色い文字を出力するだけ
-
初期spec
require 'spec_helper'
describe User do
pending "add some examples to (or delete) #{__FILE__}"
end
テストの実行
# DBの作成
$ bundle exec rake db:migrate
# 開発データベースのデータモデルdb/development.sqlite3がテストデータベースdb/test.sqlite3に反映されるようにするもの
$ bundle exec rake test:prepare
# テストを実行
$ bundle exec rspec spec/models/user_spec.rb
spec/models/user_sprc.rbにテストケースを追加
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "user@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
end
上記は下記と同義
it "should respond to 'name'" do
expect(@user).to respond_to(:name)
end
-
respond_to
メソッド- Rubyの
respond_to?
メソッドを呼び出す - そのシンボルが表すメソッドまたは属性が存在しているかどうかを確認
- Rubyの
テストを実施:成功
$ bundle exec rspec spec/
respond_to?の動作確認
$ rails c --sandbox
> user = User.new
> user.respond_to?(:name)
=> true
> user.respond_to?(:foo)
=> false
プレゼンスの検証
- 属性の存在を検証
- validates
メソッドに
presence: true`という引数を与える
- validates
app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true
end
presenceの検証
$ rails c --sandbox
# name属性を未指定で作成
> user = User.new(name: "", email: "mhartl@example.com")
# 保存しようとするとfalseが返却される
> user.save
=> false
# 検証してもfalseが返却される
> user.valid?
=> false
# エラーメッセージの確認
> user.errors.full_messages
=> ["Name can't be blank"]
- テストケースの追加
-
valid
がtrueであること - name、email属性がブランクの時は
valid?
メソッドがfalseであること
-
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "user@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should be_valid }
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
describe "when email is not present" do
before { @user.email = " " }
it { should_not be_valid }
end
end
rspecの実行:emailのpresenceがないのでエラー
$ bundle exec rspec
.......F...........
Failures:
1) User when email is not present
Failure/Error: it { should_not be_valid }
expected #<User id: nil, name: "Example User", email: " ", created_at: nil, updated_at: nil> not to be valid
app/models/user.rbにemailのpresence設定を追加
class User < ActiveRecord::Base
validates :name, presence: true
validates :email, presence: true
end
テスト成功
$ bundle exec rspec
長さの検証
spec/models/user_spec.rbに長さのテストケースを追加
describe "when name is too long" do
before { @user.name = "a" * 51 }
it { should_not be_valid }
end
テストを実施して失敗
$ bundle exec rspec
- 長さの検証の設定
length: { maximum: <最大文字数> }
app/models/user.rbに長さの設定を追加
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true
end
フォーマットの検証
spec/models/user_spec.rbにemailのフォーマット検証ロジックを追加
describe "when email format is invalid" do
it "should be invalid" do
addresses = %w[
user@foo,com
user_at_foo.org
example.user@foo.
foo@bar_baz.com
foo@bar+baz.com
]
addresses.each do |invalid_address|
@user.email = invalid_address
expect(@user).not_to be_valid
end
end
end
describe "when email format is valid" do
it "should be valid" do
addresses = %w[
user@foo.COM
A_US-ER@f.b.org
frst.lst@foo.jp
a+b@baz.cn
]
addresses.each do |valid_address|
@user.email = valid_address
expect(@user).to be_valid
end
end
end
- フォーマットの検証
format: { with: <フォーマット> }
-
\A
は文字列の先頭を表す -
\z
は文字列の末尾を表す
app/models/user.rbにemailのフォーマット検証を追加
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end
一意性の検証
-
@user.dup
- 同じユーザを複製
- 既に登録済みのユーザを登録しようとしてエラーになることを期待
spec/models/user_spec.rbに一意性検証のテストコードを追加
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.save
end
it { should_not be_valid }
end
- 一意性の検証
-
uniqueness: true
を設定
-
app/models/user.rbに一意性の検証を追加
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: true
end
一意性の検証で、現時点では大文字小文字を区別する
$ rails c --sandbox
> user = User.create(name: "Example User", email: "user@example.com")
> user_with_same_email = user.dup
# メールアドレスを大文字に変換してバリデーションチェック → 一意性がtrueとみなされている
> user_with_same_email.email = user.email.upcase
> user_with_same_email.valid?
=> true
- 大文字小文字を区別しないためには
-
:case_sensitive
を設定
-
spec/models/user_spec.rbに一意性検証のテストコードを追加
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.email = @user.email.upcase
user_with_same_email.save
end
it { should_not be_valid }
end
app/models/user.rbに大文字小文字を区別しない設定を追加
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
- DB側の一意性制約
- emailカラムにDBのインデックスを作成し、そのインデックスが一意であることを要求
-
rails generate migration
で作成 add_index :<テーブル名>, :<カラム名>, unique: true
インデックスの作成
$ rails generate migration add_index_to_users_email
invoke active_record
create db/migrate/20140824083152_add_index_to_users_email.rb
db/migrate/20140824083152_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique: true
end
end
データベースのマイグレート
$ cat db/schema.rb
ActiveRecord::Schema.define(version: 20140814160532) do
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
end
end
$ bundle exec rake db:migrate
$ cat db/schema.rb
ActiveRecord::Schema.define(version: 20140824083152) do
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
end
- メールアドレス保存時の考慮事項
- メールアドレス保存時に全ての文字を小文字にする
- DBのアダプタが常に大文字小文字を区別するインデックスを使用しているとは限らないため
- コールバック機能を使用
- メールアドレス保存時に全ての文字を小文字にする
- コールバック
-
Active Record
オブジェクトが持続している間のどこかの時点で、Active Record
オブジェクトに呼び出してもらうメソッド - 今回は
before_save
コールバックを使用
-
app/models/user.rbに、メールアドレス保存時に小文字に変換するロジックを追加
class User < ActiveRecord::Base
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, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
セキュアなパスワードの追加
- ユーザの認証
- パスワードの送信
- 暗号化
- DB内の暗号化された値との比較
- 暗号化された値同士を比較する
- セキュアなパスワードの実装方法
-
has_secure_password
メソッドを使用 - ハッシュ関数には
bcrypt
を使用して、不可逆的に暗号化してパスワードハッシュを作成
-
暗号化されたパスワード
- ユーザのデータモデル
-
password_digest
カラムを追加
-
カラム名 | 型 |
---|---|
id | integer |
name | string |
string | |
password_digest | string |
created_at | datetime |
updated_at | datetime |
Gemfileにbcrypt用のgemを追加
gem 'bcrypt-ruby', '3.1.2'
gemのインストール
$ bundle install
spec/models/user_spec.rbにpassword_digestカラムがあることのテストケースを追加
it { should respond_to(:password_digest) }
- マイグレーション名
- 末尾に
_to_users
とつけると、users
テーブルにカラムを追加するマイグレーションがRailsによって自動作成される - 第2引数でカラム名と型を指定
- 末尾に
password_digest用のマイグレーションを作成
$ rails generate migration add_password_digest_to_users password_digest:string
invoke active_record
create db/migrate/20140824101047_add_password_digest_to_users.rb
$ cat db/migrate/20140824101047_add_password
_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
開発DBをマイグレーションしてテストDBを準備し、テストを実施
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
$ bundle exec rspec spec/
パスワードと確認
-
パスワードの確認
- モデルの中で
Active Record
を使用して制限を与えるのが慣習 -
password
属性とpassword_confirmation
属性をUserモデルに追加し、レコードをDBに保存する前に2つの属性が一致するように要求する- これらの属性は一時的にメモリ上に置き、DBには保存されないようにする
- これらの属性は
has_secure_password
で自動的に実装される
- モデルの中で
-
テストコードの追加/修正
-
@user
の初期化時にpassword
属性とpassword_confirmation
属性も初期化する -
password
とpassword_confirmation
カラムの存在チェック-
password
、password_confirmation
はDB上に作成されるわけではない(仮想的な属性)
-
-
password
の存在確認テスト -
password
とpassword_confirmation
の不一致テスト
-
spec/models/user_spec.rb
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
describe "when password is not present" do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: " ", password_confirmation: " ")
end
it { should_not be_valid }
end
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
app/models/user.rbにhas_secure_passwordを追加
class User < ActiveRecord::Base
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, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
end
-
has_secure_password
を追加するだけでテストコードがパスできる- テストがOKになることを確認したら、上記の
has_secure_password
をコメントアウトしておく
- テストがOKになることを確認したら、上記の
テストコードの実行
$ bundle exec rspec spec/
ユーザー認証
-
ユーザをメールアドレスとパスワードに基づいて取得する手段
- ユーザをメールアドレスで検索
- `user = User.find_by(email: email)
- 受け取ったパスワードでユーザを認証
current_user = user.authenticate(password)
- ユーザをメールアドレスで検索
-
認証に関するテストケース
-
authenticate
が応答することの確認 - パスワードが一致する場合と一致しない場合についてテストケースを記載
- パスワードの長さの確認
-
spec/models/user_spec.rbに上記テストケースを追加
it { should respond_to(:authenticate) }
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by(email: @user.email) }
describe "with valid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
end
-
let
メソッド- テスト内でローカル変数を作成
-
let(:found_user) { User.find_by(email: @user.email) }
の場合-
found_user
という変数を作成し、ブロック内の戻り値が代入される
-
-
specify
メソッド-
it
と同義 -
it
を使用すると英語として不自然な場合に使用- ×:
user: user with invalid password should be false
- ○:
specify: user with invalid password should be false
- ×:
-
-
eq
演算子- オブジェクト同士が同値であるかどうかを確認
ユーザモデルのパスワード認証の実装
-
パスワードの長さ検証
- `length: { minimum: 6 }
-
password
とpassword_confirmation
を追加 -
パスワードが存在することの確認
-
パスワードとパスワードの確認が一致することの確認
-
password_digest
を比較してユーザを認証has_secure_password
- 内容についてはsecure_password.rbを参照
app/models/user.rbにパスワード関連の設定を追加
class User < ActiveRecord::Base
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,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
end
ユーザの作成
ユーザデータを作成
$ rails c
# ユーザの作成
> User.create(name: "Michael Hartl", email: "mhartl@example.com", password: "foobar", password_confirmation: "foobar")
# パスワードの確認
> user = User.find_by(email: "mhartl@example.com")
> user.password_digest
=> "$2a$10$GEZwKUtE7ZqAkgsBhHmIrueLFPZhECEauTC2FqbIGsWg8rzGr3Mwq"
# authenticateメソッドの確認
# パスワードが誤っている場合はfalse、正しい場合はユーザオブジェクトを返す
> user.authenticate("invalid")
=> false
> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2014-08-24 15:11:15", updated_at: "2014-08-24 15:11:15", password_digest: "$2a$10$GEZwKUtE7ZqAkgsBhHmIrueLFPZhECEauTC2FqbIGsWg...">
変更内容のコミット
変更内容のコミットとmasterへのマージ
$ git branch
filling-in-layout
master
* modeling-users
static-pages
$ git add .
$ git commit -m "Make a basic User model (including secure passwords)"
$ git checkout master
$ git merge modeling-users