0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Railsチュートリアルまとめ6 ユーザーのモデルを作成する

Posted at

個人的リマインド用

参考
Ruby on Rails チュートリアル プロダクト開発の0→1を学ぼう

ユーザーのモデルを作成する

Userモデルの作成

データモデルとして扱うデフォルトのデータ構造のことをモデル(Model)という。データを永続化するために、データベースを使ってデータを保存し、データベースとやりとりするためにRailsライブラリのActive Recordを使う。
さらにRailsにはマイグレーション(Migration)という機能があり、データの定義をRubyで記述することができるため、SQLのDDL(Data Definition Language)を学ぶ必要がない。

データベースのマイグレーション

まずモデルを作成する

rails g model User name:string email:string

モデル名の後にオプションをつけることで、データベースで使いたい2つの属性をRailsに伝える。
そしてgenerateを実行するとマイグレーションと呼ばれるファイルも新しく生成される。マイグレーションは、データベースの構造を段階的に変更する手段を提供してくれる。

db/migrate/[timestamp]_create_users.rb

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まり。changeメソッドはcreate_tableというメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成する。自分で設定していたnameやemail以外にも、created_atやupdate_atという2つのマジックカラムを生成してくれる。
ちなみにモデル名は単数系だが、テーブル名は複数形。

上記のマイグレーションが生成された後は、マイグレーションを適用する作業に入る。

rails db:migrate

モデルファイル

rails g modelでつくられるファイルは2つあり、1つがマイグレーションファイル、もう1つがモデルファイルである。

app/models/user.rb

class User < ApplicationRecord
end

ユーザーオブジェクトを作成する

rails console --sandboxで実行

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

引数なしで読んだ場合は、全てがnil。オブジェクトの属性を設定する初期化ハッシュを引数として受け渡すと

>> user = User.new(name: "Michael Hartl", email: "michael@example.com")
>> user
=> #<User id: nil, name: "Michael Hartl", email: "michael@example.com",
created_at: nil, updated_at: nil>

この設計は、同様の方法でオブジェクトを初期化するActive Recordの設計に基づいている。
またActive Recordを理解する上で、**「有効性(Validity)」**という概念も重要。

>>user.valid?
true

現時点でデータベースにデータは格納されていない。つまりUser.newはメモリ上でオブジェクトを作成しただけで、user.valid?もオブジェクトが有効かどうかを確認しただけだから、データベースにデータが存在するかどうかは有効性と関係ない。

user.save

データベースにデータを保存。ここでマジックカラムにデータが入る。

>> foo = User.create(name: "Foo", email: "foo@bar.com")

User.createを行うとモデルの生成と保存を同時に行える。

foo.destroy

destroyはデータベースから削除するが、メモリ上にはまだデータが残る。

ユーザーオブジェクトを検索する

findは見つかったらユーザーを返してくる。見つからなかったら例外を発生する。

User.find(1) ←idを渡す

find_byを使うと属性を指定してユーザーを検索できる。

