LoginSignup
0
0

More than 3 years have passed since last update.

rails-tutorial第6章

Last updated at Posted at 2020-06-04

そもそもなんで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はデフォルトで入っている。

このコマンドにより、テストファイルやマイグレーションファイルが作成される。
以下は作成されたマイグレーションファイル

db/migrate/[timestamp]_create_users.rb
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ファイルを見てみよう

app/models/user.rb
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を書いていったほうが早い

モデルのテストを見ていこう

test/models/user_test.rb
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

とすると良い。
このテストの場合、バリデーションが設定されてないのでテストは通る。

では次のテストはどうだろうか?

test/models/user_test.rb
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を設定しよう

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
end

validationはmodelのファイルに書く。

次は長さを検証してみよう

test/models/user_test.rb
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回打つなどの面倒を避ける。

この場合、バリデーションを定義していないのでテストは失敗してしまう。
なので、

app/models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

とすることで、文字数のバリデーションを定義できる。

適切なメールアドレスかチェックする

test/models/user_test.rb

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つ目のテストで失敗してしまう。

なので、正規表現でバリデーションを設定しよう。

app/models/user.rb
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内に複数あると困る。

テストを見てみよう

test/models/user_test.rb

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ですか?というテストである。このテストは落ちる。

じゃあ、一意性を担保するには?

app/models/user.rb
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とすると一意性が担保される確率が上がる。

まだ、このままだとメールアドレスの大文字小文字を区別できない。
そのため、

test/models/user_test.rb
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がデフォルトで大文字小文字を区別するようになっているためだ。

そこで

app/models/user.rb
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

できたマイグレーションファイルに

db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
  def change
    add_index :users, :email, unique: true
  end
end

このように書く。
unique: trueは一意性をDB側でも担保してくださいねーってお願い。

で、 rails db:migrateをする。

これでOKか?
いや、このままだと、テストが全て通らなくなってしまう。

これは以下が原因となっている。

test/fixtures/users.yml
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が勝手に判断して以下のようにコーディングしてくれる。

db/migrate/[timestamp]_add_password_digest_to_users.rb
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と書けば完了。以下

app/models/user.rb
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だけ。

そこで、

test/models/user_test.rb
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文字以上とかにするバリデーションを設定してあげる。

app/models/user.rb
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
を忘れずに!!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0