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.

Ruby on Rails チュートリアル第6章をやってみて

Last updated at Posted at 2022-02-13

#ユーザーのモデルを作成する
■第6章
第5章では、新しいユーザーを作成するためのスタブページを作ったところで終わった。本章では、一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について学んでいく。

Railsでは、本番アプリケーションに適した認証システムを自分で構築するアプローチも選択できる。筆者曰く、自分自身で認証システムを構築した経験があれば仕組みを理解し、必要に応じて変更することがずっと容易になるらしい。

##6.1 Userモデル
ユーザー登録でまず初めにやることは、それらの情報を保存するためのデータ構造の作成。
Railsは、データベースの細部をほぼ完全に隠蔽し、切り離してくれる。
本書ではSQLiteを開発環境で使い、また、PostgreSQLを(Herokuでの)本番環境で使う。

###6.1.1 データベースの移行
この節での目的は、簡単に消えることのないユーザーのモデルに構築。
Railsはデータを保存する際にデフォルトでリレーショナルデータベースを使う。リレーショナルデータベースは、データ行で構成されるテーブルからなり、各行はデータ属性のカラム(列)を持ちます。

$ rails generate model User name:string email:string

今回はnameemailといった属性を付けたUserモデルを使いたいので、このコマンドを打つ。name:stringemail:stringオプションのパラメータを渡すことによって、データベースで使いたい2つの属性をRailsに伝える。

*コントローラ名には複数形を使い、モデル名には単数形を用いるという慣習を頭に入れておく

今回のコマンドの結果で、マイグレーションと呼ばれる新しいファイルが生成される。

db/migrate/[timestamp]_create_users.rb

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

      t.timestamps
    end
  end
end

マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加された。複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、衝突を起こしていた。現在のタイムスタンプによる方法であれば、基本衝突は避けられる。

マイグレーションはchangeメソッドの集まり。このメソッドはcreate_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成する。

t.timestampsは特別なコマンドで、created_atupdated_atという2つの「マジックカラム(Magic Columns)」を作成します。これらは、あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプ。

  

rails db:migrate

このコマンドでマイグレーションの実行。development.sqlite3というファイルが生成される。これはSQLite5データベースの実体である。

DB Browser for SQLiteをダウンロードしてusersテーブルを確認するとこんな感じに。

スクリーンショット 2022-02-12 17.48.43.png

###6.1.2 modelファイル

ApplicationRecordはActiveRecord::Baseを継承している。

###6.1.3 ユーザーオブジェクトを作成する
データベースを変更したくない時は、コンソールをサンドボックスモードで起動。

$ rails console --sandbox

ここからはコンソールで作業。

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

引数なしで呼んだ場合は、すべての属性がnilのオブジェクトを返す。

userを作ってみる。

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

nameとemail属性がちゃんと設定された。

valid?メソッドでuserオブジェクトが有効か確認。

>> user.valid?
true

現時点ではデータベースに格納されていないので、saveメソッドを使って保存。