User.find_by(email: "michael@example.com"

また、

User.find
User.all

こんなのもある。

ユーザーオブジェクトの更新

基本的な方法は2つ。
1つ目は属性を個別に代入する。

user.email = "mhartl@example.net"
user.save

saveを忘れずに。reloadするとデータベースの情報を元にオブジェクトを再読み込みするので、次のように変更が取り消される。

>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"

2つ目はupdateする方法

>> user.update(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"

成功した時は更新と保存を同時にする。ただし、バリデーションに1つでも引っかかると失敗する。
特定の属性のみを更新したい時は、update_attributeを使う。これにはバリデーションを回避するといった特徴もある。

user.update_attribute(:name, "El Duderino")
user.save

有効性を検証する

バリデーションとテスト駆動開発の相性は最高。
手順に関しては、まず有効なモデルのオブジェクトを作成し、その属性のうち1つを有効でない属性に変更する。そして、バリデーションが失敗するかどうかをテストする。念の為、最初に作成時の状態に対してもテストを書いておき、最初のモデルが有効かどうかも確認しておく。このようにすればバリデーションのテストが失敗した時、バリデーションの実装かオブジェクトか、責任の所在がはっきりする。

rails g modelの時に、User用テストの原型ができている。

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

有効なオブジェクトに対してテストを書くために、setupという特殊なメソッドを使って有効なUserオブジェクト(@user)を作成する。
setupメソッド内に書かれた処理は、各テストが走る直前に実行される。@userはインスタンス変数だが、setupメソッド内で宣言しておけば、全てのテスト内でこのインスタンス変数が使えるようになる。

# 有効なUserかを判断
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

ちなみに下記コードでmodelに対してのみテストを行える。

rails test:model

存在性を検証する

最も基本的なバリデーションは「存在性(Presence)」。これは単に、渡された属性が存在することを検証する。テストの方法は、まず属性に対して空白の文字列をセットし、assert_notメソッドを使ってUserオブジェクトが有効でなくなったことを確認する。

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 # ←ここ この時点でテストはred
    @user.name = "     "
    assert_not @user.valid?
  end
end

name属性の存在を検査する方法はvalidatesメソッドにpresence: trueを与える。これは要素を1つ持つオプションハッシュである。

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true # ←この文でテストはgreenに
end

結果として

>> user = User.new(name: "", email: "michael@example.com")
>> user.valid?
=> false

なぜ失敗したかerrorsオブジェクトを使って確認できる。

user.errors.full_message
=> ["Name can't be blank"]

emailの方も同じ処理を

長さを検証する

今回は名前を50文字まで、メールアドレスを255文字まで。
超過する文字を入れて有効性を確認する。

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 # ←こっから2つの文でテストはredに
    @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

長さに関するバリデーションの追加

app/models/user.rb

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 } # ←greenになる
  validates :email, presence: true, length: { maximum: 255 }
end

フォーマットを検証する

最初に有効なメールアドレスと無効なメールアドレスのコレクションに対するテストを行う。

>> %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メソッドを使ってadresses配列の各要素を繰り返し取り出す。

現在は空ではないメールアドレスなら全てパスするようになっている。

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 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
end

assertの第2引数にエラーメッセージを追加している。これにより、どのメールアドレスでテストが失敗したかがわかるようになる。ちなみに詳細な文字列を調べるために.inspectが使われている。

次に@がなかったり、.が,になっているアドレスなどの「無効性」についてもテストをする。

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 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

この時点でテストはred。
フォーマット検証のバリデーションオプションはformatである。

validates :email, format: { with: /<regular expression>/ }
↑正規表現を使っている

正規表現の説明に関して、かなり難しいので本文の第6章、真ん中あたりを都度確認。

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

正規表現は定数に保存。

一意性の検証

一意性を強制するために、validatesメソッドの:uniquenessオプションを使う。
今まではUser.newで、ただ単にメモリ上にオブジェクトを作っていただけ。しかし、一意性のテストにはオブジェクトをデータベースに登録する必要がある。まずは重複したメールアドレスからテストしていく。

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
    @user.save
    assert_not duplicate_user.valid?
  end
end

dupは同じ属性を持つデータを複製するためのメソッド。@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザーの作成は無効になるはず。

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

補足:通常メールアドレスでは大文字小文字が区別されない。したがってメールアドレスの検証ではこのような場合も考慮する必要がある。この性質のため、大文字と小文字を区別しないでテストすることが重要。

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 = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"

>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true

これはfalseにならないといけない。これはcase_sensitiveをfalseにすれば解決する。

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

テストはgreenになった。
しかしまだ問題が1つ残っている。それはActive Recordはデータベースのレベルでは一意性を保持していないこと。
高速で登録ボタンを2回押した時に、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまう。
この問題はデータベース上のemaikカラムにインデックスを追加することで、一意性を示し解決することができる。またインデックスは検索にも役立ち、インデックスがなければ全表スキャンしなければいけないところを、本の索引のようにパッと見つけることができる。

emailインデックスを追加するには、データモデリングが必要。Railsではマイグレーションでインデックスを追加する。

rails g migration add_index_to_users_email

ユーザー用のマイグレーションとことなり、メールアドレスの一意性のマイグレーションは未定義になっているので、定義を記述する必要がある。

db/migrate/[timestamp]_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email, unique: true
  end
end

add_indexを使う。また、インデックス自体は一意性を強制しないが、オプションでunique: trueを指定することで強制できるようになる。

rails db:migrate

この時点でのテストはred。rails g modelの時にユーザー用のfixtureが自動的に生成されているが、ここのメールアドレスが一意になっているのが問題なので全部消す。

text/fixtures/users.yml

#全部空

もう1つやらなければならないことがあり、それは一部のデータベースアダプタが、大文字小文字を区別するインデックスを常に使っているとは限らない問題への対処。Foo@ExAMPle.Comfoo@example.comを別々の文字列として解釈するデータベースがあるが、このアプリケーションではこれらを同一の文字列として扱わなければならない。
対抗策としては、データベースに保存される直前に、全ての文字列を小文字に変換するということ。これはActive Recordの**コールバック(callback)**で実装できる。
今回の場合は、オブジェクトが保存されるタイミングで処理を実行したいので、before_saveというコールバックを使う。

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: 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
    @user.save
    assert_not duplicate_user.valid?
  end
end

セキュアなパスワードを追加する

セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それをそのままではなく、ハッシュ化したものをデータベースに保存する。ハッシュ化とは、ハッシュ関数と呼ばれるものを使って、入力されたデータを不可逆データ(復元不可能なデータ)に変換する処理を行う。ハッシュ化されたパスワードは認証する時に使う。

ハッシュ化されたパスワード

セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけで、ほぼ完了。Userモデルで次のように呼び出せる。

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

このメソッドを追加すると、次のような機能が使えるようになる。
・セキュアにハッシュ化したパスワードを、データベース内のpassword_digest属性に保存できるようになる。
・2つの仮想的な属性(passwordとpassword_confirmation)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
・authenticateメソッドが使えるようになる(引数の文字列がパスワードと一致するとUserオブジェクトを返し、一致しない場合はfalseを返すメソッド)

このhas_secure_password機能を使うためには、モデル内にpassword_digestという属性が含まれていることが条件。そのために適切なマイグレーションを生成する。
ちなみにマイグレーション名は自由に指定できるが、末尾にto_usersをつけることがおすすめ。これをRailsが認識すると、usersテーブルにカラムを追加するマイグレーションが自動的に作成されるから。

rails g migration add_password_digest_to_users password_digest:string
db/migrate/[timestamp]_add_password_digest_to_users.rb

class AddPasswordDigestToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :password_digest, :string
  end
end

今回はadd_columnメソッドを使って、usersテーブルにpassword_digestカラムを追加する。

rails db:migrate

また、has_secure_passwordを使ってパスワードをハッシュ化するためには、bcryptライブラリが必要。

Gemfile

.
.
.
gem "rails",           "7.0.4"
gem "bcrypt",          "3.1.18"
gem "bootstrap-sass",  "3.4.1"
.
.
.
bundle _2.3.14_ install

ユーザーがセキュアなパスワードを持っている

ようやくUserモデルないで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: true
  has_secure_password # ←ここ!
end

この時点でテストはred。理由は、has_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も追加されているから。
テストをパスさせるにはパスワードとパスワード確認の値を追加する。

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

これでテストはgreenになる。

パスワードの最小文字数

最大文字数の時とほぼ同じ

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
  .
  .
  .
  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.password = @user.password_confirmation = "a" * 5

その後バリデーションを追加。

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: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 } # ←ここ!
end

この時点でテストはgreen

ユーザーの作成と認証

今回はRails consoleで作る

>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>             password: "foobar", password_confirmation: "foobar")

ハッシュ化の確認

>> user = User.find_by(email: "michael@example.com")
>> user.password_digest
=> "$2a$12$WgjER5ovLFjC2hmCItmbTe6nAXzT3bO66GiAQ83Ev03eVp32zyNYG"

また、authenticateメソッドも使えるようになっているかの確認。このメソッドは引数に渡された文字列(パスワード)をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較する。
間違った値の場合

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false

正しい場合

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 03:15:38", updated_at: "2022-03-11 03:15:38",
password_digest: [FILTERED]>

ユーザーオブジェクトを返す。
またオブジェクトを返すのではなく、返される値が論理値であることが重要なので、

!!user.authenticate("foobar")
=> true

強制的に論理値を返させる。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?