#近況報告
エンジニア転職成功しました。YouTubeもはじめました。
著者略歴
YUUKI
ポートフォリオサイト:Pooks
現在:RailsTutorial2周目
#第6章 難易度 ★★★ 5時間
挫折しないRailsチュートリアルの進め方を先にお読みください↓↓
#ユーザーのモデルを作成する
第5章で作成した新規ユーザー作成のスタブページをカスタマイズ、データモデルの作成やデータ保存まで行う。
Webアプリケーションには欠かせないログイン/認証システムの実装手段は数多くあり、例えばClearanceやAuthlogicなどを使えば簡単に実装できるが、この章ではあえてRailsでゼロから作成する。
その理由としては、ゼロから作成して理解した方が、サードパーティ製品を導入することになっても容易にカスタマイズできるし、そもそも自分で認証システムを実装した経験があった方が全般的にシステムを理解できる為である。
##Userモデル
Railsではデータモデルとして扱うデフォルトのデータ構造のことをModelと呼ぶ。
MVCアーキテクチャのMに当たる部分。
Railsではデータを永続化するデフォルトの解決策として、DBを使ってデータを長期化する。
Railsでは、データベースとやりとりをするActive Record(デフォルトのRailsライブラリ)という概念があるので、SQLを意識する必要が殆どない。
また、データの定義をマイグレーションと呼ばれる機能でRubyに記述する為、新たにDDLを学ぶ必要もない。
つまり、Railsではデータベースの細部をほぼ完全に隠蔽し、切り離してくれる。
Tutorialでは、SQliteをdevelopmentで使い、PostgreSQLをproductionで使う。
ゆえに、本番環境のデータの保存方法を詳細に考える必要が殆どない。
###データベースの移行
4章で扱ったカスタムビルドクラスのUserではRailsの重要な部分である永続性
と言う要素が欠けていた。
Rails ConsoleでUserクラスのオブジェクトを作成してもexitすれば消えてしまう。
今回は簡単に消えることのないユーザーのモデルを構築する。
まずはnameとemailという属性を持つユーザーをモデリングする。
email
を一意のユーザー名として扱う。
class User
attr_accessor :name, :email
end
attr_accessorはインスタンス変数の定義で、ここではname
とemail
と言うインスタンス変数を定義している。
が、これはRubyでの書き方である。
Railsはデータを保存する際にデフォルトでRDB(リレーショナルデータベース)を使う。
例えば、nameとemailと言う属性を持つユーザーを保存する場合、name
とemail
のカラムを持つusersテーブルを作成する。
テーブルに格納されるデータの例がこれ
出典:図 6.2: usersテーブルに含まれるデータのサンプル
対応するデータモデルはこれ
今回はrails g model
コマンドでモデルを作成する。
$ rails g model User name:string email:string
Running via Spring preloader in process 22586
invoke active_record
create db/migrate/20181217232230_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
ここで重要なのは、以下の二点。
- モデル名は先頭が大文字の単数形(User)を用いていること
- name属性とemail属性にデータ型がstring(文字列) のパラメータを渡している。
コントローラ生成時にはUserをusers
と定義したが、モデル生成時にはUser
と定義し、さらに二つの属性にオプションでstringパラメータを渡している。
ここで、generateコマンドの結果としてマイグレーションファイル
が生成される。
マイグレーションは、DBの構造をインクリメンタルに変更する手段を提供する。
要求が変更された場合にデータモデルを適合させることができる為、一度設定しておけば良い。
なお、マイグレーションファイルはモデル生成スクリプトによって自動的に作られる。
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
このような記述が自動がされているが、ファイル生成時のファイル名の数値に注目。
これはファイル生成の発生日時を記録するtimestampsによって記録されている。
timestampsのお陰で全く同時にマイグレーションが生成されることが起きずに、コンフリクトを回避できる。
マイグレーション自体は、データーベースに与える変更を定義したchangeメソッド
の集まり。
上記のコードでは、changeメソッドがcreate_table
というRailsメソッドを呼んで、ユーザーを保存するためのテーブルをDBに作成する。
さらに、create_tableメソッドはブロック変数を一つ持つt
ブロックを受け取り、
tオブジェクトを使ってname
とemail
カラムをDBに生成。
データ型はどちらもstring。
なお、テーブル名はクラス名と違い、複数形のusers
を使っている(これはRailsの言葉の慣習である)
複数形を使う理由は、モデルは一人のユーザーを表すのに対し、
DBのテーブルは複数のユーザーから構成されるから。
さらに、t.timestampsでマジックカラム(created_atとupdated_atの両カラム)が作成される。
上記のマイグレーションによって作成された完全なデータモデルがこれ
出典:図 6.4: リスト 6.2で生成されたUserのデータモデル
ここで、マイグレーションに変更を反映する為に、migrating up
を行う。
$ rails db:migrate
== 20181217232230 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0044s
== 20181217232230 CreateUsers: migrated (0.0052s) =============================
これで、usersテーブルが作成される。
このコマンドを実行すると、db/development.sqlite3
という名前のファイルが生成される。
これはSQliteデータベースの実体である。
ここで、DB Browser for SQliteというツールを使って、DBの構造を見てみる。
ここにあるidは、レコードを一意に識別する為のカラムである。
####演習
1:schecma.rbというDB構造の追跡ファイルとマイグレーションファイルを見比べる。
ActiveRecord::Schema.define(version: 20181217232230) 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
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
ちょっとまだ一部のコードの意味がわからんw
2:マイグレーションファイルをロールバックしてみる。
$ rails db:rollback
== 20181217232230 CreateUsers: reverting ======================================
-- drop_table(:users)
-> 0.0043s
== 20181217232230 CreateUsers: reverted (0.0076s) =============================
schema.rbの中身を確認。
ActiveRecord::Schema.define(version: 0) do
end
中身が消えている。
これは、db:rollbackコマンドでdrop_tableコマンドを内部で呼び出し、usersテーブルを削除したため。
要はrails db:migrate
実行前に戻したのでこうなる。
ちなみに、これが上手く行くのはdrop_table
とcreate_table
がそれぞれ対応していることをchangeメソッド
が知っているから。
この対応関係を知っているため、ロールバック用の逆方向をのマイグレーションを簡単に実現できる。
なお、カラムを削除するような不可逆なマイグレーションの場合、changeメソッドの代わりにupとdownメソッドを別々に定義する必要がある。
3:やり直す
$ rails db:migrate
schema.rbの内容が戻っていればOK。
###modelファイル
ここでは作成されたuser.rb
というモデルのコードを見ていく。
class User < ApplicationRecord
end
UserクラスはApplicationRecordを継承していることがわかる。
さらに、ApplicationRecordはActiveRecord::Baseという基本クラスを継承していることになる。
####演習
1:RailsConsoleでUser.newでuserクラスのオブジェクトを生成、そのオブジェクトがApplicationRecordを継承していることを確認
2:さらにApplicationRecordがActiveRecord::Baseを継承していることを確認。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> User
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
>> User.class
=> Class
>> User.superclass
=> ApplicationRecord(abstract)
>> ApplicationRecord.superclass
=> ActiveRecord::Base
>>
OK。
###ユーザーオブジェクトを作成する
今回はRails Consoleをsandboxモードで起動する。
$ rails c --sandbox
今回はUserモデルを生成している。
なので、Railsコンソールの起動時にRailsの環境を自動的に読み込んだ際に、その環境にはモデルも含まれるので、新しいユーザーオブジェクトを作成する時に余分な作業を行わずに済む。
$ User.new
<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil, reset_digest: nil, reset_sent_at: nil>
User.new
を引数なしで呼んだ場合、全ての属性がnilのオブジェクトを返す。
以前、オブジェクトの属性を設定するための初期化ハッシュ(name: "M"〜の部分)を引数に取るように、user_example.rb
にてUserクラスの例を設計した。この設計は、同様の方法でオブジェクトを初期化するActive Record
の設計に基づいている。
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, password_digest: nil, remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil, reset_digest: nil, reset_sent_at: nil>
こうしてみると、nameとemail属性が設定されていることが分かる。
Active Recordを理解する上で、**有効性(Validity)**という概念も重要。
例えば、先ほどのuserオブジェクト
が有効かどうか確認してみる。
確認の為にvalid?
メソッドを使う。
#<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
>> user.valid?
=> true
現時点でtrueが返ってくるのはuserオブジェクトが有効だから(メモリ上でオブジェクトを作成しただけ)で、
データベースにデータがあるかどうかは有効性には関係ない
DBにUserオブジェクトを保存する為には、Userオブジェクトからsaveメソッドを呼び出す。
>> user.save
(0.1ms) begin transaction
SQL (8.8ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", "2018-12-18 23:40:49.783353"], ["updated_at", "2018-12-18 23:40:49.783353"]]
(4.6ms) commit transaction
=> true
>>
saveメソッドは成功すればtrueを、失敗すればfalseを返す。
RailsConsole上ではuser.save
に対応するSQLコマンドやその結果INSERT INTO "users"
も表示するようになっている。
作成した時点でのユーザーオブジェクトはid属性、マジックカラムのcreated_at属性・updated_at属性の値がいずれもnilであったが、saveメソッド後には
#<User id: 2, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-20 01:10:31", updated_at: "2018-12-20 01:10:31">
idに2という値が代入され、マジックカラムには現在の日時が代入されている。
これを更新すると値が変化する。
さらに、Userクラスと同様、Userモデルのインスタンスはドット記法でその属性にアクセスが可能。
>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Thu, 20 Dec 2018 01:10:31 UTC +00:00
モデルの生成と保存を二つのステップに分けておくと何かと便利だが、
Active RecordではUser.create
でモデルの生成と保存を同時に行う方法もある。
>> User.create(name: "A Nother", email: "another@example.org")
(0.1ms) SAVEPOINT active_record_1
SQL (0.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "A Nother"], ["email", "another@example.org"], ["created_at", "2018-12-20 01:31:40.553178"], ["updated_at", "2018-12-20 01:31:40.553178"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "A Nother", email: "another@example.org", created_at: "2018-12-20 01:31:40", updated_at: "2018-12-20 01:31:40">
User.createでは、論理値のtrueかfalseを返す代わりに、
ユーザーオブジェクト自身を返す点に注目
返されたオブジェクトは、例えばfoo = User.create
のように変数に代入することも可能。
destroyはcreateの逆。
>> foo = User.create(name: "Foo", email: "foo@bar.com")
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Foo"], ["email", "foo@bar.com"], ["created_at", "2018-12-20 01:34:49.287577"], ["updated_at", "2018-12-20 01:34:49.287577"]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 4, name: "Foo", email: "foo@bar.com", created_at: "2018-12-20 01:34:49", updated_at: "2018-12-20 01:34:49">
>> foo.destroy
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 4]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 4, name: "Foo", email: "foo@bar.com", created_at: "2018-12-20 01:34:49", updated_at: "2018-12-20 01:34:49">
createと同じように、destroyはそのオブジェクト自身を返す。
しかし、その戻り値を使ってもう一度destroyを呼ぶことはできない。
さらに、削除されたオブジェクトはまだメモリ上に残っている。
>> foo
=> #<User id: 4, name: "Foo", email: "foo@bar.com", created_at: "2018-12-20 01:34:49", updated_at: "2018-12-20 01:34:49">
ここで二つの疑問。
- オブジェクトが本当に削除されたかどうかどのようにして知ればいいのか
- 保存して削除されていないオブジェクトの場合、どうやってデータベースからユーザーを取得するのか
これらは次項で説明する。
####演習
1:user.nameとuser.emailがどちらもStringクラスのインスタンスであることを確認
>> user.name.class
=> String
>> user.email.class
=> String
2:create_atとupdated_atは、どのクラスのインスタンスか
>> foo.created_at.class
=> ActiveSupport::TimeWithZone
>> foo.updated_at.class
=> ActiveSupport::TimeWithZone
###ユーザーオブジェクトを検索する
まずはid1のユーザーを探してみる。
>> User.find(1)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
ここでは、User.find
にユーザーid1を渡してオブジェクトを返している。
次に、id=4のユーザーがまだデータベースに存在するかどうか確認してみる。
>> User.find(4)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=4
from (irb):44
foo.destroy
でid=4のユーザーを削除したので、ActiveRecordはこのユーザーをデータベースの中から見つけることができなかった。
代わりに、findメソッドはexceptionを発生する。
(exception = 例外)
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=4
findでActiveRecord::RecordNot ~ が返ってきてる。
これがexceptionである。例外的なイベントが発生した為 find User
できないという例外が発生した模様。
また、findメソッド以外に、Active Recordには特定の属性でユーザーを検索する方法もある。
>> User.find_by(email: "mhartl@example.com")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
このように、Userモデルにfind_byメソッドでemail属性と値を渡すことで、この値を持ったユーザーをDBから探すことができる。
ちなみに、このメソッドはユーザーをサイトにログインさせる方法を学ぶ時に役立つらしい。
また、ユーザー数が膨大になった際に特定の属性値で検索するfind_byメソッドでは効率が悪いと危惧するかも知れないが、この問題、そしてデータベースのインデックスを使った解決策については後に取り上げる。
他にユーザーを検索するには、first
メソッドやall
メソッドなどがある。
>> User.first
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
>> User.all
User Load (0.1ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">, #<User id: 2, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-20 01:10:31", updated_at: "2018-12-20 01:10:31">, #<User id: 3, name: "A Nother", email: "another@example.org", created_at: "2018-12-20 01:31:40", updated_at: "2018-12-20 01:31:40">]>
firstメソッドではデータベースの最初のユーザーを、
allメソッドでは全てのUserオブジェクトが返ってくる。
ここで、返ってきたオブジェクトのクラスがActiveRecord::Relation
となっている点に注目。
これは、各オブジェクトを配列として効率的にまとめてくれるクラスである
####演習
1:nameを使ってユーザーオブジェクトを検索してみる。また、find_by_nameメソッドが使えるか確認。
>> User.find_by(name: "Michael Hartl")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
>> User.find_by_name("Michael Hartl")
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
2:User.allが配列でないことを確かめる為に、User.alで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relation
クラスであることを確認する。
>> User.all.class
=> User::ActiveRecord_Relation
3:User.all.lengthで長さを求めてみる。
>> User.all.length
User Load (0.2ms) SELECT "users".* FROM "users"
=> 3
ん?lengthメソッドは文字列を返すんじゃなかったけ・・・
ということはallメソッドでは一つ一つのユーザーが文字列として認識されているということか?
さっきの演習でUser.allは配列ではないことを確かめたよね?
つまり、配列じゃない1,2,3のユーザーを3と数えたということか?
User.allはActiveRedcord_Relationクラスだから、このクラスについて詳しく調べる必要があるな・・・。
・・・このようにクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる
Rubyの性質をダックタイピングと呼ぶらしい。
###ユーザーオブジェクトを更新する
オブジェクトの更新方法は基本的に二つ。
1つ目
>> user = User.find(1)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-18 23:40:49", updated_at: "2018-12-18 23:40:49">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "mhartl@example.net"], ["updated_at", "2018-12-20 03:30:53.585378"], ["id", 1]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> true
この方法では**user.email = "メールアドレス"**で属性を個別に代入している。
そのあと、user.saveでデータベースに保存している。
なお、保存を行わずにreloadを実行すると
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> "mhartl@example.net"
データベースの情報を元にオブジェクトを再読み込みするので、変更が取り消される。
話を戻すが、user.saveを実行したことで、ユーザーが更新できた。
>> user.created_at
=> Tue, 18 Dec 2018 23:40:49 UTC +00:00
>> user.updated_at
=> Thu, 20 Dec 2018 03:30:53 UTC +00:00
saveしたことでマジックカラムの更新日時も更新されていることを確認できた。
2つ目の属性更新方法は、updated_attributes
を使う
>> user.update_attributes(name: "The Dude",email: "dude@abides.org")
(0.1ms) SAVEPOINT active_record_1
SQL (0.2ms) UPDATE "users" SET "email" = ?, "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["email", "dude@abides.org"], ["updated_at", "2018-12-20 03:36:10.863098"], ["name", "The Dude"], ["id", 1]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
update_attributes
メソッドは属性のハッシュを受け取り、
成功時には更新と保存を続けて同時に行う。(trueを返す)
ただし、検証(valid?)に一つでも失敗すると、update_attributes
の呼び出しは失敗する。
例えば、password保存の要求でバリデーションを設定していると、保存ができない時にupdate_attributesの呼び出しが失敗する。
特定の属性のみを更新したい場合は、次のようにupdate_attributes
を使う。
>> user.update_attribute(:name, "EI Duderino")
(0.1ms) SAVEPOINT active_record_1
SQL (0.6ms) UPDATE "users" SET "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["updated_at", "2018-12-20 03:59:17.171879"], ["name", "EI Duderino"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
>> user.name
=> "EI Duderino"
####演習
1:userオブジェクトへの代入を使ってname属性を更新し、saveで保存
>> user.name = "YUUKI"
=> "YUUKI"
>> user.save
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["updated_at", "2018-12-20 04:00:12.477014"], ["name", "YUUKI"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
2:update_attributesを使って、email属性を更新お呼び保存
>> user.update_attribute(:email, "yuuki@foo.com")
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "yuuki@foo.com"], ["updated_at", "2018-12-20 04:01:56.781841"], ["id", 1]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> true
sを抜く点に注意(update_attributes付きは複数の属性値を変更する場合に使う)
(問題文にはs付きで書いてあるけど)
3:マジックカラムであるcreated_atも直接更新できることを確認する。
>> user.update_attribute(:created_at, 1.year.ago)
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ? [["created_at", "2017-12-20 04:04:31.211527"], ["updated_at", "2018-12-20 04:04:31.212291"], ["id", 1]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> true
1.year.agoは現在の時刻から1年前の時間を算出するRails特有の時間指定方法。
##ユーザーを検証する
作成したUserモデルにアクセス可能なnameとemail属性が与えられた。
(attr_accessorメソッドで:nameと:emailというインスタンス変数を定義した)
この二つに値を渡して使うのだが、現在はどんな値でも受け取れてしまう為、ある程度の制限を付ける必要がある。
例えば、nameに値を渡す際空にしてはいけないとか、メールアドレスはフォーマットに従うとか、制限を設ける。
特にメールアドレスは一意のユーザー名として扱うので、データベース内で重複が起きないようにする必要がある
Active Recordでは、**Validation(検証)**という機能を通して制約を課す。
ここでは
- presence(存在性)
- length(長さ)
- format(フォーマット)
- uniqueness(一意性)
のValidationを行う。
###有効性を検証する
テスト駆動開発とモデルのバリデーション機能はピッタシの機能と言えるので、従来通りテストの失敗→成功といった流れで書いていく。
具体的なテスト方法は
①有効なモデルのオブジェクトを作成
②属性のうちの1つを有効でない属性に意図的に変更する
③バリデーションで失敗するかどうかテストする
念のため、最初に作成時の状態に対してもテストを書いておき、最初のモデルが有効であるかを確認する。
(こうすることで、バリデーションの実装に問題があったのか、オブジェクトそのものに問題があったのかを確認できる)
まずはUserモデルのテストw確認。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
最初は有効なオブジェクトに対してテストを書くため、setup
メソッドを使い、その中に有効なUserオブジェクト**@user**を作成する。
これはインスタンス変数だが、setupメソッド内で宣言しておけば、
全てのテスト内でこのインスタンス変数が使えるようになる
したがって、**valid?**メソッドを使ってUserオブジェクトの有効性をテストできる。
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid? #@user.valid?がtrueを返すと成功、falseを返すと失敗
end
このテストではシンプルなassertメソッドを使ってテストする。
@userはまだバリデーションを設定しておらず、有効なので成功する。
$ rails t
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
ここで、モデルのみテストを走らせてみる。
$ rails test:models
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
結果は同じ。なぜなら、先ほどのテストはguardの自動テストで保存したファイルのみテストを走らせたから。
####演習
1:コンソールから新しく生成したuserオブジェクトか有効(valid)かどうか確認。
>> @user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> @user.valid?
=> true
2:以前生成したuserオブジェクトも有効かどうか検証
>> User.find(1).valid?
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> true
>> User.find(2).valid?
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> true
以前生成した2つのユーザー。どっちもtrue。
###存在性を検証する
最も基本的なバリデーションは存在性。
Presenceという名で、こういったバリデーションのメソッドをバリデーションヘルパーと言う。
例えば、今回はユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証する。
(つまり、存在=true)
まずは、先ほどのUserモデルのテストファイルに、name属性の存在性に関するテストを追加する。
test "name should be present" do
@user.name = " "
assert_not @user.valid? #@userが有効でなくなったことを確認(@userが無効なら成功、有効なら失敗)
end
@userは有効なのでテストは失敗
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips
name属性の存在を検査する方法は、
validatesメソッドにpresence: trueという引数を与えること
validates :name, presence: true
presence: trueという引数は、要素が1つのオプションハッシュ。
つまり、presenceヘルパーでtrueを指定したことで、validatesにname属性の存在性を検証する値を渡している。
:nameの部分はハッシュ〜のキーだが、シンボルではないので単独での使用が可能
これでテストは成功。
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
カッコを使うとこのような表記になる
validates(:name, presence: true)
結果は同じ。
コンソールを起動して、Userモデルに検証を追加した効果を見てみる。
>> user = User.new(name: "", email: "mhartl@example.com")
=> #<User id: nil, name: "", email: "mhartl@example.com", created_at: nil, updated_at: nil>
>> user.valid?
=> false
presence: true
が効いているのでfalse
が返ってきた。
このように、user変数が有効かどうかをvalid?メソッドでチェックすることができる。
もし、オブジェクトが1つ以上の検証に失敗したときは、falseを返す。
また、全てのバリデーションが通った時にtrueを返す。
今回は検証が1つしかないので、どの検証が失敗したかわかる。
失敗した時に作られるerrorオブジェクトを使って確認してみる。
>> user.errors.full_messages
=> ["Name can't be blank"]
つまり、Nameは空(blank)にはできませんよと否定されている。
一応、blankメソッドで確認。
>> user.blank?
=> false
つまり、空白であるかどうか以前にUserオブジェクトにblank?が渡せない(validatesが引っ掛かるから)ってこと。
これは、先ほどのpresenceヘルパーが内部でblank?メソッドを使用している為、このような結果になる。
Userオブジェクトは有効ではないので、saveしようとしても失敗する。
>> user.save
(0.1ms) SAVEPOINT active_record_1
(0.1ms) ROLLBACK TO SAVEPOINT active_record_1
=> false
この変更を行った結果、テストは成功する。
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
email属性にもname属性と同じようなテストを書く。
test "email bhould be present" do
@user.email = " "
assert_not @user.valid?
end
validates :email, presence: true
テストを走らせると成功。
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
####演習
1:新しいユーザーuを作成し、作成した時点では有効ではない(invalid)ことを確認する。また、理由はなぜかエラーメッセージを確認してみる。
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]
NameとEmailがblankにできない(validationが効いている)
2:u.errors.messages
を実行すると、ハッシュ形式でエラーが取得できる。
emailに関するエラーだけを抽出してみる。
>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors.messages[:email]
=> ["can't be blank"]
*エラーメッセージはハッシュ(配列)で格納されている為、[:email]を指定して呼び出す。
###長さを検証する
存在性の次は文字列の長さを検証してみる。
今回は、ユーザーの名前の長さを制限する。
50文字を上限とし、51文字以上の名前は長すぎることを検証する。
また、メールアドレスについても問題になることがあるのでバリデーションを賭ける。
メールアドレスは255文字を上限とする。
(ほとんどのデータベースでは文字列の上限を255文字としている)
まずはテストから。
test "name should not be too long" do
@user.name = "a" * 51 #51文字の"a"を@user.nameに代入
assert_not @user.valid? #@userが有効でなくなった(nameが50文字より多い)ことを確認(@userが無効なら成功、有効なら失敗)
end
test "email should not be too long" do
@user.email = "a" * 244 + "@example.com" #244文字の"a"と"@example.com"を足し合わせた文字を@user.emailに代入
assert_not @user.valid? #@userが有効でなくなった(emailが255文字より多い)か確認(@userが無効なら成功、有効なら失敗)
end
ここで、"a"* 51というかけ算をコンソール上で行い、文字数を確認してみる。
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
メールアドレスの長さに対するバリデーションも検証
>> "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com"
>> ("a" * 244 + "@example.com").length
=> 256 #上限が255
この時点でテストは失敗
5 tests, 5 assertions, 2 failures, 0 errors, 0 skips
これをパスさせるために、長さを検証するための検証の引数を使う必要がある。
:nameと:emailに**:maximumパラメータと共に用いられる:length**を使って長さを強制する。
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
これでテストを走らせると通る。
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips
####演習
1:長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認
>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.name = "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> user.email = "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com"
>> user.valid?
=> false
2:バリデーション失敗の原因をエラーメッセージで確認
>> user.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
名前もEmailも文字が長い、最大値は名前が50でメールが255ですよ。と怒られている
###フォーマットを検証する
email属性の場合は最大文字数と空のメールアドレスかどうかのバリデーションしか掛けていない為、
他にもメールアドレスのパターン(user@example.comなどの)に合っているかどうかも確認してみる。
最初に、有効なメールアドレスと無効なメールアドレスのコレクションに対するテストを行う。
コレクションを作る方法として、文字列の配列を簡単に作れる%w[]
という便利なテクニックを使う。
>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
>> addresses.each do |address|
?> puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp
eachメソッドを使ってaddresses配列の各要素を繰り返し取り出した。
(これは第4章で学んだブロックの繰り返しテクニック)
メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい。
なので、有効なメールアドレスと無効なメールアドレスを用意し、バリデーション内のエラーを検知していく。
例: "user@example,com" → ✖︎ "user@example.com" → ●
#メールアドレスのフォーマットに対するテスト
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] #5つのアドレスを配列で指定
valid_addresses.each do |valid_address| #それぞれの要素をブロックvalid_addressに繰り返し代入。一個ずつ検証する。
@user.email = valid_address #@user.emailにブロックを代入
assert @user.valid?, "#{valid_address.inspect} should be valid" #@userが通ったらtrue、通らなかったらfalse。さらに、第二引数でどのメールアドレスで失敗したかエラーメッセージを追加
end
end
ここで重要なのがeachメソッドとブロックの中身。
各メールアドレスを一個ずつ順にテストしている。
さらに、assertメソッドの第二引数に注目。
assert @user.valid?, "#{valid_address.inspect} should be valid"
この文では、詳細な文字列を調べるためにinspect
メソッドを調べている。
この引数により、どのメールアドレスで失敗したかエラーメッセージが追加される。
次に、無効なメールアドレス(無効性(invalidity))の検証を行う。
例としては、.
が,
となっていたり、@
がないメールアドレスで失敗するかどうかを検証する。
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] #配列で5つのアドレス指定
invalid_addresses.each do |invalid_address| #それぞれの要素をブロックinvalid_addressに繰り返し代入。1つずつ検証。
@user.email = invalid_address #@user.emailにブロックを代入
assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" #@userが有効なら失敗、無効なら成功。第二引数で失敗したメールアドレスをそのまま文字列として表示
end
end
この時点でテストは失敗。
7 tests, 11 assertions, 1 failures, 0 errors, 0 skips
なぜなら@userが有効だから。(メールアドレスのvalidationは空か255文字以上しか検証していないから)
メールアドレスのフォーマットを検証するためには、format
を使う。
validates :email, format: { with: /<regular expression>/ }
ここで引数にregular expression
という正規表現を取っている。
今回はこのような正規表現を使う。
VALID_EMAIL_REGEX = /¥A[¥w+¥-.]+@[a-z¥d¥-.]+¥.[a-z]+¥z/i
Rubularを使って、上記の正規表現で通るメールアドレスを確認してみる。
枠で囲んであるアドレスは正規表現に合致しているが、
囲まれてないアドレスは合致していない。
これを実際にvalidationで使ってみる。
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 }
ここでテストが通ることを確認。
7 tests, 15 assertions, 0 failures, 0 errors, 0 skips
####演習
1:有効なメールアドレスと無効なメールアドレスを確認
2:foo@bar..com
のようなドットの連続を誤りを検知できない。このメールアドレスを無効なメールアドレスリストに追加し、テストが失敗することを確認。
invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
foo@bar_baz.com foo@bar+baz.com foo@bar..com]
$ rails test:models
7 tests, 16 assertions, 1 failures, 0 errors, 0 skips
さらに、下記のような複雑な正規表現を使ってテストがパスすることを確認。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
7 tests, 16 assertions, 0 failures, 0 errors, 0 skips
3:foo@bar..com
がRubularで失敗するかどうか、正規表現を使って確認。
###一意性を検証する
重複したメールアドレスに制限を掛ける為、validatesメソッドの:unique
オプションを使う。
ここで注意点としては、一意性のテストを書く際は、必ずレコードをデータベースに登録する必要がある。
その為、まずは重複したメールアドレスからテストしていく。
test "email addresses should be unique" do
duplicate_user = @user.dup #@userを複製する
@user.save #@userをデータベースに保存
assert_not duplicate_user.valid? #複製したduplicate_userが有効なら失敗、無効なら成功
end
重複したユーザーを作成する為、dupメソッドを使っている。
@userを保存した後では、複製されたユーザーのメールアドレスが存在する。assert_not
を使っている為、@user
複製が有効だと失敗する。
test_email_addresses_should_be_unique#UserTest (0.05s)
Expected true to be nil or false
test/models/user_test.rb:56:in `block in <class:UserTest>'
8/8: [==================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.05117s
8 tests, 17 assertions, 1 failures, 0 errors, 0 skips
テストをパスさせるため、emailバリデーションにuniqueness: true
というオプションを追加。
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 },
uniqueness: true #メールアドレスに「一意性がtrue」というオプションを追加
テストがパスする。
8 tests, 17 assertions, 0 failures, 0 errors, 0 skips
これでOK・・・ではなく、通常、メールアドレスの文字列は大文字小文字の区別がないため、どちらの場合も検証しなければならない。
つまり、テストに同一メールアドレス文字が大文字の場合の検証を追記する必要がある。
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を複製する
duplicate_user.email = @user.email.upcase #複製したduplicate_userのメールアドレス欄の文字列を大文字にする
@user.save #@userをデータベースに保存
assert_not duplicate_user.valid? #@userの複製が有効なら失敗、無効なら成功
end
一度Rails Console で確認。
$ rails c --sandbox
Running via Spring preloader in process 15721
Loading development environment in sandbox (Rails 5.1.6)
Any modifications you make will be rolled back on exit
>> user = User.create(name: "Example User", email: "user@example.com")
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2018-12-22 22:56:54", updated_at: "2018-12-22 22:56:54">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "USER@EXAMPLE.COM"], ["LIMIT", 1]]
=> true
ここで複製したユーザーのメールアドレスを大文字にしたもののvalidationがtrueになっている
>> duplicate_user
=> #<User id: nil, name: "Example User", email: "USER@EXAMPLE.COM", created_at: nil, updated_at: nil>
>> user
=> #<User id: 3, name: "Example User", email: "user@example.com", create
↑大文字小文字で区別はされているが同じメールアドレスが複製可能となっている
これは本来ならばvalid?でfalseにしなければならない。
ということで、:uniqueness
に:case_sensitive
オプションを渡してみる。
uniqueness: { case_sensitive: false } #大文字小文字を区別しない(false)に設定する このオプションには通常のuniquenessはtrueと判断する。
ここで重要なのは、このオプションには一意性の検証も含まれる。
つまり、追記でuniqueness: true
とする必要がない
これでテストがパスする。
8 tests, 17 assertions, 0 failures, 0 errors, 0 skips
ようやく・・・完成。。。と思ったら、まだあるらしい。
どうも、まだActive Recordはデータベースのレベルでは一意性を保証できていないんだとか。
具体的にどういうことか、というと、このままビルドしてアプリケーションをローンチしてしまうと、
ユーザーが会員登録する場合、登録申請ボタンを素早く2回クリックした場合、リクエストが二つ連続で送信され、同一のメールアドレスを持つユーザーレコードが(一意性の検証を行なっているにも関わらず)作成されてしまう。
RailsのWebサイトでは、トラフィックが多い時にこのような問題が発生する模様。
実はこの問題も簡単に解決できる模様。
その方法は、データベースレベルでも一意性を強制すること
具体的には、データベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意であるようにすれば解決する(らしい)
また、データベース上のユーザー探索で、findメソッドを使って探す場合、今のままでは先頭から順にユーザーを一人ずつ探していくことになる。そうなると、例えばユーザーが1000人登録されている場合、1000番目のユーザーを探し当てるには1000回もサーチしなければならない。
これはデータベースの世界では**全表スキャン(Full-table Scan)**として知られており、
DBへの負担やリクエストの回数など考えても極めてよくない。
なので、その点からもemailのカラムにインデックスを追加すれば、割り当てたインデックスを管理する表の中から探し当てればいい訳で、アルゴリズムの計算量が比較的少なくて済む。
Userのemailカラムにインデックスを与えてfindで探す場合、ハッシュ表探索法でサーチしてる認識でいいのかな?
(一回で探し当てるって書いてあったのでアルゴリズム計算量O(1)だと勝手にそう解釈したけど)
emailカラムにインデックスを追加するためには、migrationジェネレーター
を使ってマイグレーションを直接作成する必要がある。
$ rails g migration add_index_to_users_email
Running via Spring preloader in process 19811
invoke active_record
create db/migrate/20181223003323_add_index_to_users_email.rb
生成されたファイルに、add_indexというメソッドを使ってusersテーブルのemailカラム
に、一意性を強制する為のunique: true
オプションを渡す。
class AddIndexToUsersEmail < ActiveRecord::Migration[5.1]
def change
add_index :users, :email, unique: true
end
end
設定したらデータベースに反映
$ rails db:migrate
この時点ではテストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テスとは失敗する。
今まではgenerate
コマンドでfixtureが自動的に生成されていたが、
今回はmigration
コマンドを使用した為生成されていない。
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
このメールアドレスが一意になっていない為、失敗するのだが、これまでテストが通っていたのはバリデーションを中継していない為である。
今回は、fixtureは削除しておく
#8章まで使わない
これでテストはパスする。
しかし、メールアドレスの一意性を保証するにはあと一つやらなければならないことがある。
それは、いくつかのDBアダプタが、常に大文字小文字を区別するインデックスを使っているとは限らないから。
例えばFoo@ExAMPle.Com
とfoo@example.com
が別々の文字列と解釈してしまうケースがある。
これらの文字列は同一であると解釈されるべきなので、
今回はデータベースに保存される直前に全ての文字列を小文字に変換する
これを実装するために、Active Recordのcallback
メソッドを利用する。
これはある特定の時点で呼び出されるメソッド。今回はオブジェクトが保存される前に実行したいため
before_save
というコールバックを使う。
before_save { self.email = email.downcase } #オブジェクトが保存される前に、インスタンス変数(email)自身に、小文字のemailの値を代入。
befre_saveコールバックに{}内のブロックを渡してユーザーのメールアドレスを設定している。
現在の値をStringクラスのdowncaseメソッドを使って、小文字にしたメールアドレスに変換しているため、
大文字小文字が混同する心配がなくなる。
また、上記のコードは
before_save { self.email = self.email.downcase }
と書くこともできるが、Userモデルの中では右式のselfを省略できる。
(ここのselfは現在のユーザーを指す)
左側のselfは省略することはできないので注意。
これで、先程の問題点をクリアにできた。
- 二度目のレコード保存のリクエストは一意性の制約によって拒否
- emailにインデックス属性を追加したことで、サーチ効率の向上
- 大文字小文字混同のメールアドレスをDBsave前に小文字に変換し一意性を保証
####演習
1:メールアドレスを小文字にするテストを書く
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 #第一引数で@userのEmailを小文字に変換、第二引数でDBからEmail(大文字小文字混同のemail)を再読み込み、この二つが同一であればtrueを返す
end
この状態でテストを走らせると成功し、Userモデルのbefore_action
をコメントアウトすると失敗することを確認。
#before_save { self.email = email.downcase }
Expected: "foo@example.com"
Actual: "Foo@ExAMPle.Com"
test/models/user_test.rb:64:in `block in <class:UserTest>'
9/9: [==================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.06880s
9 tests, 18 assertions, 1 failures, 0 errors, 0 skips
2:破壊的メソッドを使い、selfを使わずにemailの文字列を小文字に変換
before_save { email.downcase! }
テストがパスすればOK
##セキュアなパスワードを追加する
セキュアなパスワードという手法では
各ユーザーにパスワードとパスワードの確認を入力させ、それをハッシュ化したものをデータベースに保存する
ハッシュ化されたパスワードは、第8章のログイン機構でユーザーを認証する際に利用する。
ユーザーの認証は
1:パスワードの送信
2:ハッシュ化
3:データベース内のハッシュ化された値と比較
という手順で進める。
ここでの3のハッシュ値はユーザーが新規会員登録時に送信したパスワードをハッシュ化したハッシュ値を使う為、
会員登録時には比較は行わない(ログインではない為)
生のパスワードを平文と言うが、平文のままDBに保存することは危険な為、必ずハッシュ化させておく必要がある。
仮にパスワードの内容が盗まれても、パスワードの安全性が保たれる。
###ハッシュ化されたパスワード
セキュアなパスワードの実装はhas_secure_password
メソッドの呼び出しだけで殆ど完成してしまう。
class User < ApplicationRecord
has_secure_password
このメソッドによって
- ハッシュ化したパスワードをDB内のpassword_digest属性に保存できる
-
password
とpassword_confirmation
が使えるようになり、存在性と値が一致するかどうかのバリデーションも追加される - authenticateメソッドが使えるようになる
has_secure_password機能を使えるようにするには、モデル内にpassword_digestという属性を追加する必要がある。
今回はUserモデルで使うので、usersのデータモデルは以下のようになる。
出典:図 6.8: Userモデルにpassword_digest属性を追加する
データモデルを上記のようにするため、まずはpassword_digest
カラム用のマイグレーションを生成する。
$ rails g migration add_passwrod_digest_to_users password_digest:string
Running via Spring preloader in process 29699
invoke active_record
create db/migrate/20181223043336_add_passwrod_digest_to_users.rb
生成されたファイルを確認。
class AddPasswrodDigestToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :password_digest, :string
end
end
ファイル名の末尾にto_users
を入れたことで自動的に:usersが入り、password_digestの引数を与えた値も入っている。
データベースにマイグレーションを反映させる為に、rails db:migrate
実行。
rails db:migrate
ハッシュ関数の利用にはbcrypt
gemを追加する。
gem 'bcrypt', '3.1.12'
bundle installを実行
bundle install
###ユーザーがセキュアなパスワードを持っている
Userモデルにhas_secure_passwordが使えるようになったので、早速使用してみる。
has_secure_password
ここでテストを走らせると失敗する。その理由は、has_secureを書いたことによりpassword属性とpassword_confirmation属性に対してのバリデーションが自動で追加されていることによる。
テストをパスさせる為に、以下のキーと値を追加する。
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
テストを走らせる。
9 tests, 18 assertions, 0 failures, 0 errors, 0 skips
####演習
userオブジェクトを作成し、有効な名前とメールアドレスを与えてもvalid?で失敗する理由
>> user = User.new(name: "yuuki", email: "yuuki@foo.com")
=> #<User id: nil, name: "yuuki", email: "yuuki@foo.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "yuuki@foo.com"], ["LIMIT", 1]]
=> false
>> user.errors.full_messages
=> ["Password can't be blank"]
###パスワードの最小文字数
パスワードを簡単に当てられないようにする為、パスワードの最小文字数を設定する。
今回は以下の二つのバリデーションを設定する。
- パスワードが空でないこと
- パスワードの最小文字数を6文字にする
まずはテストから書いていく
test "password should be present (nonblank)" do #passwordとpassword_confirmationが空かどうか検証
@user.password = @user.password_confirmation = " " * 6 #二つの属性に空白文字を6個代入
assert_not @user.valid? #@userが有効なら失敗、無効なら成功
end
test "password should have a minimum length" do #passwordとpassword_confirmationが最低6文字以上あるかどうか検証
@user.password = @user.password_confirmation = "a" * 5 #二つの属性に"a"をを5個代入
assert_not @user.valid? #@userが有効なら失敗、無効なら成功
end
ここで注目したいのは
@user.password = @user.password_confirmation = "a" * 5
の部分。=が二つ付いている、このような構文を多重代入(multiple Assignment)と呼ぶ。
上記の文では@user.password
と@user.password_confirmation
に"aaaaa"
を代入している。
ちなみに、has_secure_passwordメソッドの存在性のバリデーションは、
新規にレコードが追加された時だけしか適用されず、例えばレコードのユーザー名を空白の文字で埋めて更新しようとすると、バリデーションが適用されずに更新されてしまう
この時点でテストはもちろん失敗。
テストをパスさせる為に、パスワードが空でないこと、最低6文字以上のバリデーションを掛ける。
validates :password, presence: true,length: { minimum: 6 } #passwordの文字列が空でなく、6文字以上ならtrue
テストがパスする。
10 tests, 14 assertions, 0 failures, 0 errors, 0 skips
####演習
有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトがfalseになることを確かめ、エラーメッセージを確認。
$ rails c --sandbox
>> user = User.new(name: "yuuki", email: "yuuki@foo.com", password: "asdf")
=> #<User id: nil, name: "yuuki", email: "yuuki@foo.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$/JBZ/iqkeaQRdRx8N/NSW.BA23YaZPo7OzllMIeNPvR...">
>> user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "yuuki@foo.com"], ["LIMIT", 1]]
=> false
>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]
パスワードが短い、最低でも6文字以上の文字列にせよ、とエラーがでている。
###ユーザーの作成と認証
ここでhas_secure_passwordやauthenticateメソッドの効果を確かめるために、
Rails Consoleを使い、実際にデータベースにユーザーを作成してみる。
User.create(name: "Michael Hartl", email: "mhartl@example.com",
?> password: "foobar", password_confirmation: "foobar")
(0.1ms) begin transaction
User Exists (0.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]]
SQL (4.8ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", "2018-12-23 20:59:49.589763"], ["updated_at", "2018-12-23 20:59:49.589763"], ["password_digest", "$2a$10$W5uH1QYvbuqcARBxVnyDE.hVIHNuGMB4DW67hCv7Y/pgqTJ3XAkUS"]]
(13.5ms) commit transaction
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-23 20:59:49", updated_at: "2018-12-23 20:59:49", password_digest: "$2a$10$W5uH1QYvbuqcARBxVnyDE.hVIHNuGMB4DW67hCv7Y/p...">
実際にデータベースに保存されたかどうか SQLiteでusersテーブルを確認。
コンソールで作成したユーザーをサーチしてみる
>> user = User.find_by(email: "mhartl@example.com")
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-23 20:59:49", updated_at: "2018-12-23 20:59:49", password_digest: "$2a$10$W5uH1QYvbuqcARBxVnyDE.hVIHNuGMB4DW67hCv7Y/p...">
>> user.password_digest
=> "$2a$10$W5uH1QYvbuqcARBxVnyDE.hVIHNuGMB4DW67hCv7Y/pgqTJ3XAkUS"
foobar
という文字列がハッシュ化されているのが分かる。
これは、bcrypt
を使って生成しているので、この文字列から元々のパスワードを導出することはほぼ不可能。
さらに、has_secure_password
をUserモデルに追加したことで、authenticate
メソッドが使えるようになっている。
>> user.authenticate("not_the_right_password")
=> false
>> user.authenticate("foobaz")
=> false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-12-23 20:59:49", updated_at: "2018-12-23 20:59:49", password_digest: "$2a$10$W5uH1QYvbuqcARBxVnyDE.hVIHNuGMB4DW67hCv7Y/p...">
引数に間違ったパスワードを与えると、false
を返し、正しいパスワードを与えると、ユーザーオブジェクトを返していることが分かる。
後にauthenticateメソッドを使ってユーザーを認証しログイン機構を作る。
なお、authenticateは返す論理値はtrueであることは確認しておきたい。
>> !!user.authenticate("foobar")
=> true
この性質を利用すると、user.authenticateを活用できる。
####演習
コンソールを再起動し、userオブジェクトを検索して名前を新しい文字列に変えてみる。
>> user.name = "YUUKI"
=> "YUUKI"
>> user.save
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]]
(0.1ms) rollback transaction
=> false
>> user.errors.full_messages
=> ["Password can't be blank", "Password is too short (minimum is 6 characters)"]
>> user.password
=> nil
保存できない理由は、passwordに値が入っていないから。会員制のサイトでユーザー名を更新する時にパスワードの入力が求められるのに入力せずに更新しようとしたらエラーになるあの仕組みだと思われるw
今度はupdate_attributes
メソッドを使って
nameとpasswordの値を同時に更新してみる。
>> user.update_attributes(name: "YUUKI", password: "foobaka")
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]]
SQL (6.3ms) UPDATE "users" SET "name" = ?, "password_digest" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "YUUKI"], ["password_digest", "$2a$10$AxSaG6sZMlG/lLCye8qiDeqLEBuYPsltBx5glmWhpU.d2sAwjOrsW"], ["updated_at", "2018-12-23 21:35:02.139347"], ["id", 1]]
(8.7ms) commit transaction
=> true
>> user
=> #<User id: 1, name: "YUUKI", email: "mhartl@example.com", created_at: "2018-12-23 20:59:49", updated_at: "2018-12-23 21:35:02", password_digest: "$2a$10$AxSaG6sZMlG/lLCye8qiDeqLEBuYPsltBx5glmWhpU....">
>>
できた。
##Git・デプロイ
とりあえずテスト・コミット
$ rails t
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"
次にmasterブランチにマージ。その後プッシュ
$ git checkout master
$ git merge modeling-users
$ git push
Herokuにpush後、本番環境でマイグレーションを走らせる為にheroku run コマンドを打つ。
$ rails t
$ git push heroku
$ heroku run rails db:migrate
うまくいったかどうか、本番環境のコンソールに接続し確認できる
$ heroku run rails console --sandbox
User.create(name: "Michael Hartl", email: "michael@example.com",
password: "foobar", password_confirmation: "foobar")
作成できた。
#単語集
- ブラックボックス
内部構造が明らかになっていないが何となく使っている、ような状態のこと。
中身がわかっていないにも拘らず、プログラムの動作だけを確認するテスト手法に「ブラックボックステスト」というものもある。
- Active Record
データオブジェクトの作成/保存/検索のためのメソッドをもったRailsライブラリ。
これらのメソッドを使う上で、関係データベースで使うSQLを意識する必要が殆どない。
- DDL
データ定義言語。DBのTableに命令する言語のことであり、主な命令にcreate table
がある。
- マイグレーション
Active Recordの機能の一つ。
マイグレーション用の言語DSLを使用し、テーブルの変更を簡単に記述できる。
- RDB
リレーショナルデータベースの略。列(カラム)と行(レコード)で作られ、データ型が定義されておりなおかつ他の表とリレーションシップ(関連線)があるデータベースのこと。完全に正規化されたデータベースのことである。
- コンフリクト
データやファイルが同じタイミングで実行されることで動作が不安定になったりすること。
DB関連でよく使われる。
- create_table
DBにテーブルを新規作成するRails特有のメソッド。
- timestamps
Railsの特別なメソッドで、created_atとupdated_atという二つのマジックカラムを作成する。
これらは、あるユーザーが作成または更新された時に、その時刻を自動的に記録するタイムスタンプのこと。
データ型はdatetime
となる。
- integer
データ型の一つで、整数(小数点以下がない数)を扱う時に使われる。
- ロールバック(rollback)
トランザクション(DBに対するひとまとまりの処理)を開始して何らかの障害が発生した場合、直前のチェックポイントの状態まで戻す。
逆の言葉にロールフォワードがある。
- sandbox
Rails Consoleをデータベースに反映せずに行うモードのこと。使い方は
rails c --sandbox
なお、データベースへの変更を取り消してくれる証拠に
Any modifications you make will be rolled back on exit
このようなメッセージが表示される。
これを略すると**「ここで行った全ての変更は終了時にロールバックされます」**
- find
データベースからオブジェクトを見つけるメソッド。引数に条件を指定してデータベースから検索する。
例
User.find(1) # id1のユーザーをユーザーモデルの中から探し出す。
- exception
例外という意味。Railsではプログラムの実行時に何か例外的なイベントが発生したことを示すために使われる。
- ActiveRecord::Relation
各オブジェクトを配列として効率的にまとめてくれるクラスのこと。
- SQLクエリ
DBMSに対するSQL命令のこと。
- setup
各テストが走る直前に実行される特殊なメソッド。
- assert
指定した要素の真偽を問うメソッド。
trueなら成功、falseなら失敗を返す。
- presence
渡された属性が存在することを検証する。
つまり、値がnilや空文字でないことを確認している。
- オプションハッシュ
Railsヘルパーの引数の中で、オプションで何らかの属性と値を指定することができるRailsの機能。
例
link_to "sample_app", "#", id: "logo" #第三引数でid属性に"logo"と指定することで、cssでidlogoを指定できている。
- errors.full_messages
エラーのメッセージをフルに返してくれるメソッド。
- collection
全部のデータにアクションを使用する。
- 正規表現
いくつかの文字列を一つの形式で表現するための表現方法。
いくつかの文字列に対して下記のような特殊文字を与えて使う。特殊文字はメタ文字とも呼ばれる。
. ^ $ [ ] * + ? | ( )
- format
withオプションで与えられた正規表現と属性の値がマッチするかどうかをテストすることによって、バリデーションを行う。
使い方
validates :legacy_code, format: { with: 正規表現(例/\A[a-zA-Z]+\z/) }
上記のwithオプションで与えられた正規表現の値に合致すればtrue、合致しなければfalse
- 定数
変数の更新できないバージョン。定数を定義する際は、先頭の文字を大文字にする。
例:
Test = 4 * 5
puts Test
他言語では定数は値を上書きできないが、rubyの場合は警告メッセージは出るが上書きできる。
>> TEST = 5
=> 5
>> TEST = 10
(irb):6: warning: already initialized constant TEST
(irb):5: warning: previous definition of TEST was here
=> 10
値を上書きされたくなければ、定数をmoduleに格納し、module及び定数をfreezeする。
>> module TEST
>> TOWN = "TOKYO".freeze
>> end
=> "TOKYO"
>> TEST.freeze
=> TEST
>> TEST::TOWN = "aa"
RuntimeError: can't modify frozen Module
from (irb):9
>> TEST::TOWN.downcase!
RuntimeError: can't modify frozen String
from (irb):10:in `downcase!'
from (irb):10
- dup
同じ属性を持つデータを重複するためのメソッド。
- fixture
テスト実行前に初期データを投入するディレクトリのこと。拡張子はyml
- コールバック
ある特定の地点で呼び出されるメソッド。書き方は
before_save {オブジェクトが保存される前に行う処理を書く}
様々なコールバックを使って、オブジェクトを作成したり、更新したり、削除したりできる。
- ハッシュ化
ハッシュ関数を用いて、不可逆(元に戻せない)なハッシュ値に変換する方法。
- add_column
カラムを追記するメソッド。
- authenticate
引数に渡された文字列をハッシュ化した値と、データベース内にあるpassword_digestのカラム値を検証する。