>> user.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (7.1ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", "2022-02-12 10:41:35.029896"], ["updated_at", "2022-02-12 10:41:35.029896"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

idが更新され、マジックカラムには現在の日時が代入されている。

>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2022-02-12 10:41:35", updated_at: "2022-02-12 10:41:35">

###6.1.4 ユーザーオブジェクトを検索する
データベースに保存されているかどうかを確認するために、User.findを使う。

User.find(1)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2022-02-12 10:41:35", updated_at: "2022-02-12 10:41:35">

User.findにユーザーのidを渡している。その結果、Active Recordはそのidのユーザーを返す。他にもいろんなメソッドがある。

###6.1.5 ユーザーオブジェクトを更新する
更新の方法は基本的に2つ。1つは属性を個別に代入する方法。

>> user           # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true

変更をデータベースに保存するために最後にsaveを実行する必要がある。

もう1つの方法はupdate_attributesを使うケース。

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

このメソッドは、属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行う。

##6.2 ユーザーを検証する
nameemailにあらゆる文字列を許すのは避けるべきなので、これらの属性値に、何らかの制約を与える。検証 (Validation) という機能を通して、こういった制約を課すことができるようになっている。

###6.2.1 有効性を検証する
まず有効なモデルのオブジェクトを作成し、その属性のうちの1つを有効でない属性に意図的に変更します。そして、バリデーションで失敗するかどうかをテストする、といった方針で進めていく。

###6.2.2 存在性を検証する
最も基本的なバリデーションは「存在性 (Presence)」。

user_test.rbname属性の存在性に関するテストを追記。

user.rbに、空白が入らないようにバリデーションを追加する。

class User < ApplicationRecord
 validates :name, presence: true
end

そうしたら、空白を弾くように。

$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false

user.errors.full_messagesで、なんでエラーになったのか見れる。

emailでも同様のことをやる。

###6.2.3 長さを検証する
これでUserモデル上に名前を持つことを強制されるようになった。名前の長さにも制限を与える。

名前は50文字まで、emailは255文字まで。

user_test.rbに追記。

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

user.rb長さのバリデーション追加。

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

###6.2.4 フォーマットを検証する
有効なメールアドレスかどうかの判定をする。

メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい。

user_test.rbで有効なメールフォーマットをテストする。

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

以下のコードでどのメールアドレスでテスト失敗したのか特定できるように。

assert @user.valid?, "#{valid_address.inspect} should be valid"

次は無効性についてテスト。フォーマット検証には、formatを使う。

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

メールフォーマットを正規表現で検証する。

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

正規表現VALID_EMAIL_REGEXは定数。大文字で始まる名前はRubyでは定数を意味する。残る制約は、メールアドレスが一意であることを強制することだけとなった。

###6.2.5 一意性を検証する
メールアドレスの一意性を強制するために (ユーザー名として使うために)、validatesメソッドの:uniqueオプションを使う。

一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要がある。

user_test.rbに重複するメールアドレス拒否のテスト。

  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end

@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしている。

大文字小文字を区別しない一意性のテストも必要。

duplicate_user.email = @user.email.upcase

このコードを上のコードに追加。

ここまでやったことは、あくまでもアプリケーションレベルで一意性を強制しただけであるため、素早く2回連続でクリックしたなどのデータベースレベルでは一意性を強制したことにならない。

emailインデックスを追加すると、データモデリングの変更が必要になる。
今回は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要がある。

$ rails generate migration add_index_to_users_email

メールアドレスの一意性のマイグレーションは未定義なので、[timestamp]_add_index_to_users_email.rbに次のように定義。

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

usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っている。

最後に、データベースをマイグレート。

$ rails db:migrate

データベースによって、メールアドレスの大文字と小文字を区別せずにバリデーションが効かないという問題がある。

これを、user.rbbefore_saveというコールバックで解決する。

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

##6.3 セキュアなパスワードを追加する
最後の砦である「セキュアなパスワード」に取り掛かる。セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それをハッシュ化したものをデータベースに保存する。

今回の「ハッシュ化」は、ハッシュ関数を使って、入力されたデータを不可逆なデータにする処理を指す。

ハッシュ化することで、仮にデータベースの内容が盗まれたり覗き見されるようなことがあっても、パスワードの安全性が保たれる。

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

セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけでほぼ終わるっぽい。

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

このメソッドを追加すると、次のような機能が使えるようになる。

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

has_secure_password機能を使えるようにするには、モデル内にpassword_digestという属性が含まれていることが条件となる。

今回はUserモデルにそれを追加する。そのために以下のコマンドを実行。

$ rails generate migration add_password_digest_to_users password_digest:string

上のコマンドではpassword_digest:stringという引数を与えて、今回必要になる属性名と型情報を渡している。

また、has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になる。bcrypt gemをGemfileに追加。

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

$ rails test

を実行してもREDに。

セットアップメソッドでいろいろと情報が足りてないらしい。
user_test.rbにパスワードとパスワード確認を追加。

 def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

これでテストはGREENに。

###6.3.3 パスワードの最小文字数
今回は簡潔にパスワードが空でないことと最小文字数 (6文字) の2つを設定する。

パスワードの長さが6文字以上であることを検証するテスト。

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

また、空のパスワードを入力させないために、存在性のバリデーションも追加。

###6.3.4 ユーザーの作成と認証
以上でUserモデルの基本部分が完了。次章でユーザー情報表示ページを作成するときに備えて、データベースに新規ユーザーを1人作成しておく。

今度はrails consoleでサンドボックスでないパターン。

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

うまくデータベースに保存されていました。

スクリーンショット 2022-02-13 16.16.05.png

感想

この章から難易度がまた一段と上がった気がします。たまにエラーも出ましたが無事解決できました。
半年前くらいにインターンを始めようとしたときに「バックエンドとフロントエンド、どっちに興味があるの?」と聞かれた時、何もわかんなかったのが、この章で、バックエンドの人ってこんな感じのことやってんだろうなーとなんとなくですがわかった気がします。

疑問・単語集

*rails testを実行しようとしたら

Migrations are pending. To resolve this issue, run:
        bin/rails db:migrate RAILS_ENV=test

とエラーになってしまいました。以下のコードを実行したらうまくいきました。

$RAILS_ENV=test rails db:drop
$RAILS_ENV=test rails db:create
$RAILS_ENV=test 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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?