LoginSignup
0
0

Railsチュートリアル6章学習まとめ

Posted at

6.1Userモデル

Ruby on Railsでは、モデル(Model)とはデータモデルを指し、これはMVC(Model-View-Controller)の「M」です。Railsはデータの永続化にデータベースを使用し、データベースとのやりとりはActive Recordというライブラリを通じて行われます。Active Recordを使用すると、データの作成、保存、検索が可能で、これを行う際にSQL(Structured Query Language)を意識する必要はありません

さらにマイグレーションという機能がある為SQLについて学ぶ必要はないんですね。

とはいっても今までチュートリアルの言われるがままにマイグレーションをやってきました。
マイグレーションって一体なんなのか、咀嚼していきたいと思います。

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

ユーザークラスと同様に、nameとemailの2つの属性を持つユーザーをモデリングするところから始めるとのこと。

class User
  attr_accessor :name, :email
  .
  .
  .
end

Railsでユーザーをモデリングする際、属性を明示的に識別する必要はありません。Railsはデフォルトでリレーショナルデータベースをデータ保存に使用します。
とはいってもどういうことなのかよくわかりません。
更に見ていきます。

リレーショナルデータベース

テーブルはデータ行で構成され、各行はデータ属性のカラム(列)を持ちます。例として、nameとemailのカラムを持つusersテーブルを作成し、各行は1人のユーザーを表します。具体的なデータと対応するデータモデルは下記

スクリーンショット 2024-04-21 10.58.31.png
スクリーンショット 2024-04-21 10.58.35.png

なるほど?なんとなくわかりました。

つづいてUserモデルを作成します

$ rails generate model User name:string email:string

Userモデルを作成します、nameとemailにそれぞれ値を設定します。

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

できました。

rails db:migrateをマイグレーションの適用(migrating up)と呼ぶ。

6.2 ユーザーを検証する

6.1で作成したUserモデルに、アクセス可能なnameとemail属性が与えたが、現在どんな値でも渡す
事が可能になっている。現在は空欄でさえ渡す事が可能な状態です。

これは問題がありますね。

この辺りを例えば実践でやっていくとなるとあらかじめ要件定義が必要になってくると思います。
(経験者の方は経験則でパパっとやっちゃえそうですが)
例えば
・半角のみにする
・パスワードは英数字記号を混ぜた8文字以上にする
・空欄は不可

等が私でもパッと思いつきます。

さて本編に戻ります。

Active Recordでは検証(Validation)を通して、こういった制約を課すことができるようになっている。

ということはActive Recordを通じてemailname等を設定するという話ですね
ではよく使われるケースの記述説明をchatGPTで捕捉していきます。
よく使われるケース
存在性(presence)の検証
この検証は、特定のフィールドが空でないことを確認するために使用されます。たとえば、ユーザーの名前やメールアドレスが必須である場合、これらのフィールドに対して存在性の検証を設定します。これは、validates :name, presence: true のように指定して行います。
長さ(length)の検証
長さの検証は、文字列が指定された長さの範囲内にあることを保証するために使用されます。例えば、パスワードが最少8文字以上、最大20文字以下でなければならない場合、validates :password, length: { in: 8..20 } のように設定します。
フォーマット(format)の検証
フォーマットの検証は、フィールドのデータが指定された正規表現に一致することを保証します。例えば、メールアドレスが適切なフォーマットに従っているかを確認するために、validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } のように使用されます。
一意性(uniqueness)の検証
一意性の検証は、指定されたフィールドの値がデータベース内で重複していないことを確認します。例えば、ユーザー名が他のユーザーと重複しないようにするために、validates :username, uniqueness: true を使います。
(6.3.2では、よく使われる最終検証として確認(confirmation)も追加)
確認の検証は、二つのフィールドが一致することを確認するために使われます。通常、パスワード確認などに用いられます。ユーザーが提供したパスワードとパスワード確認フィールドが同じ値かどうかを検証するために、validates :password, confirmation: true と設定します

有効性を検証する

具体的なテスト方法
1.有効なモデルオブジェクトを作成する
2.意図的に失敗するデータを記述する
3.テストする(失敗を確認)
4.修正する
5.テストする(成功を確認)
→最初に書いてあったコードの状態でもテストを実行する。

こんな感じですね。
テストをする際の注意ですが、デバッグやってたときあらゆるパターンを考えて
リストを作成して自身でチェックシートを潰すようにやってました。

こういう時も記録化しておいたほうが後々のことも考えるといいと思います。

rails generate model User name:string email:string
ではこちらを実行したあとにできたファイルを見ていきます。

test/models/user_test.rb
require "test_helper"

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

setupメソッドは各テストの前に実行され、
この方法を通じてvalid?メソッドで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

ではこのテストケースのコードをリーディングしてchatGPTに聞いてみました。
私の解釈は下記です。

def setupで
@userの変数を定義しています。
そしてUser.newで新しいユーザーを作成しています。
(name :”Example User","email:"user@example.com")でUser.newの中身を定義しています。
その後テストコードのバリデーションを実行するといっており
ブロックでassertメソッドで@userのバリデーションが正常化どうかテストをしています。

chatGPTにあってるか確認したとこ非常にいい理解をしているといわれたので一安心しました。

話が脱線しましたがrails test:modelsを使用しtest modelのtestは正常でした。

6.2.2 存在性を検証する

