そもそもなんでmodelが必要なの?
・永続的な情報を保存したいけど普通の変数だと実現できない。
・永続的な情報の保存にはDBを使わないといけない。
・ActiveRecordを使うと変数のようにDBに保存をすることができる。
・モデルはActiveRecordを継承したApplicationRecordを継承している。
・つまり、RubyとDBの橋渡しをしてくれるから。
Userモデルの作成
$ rails generate model User name:string email:string
モデルの作成時はUserのように単数形で書く。
name:string はnameカラムでデータ形式はstringだよーって意味。
ちなみに id:integer created_at:datetime updated_at:datetimeはデフォルトで入っている。
このコマンドにより、テストファイルやマイグレーションファイルが作成される。
以下は作成されたマイグレーションファイル
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
これで $ rails db:migrateをすると、Userリソースを保存するためのテーブルが作成される。
rails g modelコマンドだけではテーブルは作成されないから注意が必要。
$ rails db:rollback
このコマンドによってrails db:migrateの更新を戻すことができる。
modelファイルを見てみよう
class User < ApplicationRecord
end
UserクラスがApplicationRecordを継承していることがわかる。
この継承によって、Userクラスのインスタンスにsaveメソッドが使えるようになり、DBに保存することができる。(findメソッドとかallメソッドとか色々使える。)またマジックカラム(id ,created_at, updated_at)はDBに保存されて初めて値が埋まる。
User.create
User.create(name: "A Nother", email: "another@example.org")
user.new, user.saveなどが面倒なときは、User.createでいきなりDBベースに保存することができる。
また、
u = User.create(name: "A Nother", email: "another@example.org")
createメソッドはUserインスタンスを返すので、上記でDBに保存し、かつローカル変数uに代入することができる。
findメソッドとfind_byメソッドの違い
findメソッドは見つからなかったときに例外を出すのに対して、find_byは見つからなかったときにnilを返す。
update_attributes
update_attributesはcreateと似ていて、更新のショートカットを可能にする。
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
このように1行で書ける。
update_attribute
update_attributeは二つの引数を使って更新する。
>> user.update_attribute(:name, "El Duderino")
=> true
update_attributeはvalidationを介さずにDBに登録をすることができるという特徴がある。
ユーザーを検証する
モデルの場合、テストコードを先に書いて、あとでvalidationを書いていったほうが早い
モデルのテストを見ていこう
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メソッドはその下の test do endが実行される直前に実行されるという特徴がある。
また、modelだけテストをしたいときは、
$ rails test:models
とすると良い。
このテストの場合、バリデーションが設定されてないのでテストは通る。
では次のテストはどうだろうか?
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
この場合、user.nameが空文字の際に、@userはnot validじゃなきゃいけないよね?っていうテスト。
この場合、バリデーションが設定されていないので、テストは失敗する。
validationを設定しよう
class User < ApplicationRecord
validates :name, presence: true
end
validationはmodelのファイルに書く。
次は長さを検証してみよう
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 long" 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
@user.nameはsetupメソッドでテスト直前に定義されるから問題ない。
"a" * 51とすることで、aを50回打つなどの面倒を避ける。
この場合、バリデーションを定義していないのでテストは失敗してしまう。
なので、
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
とすることで、文字数のバリデーションを定義できる。
適切なメールアドレスかチェックする
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[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
end
現在、メールアドレスは存在性と長さしかバリデーションを設定してないので、2つ目のテストで失敗してしまう。
なので、正規表現でバリデーションを設定しよう。
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
formatオプションで設定すると、その型通りじゃないとvalidにならないようになる。
ちなみにVALID_EMAIL_REGEXは定数である。
一意性を知ろう。
同じメールアドレスがDB内に複数あると困る。
テストを見てみよう
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
dupは複製するメソッド。
@userがDBに保存された状態で、duplicate_userという複製されたインスタンスはvalidですか?というテストである。このテストは落ちる。
じゃあ、一意性を担保するには?
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
このようにuniqueness: trueとすると一意性が担保される確率が上がる。
まだ、このままだとメールアドレスの大文字小文字を区別できない。
そのため、
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
最後のテスト、@userがDBに登録され、またメールアドレスが大文字になったインスタンスはnot validになるか?というテストなのだが、落ちてしまう。これは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 }に変えてあげる。
case_sensitive: falseは大文字小文字を区別しなくてもいいよーって意味。
これで大文字小文字関係なく、スペルが同じアドレスは登録できなくなり、一意性が担保されるようになる?
いや、まだだ。
全く同じメールアドレスが全く同じ時間に登録されたらどうなるか?
なんと、どちらも登録されてしまう。。。
なので、DBにも一意性を担保してもらうようにお願いをしなければいけない。
具体的には片方のアドレスが登録されるまで次の登録を待ってもらう。
その際はマイグレーションファイルを使う
$ rails generate migration add_index_to_users_email
できたマイグレーションファイルに
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
def change
add_index :users, :email, unique: true
end
end
このように書く。
unique: trueは一意性をDB側でも担保してくださいねーってお願い。
で、 rails db:migrateをする。
これでOKか?
いや、このままだと、テストが全て通らなくなってしまう。
これは以下が原因となっている。
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
テスト用データベースの中に、MyStringというメールアドレスが2つあり、一意性に引っかかってしまったのだ。
解決策としては、このファイルの内容を全て消してあげれば良い。
セキュアなパスワードを設定する。
まずは散らばった文字列のパスワードのハッシュ値を入れる場所を作る。
$ rails generate migration add_password_digest_to_users password_digest:string
ちなみにマイグレーションファイル名を add_カラム名_to_テーブル名とするとRailsが勝手に判断して以下のようにコーディングしてくれる。
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :password_digest, :string
end
end
あとは、上のファイルが自分のやりたいことと一致するか確認して、
$ rails db:migrateを実行する。
これでパスワードのハッシュ値を保存する場所ができた。
bcrypt
次に、パスワードをハッシュ化するためのgem bcryptをインストールする。
gem 'bcrypt', '3.1.12'
bundle
そして、モデルファイルに 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
ただ、この状態だとテストが落ちてしまう。
理由はtestファイルのsetupメソッドにpassword属性とpassword_confirmation属性の値を指定していないためらしい。
ちなみにpasswordとpassword_confirmationは仮想的な属性で、実際にDBに保存されるのはpassword_digestだけ。
そこで、
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
というように、仮想的な属性を指定してあげる。これで一応テストは通る。
あとは、パスワードの文字数を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: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
end
パスワードの存在性と長さ6文字以上を指定。
これで一応セキュアなパスワードは実装完了。
パスワードの認証について
has_secure_passwordをUserモデルに追加したことで、そのオブジェクト内でauthenticate()メソッドが使えるようになっています。このメソッドは、引数に渡された文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較します。
マイグレーションファイルを色々設定したあとは、
$ heroku run rails db:migrate
を忘れずに!!!