最も基本的なバリデーションは存在性の検証とのこと。ここではユーザーモデルに保存される前に
nameとemailフィールドの両方あるかどうか確認するのを保証するとのことでした。

とはいっても何いってるのかよくわからないのでとりあえずコードを見ていきます。

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
$ rails test:models

この状態では@user.nameが空欄なので失敗します。

name属性の存在を検査する方法は、リスト 6.9に示したとおり、validates メソッドにpresence: trueという引数を与えて使うとのこと。

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

validatesにnameやpresence: trueを渡すことによってtestデータも含めて定義したことになり
空欄のエラーは解消されテストが成功されるってことですね。

また複数のバリデーションがある際はrails console
下記コマンドを実行しチェックするのが良さそうです。

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

これはnameが空なので失敗してるということになります。

$ rails test

これでテストは成功。

6.2.3長さを検証する

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

これは文字列の長さが適正なのかどうかチェックするコードですね。
基本的にlengthのコードで文字数制御が行われているようです。

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

ではその制御ですがClass UserにApplicationRecordを用いて
length:{ maximum :xx }で制御していくとのこと。

6.2.4フォーマットを検証する

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

文字列の配列をつくる際に便利なコマンドが%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配列の各要素を繰り返し取り出しテスト準備が完了した。

メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい部分なので、有効なメールアドレスと無効なメールアドレスをいくつか用意して、バリデーション内のエラーを検出する

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 @user.valid?, "#{valid_address.inspect} should be valid"
どのメールアドレスで失敗したのかはこちらの上記でわかるようです。

メールアドレスのフォーマットを検証するためには、次のようにformatというオプションを使います。
とのこと。

validates :email, format: { with: /<regular expression>/ }

このオプションは引数に正規表現(Regular Expression)(regexとも呼ばれます)を取る。

そこでメールアドレスにありがちな間違いをピックアップし
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
で表記する

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

一意性を検証する

ユーザー名として使うメールアドレスの一意性を強制するために、validatesメソッドの:uniquenessオプションを使います。ただしここで重大な警告があるとのこと。

よくわかりませんが見ていきます。

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

このコードは@user.dupでメールアドレスを重複し

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で
テストを重複しないようにチェックしている。

続いて、ダブルクリックしたりするとユーザーができてしまう対応について
rails generate 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

emailにindexを追加しマイグレーションしていく
$ rails db:migrate

test/fixtures/users.yml
# 空にする (既存のコードは削除する)

続いてサンプルを削除する

さらに、データベースに保存される直前にすべての文字列を小文字に変換する」作業を行う。
例えば"Foo@ExAMPle.CoM"という文字列が渡されたら、保存する直前に"foo@example.com"に変換をする。

これは、Active Recordのコールバック(callback)メソッドで実装することにします。コールバックメソッドは、Active Recordオブジェクトが存在する間の特定の時点で呼び出されるとのこと。

とりあえずごちゃ混ぜの文字列がきたらコールバックメソッドを使って変換する作業を行うってことですね。

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

`before_saveコールバックにブロックを渡し、downcaseという文字列メソッドでメールアドレスを小文字に変換

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

元に戻す、先ほどのbefore_saveで設定したことにより元に戻してOKになったと考えます。

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

セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それをそのままではなくハッシュ化したものをデータベースに保存するとのこと。

ハッシュ化=復元不可なデータにする。
生のパスワードをデータベースに保存してたら万が一もあるから突破されてもわからんように
しておきましょうねということですね。

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

ここでもApplication Recordを使ってhas_secure_passwordで設定するとのこと。

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

パッと見た感じですが、保存と仮想属性のオプションが増え存在確認ができる、更に認証作業ができるといった感じですかね?とても便利です。

このチート的なhas_secure_password機能存在を利用するには1つの条件がいる
モデル内にpassword_digestという属性が含まれていることです

つまりモデル内にpassword_digestという定義を作成していたら使用できるってことですね。

そこからマイグレーションファイルを作成します。

$ rails generate 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

usersに渡す為に to usersとつけて :password_digest, :stringと設定している感じですね。
結構難しいですが、この辺は慣れですね...。

更にこれを利用するにはbcryptライブラリが必要とのことなのでインストールをするようです。

gem "bcrypt", "3.1.18"
gem fileに記述し
bundle installを実行

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

has_secure_passwordを追加しました。
testをかけても現時点では失敗です。

失敗理由は下記のようです。
テストが失敗する理由は、6.3.1で触れたようにhas_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加されているからです。@user 変数にこのような属性がセットされていません。

つまり@usersにはhas_secure_passwordのバリデーション設定がついてないからエラーがでてるってことですね。

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

なので当初の前提条件としてsetupしているところに設定を追加します。

これで成功です。めでたしめでたし。

本章まとめ(引用)

・マイグレーションを使うと、アプリケーションのデータモデルを修正できる
・Active Recordを使うと、データモデルの作成や操作を行う多数のメソッドが使えるようになる
・Active Recordのバリデーションを使うと、モデルに対して制約を追加できる
・よくあるバリデーションには、存在性・長さ・フォーマットなどがある
・正規表現は謎めいて見えるが非常に強力である
・データベースにインデックスを追加すると検索の効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
・has_secure_passwordメソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる

感想

結構難しくなってきて理解が追いつかないところが多かったです。
値の設定の仕方やhas_secure_passwordなど重要な箇所も多いので
ここも改めて重点的に抑えておくポイントですね。
あとはテストの実装にも注意しながらやっていきたいです。

以上

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