LoginSignup
11
12

More than 5 years have passed since last update.

Rails Tutorialsで勉強のメモ その6(ユーザーモデルの作成)

Posted at

やること

  • サイトのユーザー用のデータモデルの作成
  • データを保存する手段の確保
ブランチを切る
$ git checkout master
$ git checkout -b modeling-users

Userモデル

  • モデル
    • Railsの場合、データモデルで使用するデフォルトのデータ構造をさす
  • データを永続化する方法
    • データベースを使用してデータを長期間保存
    • データベースとやり取りするRailsライブラリはデフォルトでActive Recordという
  • Active Record
    • データオブジェクトの作成/保存/検索のためのメソッドを保持している
    • 上記メソッドを使用する際にSQLを意識する必要はない
  • マイグレーション
    • データの定義をRubyで記述
      • DDLを使う必要がない

データベースの移行

  • generate model
    • モデルを作成するコマンド
    • モデル名には単数形を用いる
      • コントローラ名は複数形
Userモデルの作成
$ rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/20140814160532_create_user.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
  • マイグレーション
    • データベースの構造をインクリメンタルに変更する手段を提供
    • 要求が変更された場合にデータモデルを適合させることができる
    • 上記の例の場合、db/migrate/20140814160532_create_users.rbにあたる
      • タイムスタンプを名前の中につけることで、複数開発者が生成しても競合しないようにしている
db/migrate/20140814160532_create_user.db:Userモデルのマイグレーション
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :user do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
  • マイグレーション

    • データベースに与える変更を定義したchangeメソッドの集合
      • create_tableメソッドを呼び、ユーザーを保持するためのテーブルをデータベースに作成する
        • モデルは単数形(user)だが、テーブル名は複数形(users
      • ブロックの中で、nameemailというカラムをデータベースに作成
      • t.timestampcreated_atupdated_atというカラムを作成
        • レコード作成/更新時に自動的に更新されるカラム
  • ユーザーデータモデル

    • idは各行を一意に識別するためのカラムで自動生成される
カラム名
id integer
name string
email string
created_at datetime
updated_at datetime
  • マイグレーションの実行
    • bundle exec rake db:migrate
    • マイグレーションの適用(migrating up)
    • 内部的にcreate_tableを呼び出している
    • db/development.sqlite3というDDLのファイルが作成される
      • SQLite Database Browserでデータベースの構造を参照できる
  • マイグレーションの取り消し
    • bundle exec rake db:rollback
    • マイグレーションの取り消し(migrating down)
    • 内部的にdrop_tableを呼び出している
マイグレーションの適用
$ bundle exec rake db:migrate
== 20140814160532 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0077s
== 20140814160532 CreateUsers: migrated (0.0079s) =============================

modelファイル

  • Userモデル
    • ActiveRecord::Baseの全ての機能を持つ
app/models/user.rb:Userモデル
class User < ActiveRecord::Base
end

ユーザーオブジェクトの作成

  • rails consoleのサンドボックスモード
    • コンソール上でDBの変更を行ってもロールバックされる
Userオブジェクトの操作:作成・保存と削除
$ rails c --sandbox
Loading development environment in sandbox (Rails 4.0.4)
Any modifications you make will be rolled back on exit

# Userオブジェクトの作成
> User.new

# Userオブジェクトを初期化してuser変数に代入
> user = User.new(name: "Michael Hartl", email: "mhart@example.com")

# user変数をDBに保存
> user.save

# user変数の属性を確認
> user.name
 => "Michael Hartl"
> user.email
 => "mhart@example.com"
> user.updated_at
 => Sat, 23 Aug 2014 13:24:40 UTC +00:00

# Userオブジェクトの作成と同時に保存
> User.create(name: "A Nother", email: "another@example.org")
   (0.2ms)  SAVEPOINT active_record_1
  SQL (0.9ms)  INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Sat, 23 Aug 2014 14:09:14 UTC +00:00], ["email", "another@example.org"], ["name", "A Nother"], ["updated_at", Sat, 23 Aug 2014 14:09:14 UTC +00:00]]
   (0.4ms)  RELEASE SAVEPOINT active_record_1
 => #<User id: 4, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:09:14", updated_at: "2014-08-23 14:09:14">

# userの破棄
> user = user.destroy
   (0.4ms)  SAVEPOINT active_record_1
  SQL (0.6ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 3]]
   (0.8ms)  RELEASE SAVEPOINT active_record_1
 => #<User id: 3, name: "Michael Hartl", email: "mhart@example.com", created_at: "2014-08-23 13:24:40", updated_at: "2014-08-23 13:24:40">

ユーザーオブジェクトの検索

Userオブジェクトの検索
# idで検索
> User.find(5)
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 5]]
 => #<User id: 5, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:30:53", updated_at: "2014-08-23 14:30:53">

# Emailで検索
> User.find_by_email("another@example.org")
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."email" = 'another@example.org' LIMIT 1
 => #<User id: 4, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:09:14", updated_at: "2014-08-23 14:09:14">

# 上記よりもハッシュで指定するfind_byメソッドを使うべき
> User.find_by(email: "another@example.org")
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."email" = 'another@example.org' LIMIT 1
 => #<User id: 4, name: "A Nother", email: "another@example.org", created_at: "2014-08-23 14:09:14", updated_at: "2014-08-23 14:09:14">

# 最初の1件目を取得
> User.first
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
 => #<User id: 2, name: "テストユーザー", email: "admin@localhost", created_at: "2014-08-02 06:36:09", updated_at: "2014-08-02 06:36:09">

# 全てのデータを返す
> User.all

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

  • オブジェクトの更新方法
    • 属性を個別に代入
      • saveメソッド
    • 属性の更新と保存を同時に行う
      • update_attributesメソッド
属性の更新
# userの登録
> user = User.create(name: "Michael Hartl", email: "mhart@exampl
e.net")
   (0.2ms)  SAVEPOINT active_record_1
  SQL (0.6ms)  INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Sat, 23 Aug 2014 14:39:04 UTC +00:00], ["email", "mhart@example.net"], ["name", "Michael Hartl"], ["updated_at", Sat, 23 Aug 2014 14:39:04 UTC +00:00]]
   (1.4ms)  RELEASE SAVEPOINT active_record_1
 => #<User id: 7, name: "Michael Hartl", email: "mhart@example.net", created_at: "2014-08-23 14:39:04", updated_at: "2014-08-23 14:39:04">

# 属性の変更
> user.email = "mhartl@example.com"
 => "mhartl@example.com"

# 属性の更新
> user.save
   (0.2ms)  SAVEPOINT active_record_1
  SQL (0.8ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = 7  [["email", "mhartl@example.com"], ["updated_at", Sat, 23 Aug 2014 14:39:30 UTC +00:00]]
   (0.2ms)  RELEASE SAVEPOINT active_record_1
 => true

# 属性の更新+保存
> user.update_attributes(name: "The Dude", email: "dude@abides.org")
   (0.2ms)  SAVEPOINT active_record_1
  SQL (1.0ms)  UPDATE "users" SET "name" = ?, "email" = ?, "updated_at" = ? WHERE "users"."id" = 7  [["name", "The Dude"], ["email", "dude@abides.org"], ["updated_at", Sat, 23 Aug 2014 14:43:01 UTC +00:00]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
 => true

ユーザの検証

  • Active Recordの検証(バリデーション)
    • 存在の検証(presence)
    • 長さの検証(length)
    • フォーマットの検証(format)
    • 一意性の検証(uniqueness)

最初のユーザテスト

  • 初期spec
    • pendingメソッドを呼び出して、黄色い文字を出力するだけ
初期spec
require 'spec_helper'

describe User do
  pending "add some examples to (or delete) #{__FILE__}"
end
テストの実行
# DBの作成
$ bundle exec rake db:migrate

# 開発データベースのデータモデルdb/development.sqlite3がテストデータベースdb/test.sqlite3に反映されるようにするもの
$ bundle exec rake test:prepare

# テストを実行
$ bundle exec rspec spec/models/user_spec.rb
spec/models/user_sprc.rbにテストケースを追加
require 'spec_helper'

describe User do
  before { @user = User.new(name: "Example User", email: "user@example.com") }

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
end
上記は下記と同義
it "should respond to 'name'" do
  expect(@user).to respond_to(:name)
end
  • respond_toメソッド
    • Rubyのrespond_to?メソッドを呼び出す
    • そのシンボルが表すメソッドまたは属性が存在しているかどうかを確認
テストを実施:成功
$ bundle exec rspec spec/
respond_to?の動作確認
$ rails c --sandbox

> user = User.new
> user.respond_to?(:name)
 => true
> user.respond_to?(:foo)
 => false

プレゼンスの検証

  • 属性の存在を検証
    • validatesメソッドにpresence: true`という引数を与える
app/models/user.rb
class User < ActiveRecord::Base
  validates :name, presence: true
end
presenceの検証
$ rails c --sandbox

# name属性を未指定で作成
> user = User.new(name: "", email: "mhartl@example.com")

# 保存しようとするとfalseが返却される
> user.save
 => false

# 検証してもfalseが返却される
> user.valid?
 => false

# エラーメッセージの確認
> user.errors.full_messages
 => ["Name can't be blank"]
  • テストケースの追加
    • validがtrueであること
    • name、email属性がブランクの時はvalid?メソッドがfalseであること
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  before { @user = User.new(name: "Example User", email: "user@example.com") }

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }

  it { should be_valid }

  describe "when name is not present" do
    before { @user.name = " " }
    it { should_not be_valid }
  end

  describe "when email is not present" do
    before { @user.email = " " }
    it { should_not be_valid }
  end
end
rspecの実行:emailのpresenceがないのでエラー
$ bundle exec rspec
.......F...........

Failures:

  1) User when email is not present
     Failure/Error: it { should_not be_valid }
       expected #<User id: nil, name: "Example User", email: " ", created_at: nil, updated_at: nil> not to be valid
app/models/user.rbにemailのpresence設定を追加
class User < ActiveRecord::Base
  validates :name, presence: true
  validates :email, presence: true
end
テスト成功
$ bundle exec rspec

長さの検証

spec/models/user_spec.rbに長さのテストケースを追加
  describe "when name is too long" do
    before { @user.name = "a" * 51 }
    it { should_not be_valid }
  end
テストを実施して失敗
$ bundle exec rspec
  • 長さの検証の設定
    • length: { maximum: <最大文字数> }
app/models/user.rbに長さの設定を追加
class User < ActiveRecord::Base
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true
end

フォーマットの検証

spec/models/user_spec.rbにemailのフォーマット検証ロジックを追加
  describe "when email format is invalid" do
    it "should be invalid" do
      addresses = %w[
        user@foo,com
        user_at_foo.org
        example.user@foo.
        foo@bar_baz.com
        foo@bar+baz.com
      ]
      addresses.each do |invalid_address|
        @user.email = invalid_address
        expect(@user).not_to be_valid
      end
    end
  end

  describe "when email format is valid" do
    it "should be valid" do
      addresses = %w[
        user@foo.COM
        A_US-ER@f.b.org
        frst.lst@foo.jp
        a+b@baz.cn
      ]
      addresses.each do |valid_address|
        @user.email = valid_address
        expect(@user).to be_valid
      end
    end
  end
  • フォーマットの検証
    • format: { with: <フォーマット> }
    • \Aは文字列の先頭を表す
    • \zは文字列の末尾を表す
app/models/user.rbにemailのフォーマット検証を追加
class User < ActiveRecord::Base
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end

一意性の検証

  • @user.dup
    • 同じユーザを複製
    • 既に登録済みのユーザを登録しようとしてエラーになることを期待
spec/models/user_spec.rbに一意性検証のテストコードを追加
  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.save
    end

    it { should_not be_valid }
  end
  • 一意性の検証
    • uniqueness: trueを設定
app/models/user.rbに一意性の検証を追加
class User < ActiveRecord::Base
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end
一意性の検証で、現時点では大文字小文字を区別する
$ rails c --sandbox

> user = User.create(name: "Example User", email: "user@example.com")
> user_with_same_email = user.dup
# メールアドレスを大文字に変換してバリデーションチェック → 一意性がtrueとみなされている
> user_with_same_email.email = user.email.upcase
> user_with_same_email.valid?
 => true
  • 大文字小文字を区別しないためには
    • :case_sensitiveを設定
spec/models/user_spec.rbに一意性検証のテストコードを追加
  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.email = @user.email.upcase
      user_with_same_email.save
    end

    it { should_not be_valid }
  end
app/models/user.rbに大文字小文字を区別しない設定を追加
class User < ActiveRecord::Base
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end
  • DB側の一意性制約
    • emailカラムにDBのインデックスを作成し、そのインデックスが一意であることを要求
    • rails generate migrationで作成
    • add_index :<テーブル名>, :<カラム名>, unique: true
インデックスの作成
$ rails generate migration add_index_to_users_email
      invoke  active_record
      create    db/migrate/20140824083152_add_index_to_users_email.rb
db/migrate/20140824083152_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end
データベースのマイグレート
$ cat db/schema.rb
ActiveRecord::Schema.define(version: 20140814160532) do

  create_table "users", force: true do |t|
    t.string   "name"
    t.string   "email"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

end

$ bundle exec rake db:migrate

$ cat db/schema.rb
ActiveRecord::Schema.define(version: 20140824083152) do

  create_table "users", force: true do |t|
    t.string   "name"
    t.string   "email"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "users", ["email"], name: "index_users_on_email", unique: true

end
  • メールアドレス保存時の考慮事項
    • メールアドレス保存時に全ての文字を小文字にする
      • DBのアダプタが常に大文字小文字を区別するインデックスを使用しているとは限らないため
    • コールバック機能を使用
  • コールバック
    • Active Recordオブジェクトが持続している間のどこかの時点で、Active Recordオブジェクトに呼び出してもらうメソッド
    • 今回はbefore_saveコールバックを使用
app/models/user.rbに、メールアドレス保存時に小文字に変換するロジックを追加
class User < ActiveRecord::Base
  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, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

セキュアなパスワードの追加

  • ユーザの認証
    • パスワードの送信
    • 暗号化
    • DB内の暗号化された値との比較
      • 暗号化された値同士を比較する
  • セキュアなパスワードの実装方法
    • has_secure_passwordメソッドを使用
    • ハッシュ関数にはbcryptを使用して、不可逆的に暗号化してパスワードハッシュを作成

暗号化されたパスワード

  • ユーザのデータモデル
    • password_digestカラムを追加
カラム名
id integer
name string
email string
password_digest string
created_at datetime
updated_at datetime
Gemfileにbcrypt用のgemを追加
gem 'bcrypt-ruby', '3.1.2'
gemのインストール
$ bundle install
spec/models/user_spec.rbにpassword_digestカラムがあることのテストケースを追加
   it { should respond_to(:password_digest) }
  • マイグレーション名
    • 末尾に_to_usersとつけると、usersテーブルにカラムを追加するマイグレーションがRailsによって自動作成される
    • 第2引数でカラム名と型を指定
password_digest用のマイグレーションを作成
$ rails generate migration add_password_digest_to_users password_digest:string
      invoke  active_record
      create    db/migrate/20140824101047_add_password_digest_to_users.rb

$ cat db/migrate/20140824101047_add_password
_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
  def change
    add_column :users, :password_digest, :string
  end
end
開発DBをマイグレーションしてテストDBを準備し、テストを実施
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
$ bundle exec rspec spec/

パスワードと確認

  • パスワードの確認

    • モデルの中でActive Recordを使用して制限を与えるのが慣習
    • password属性とpassword_confirmation属性をUserモデルに追加し、レコードをDBに保存する前に2つの属性が一致するように要求する
      • これらの属性は一時的にメモリ上に置き、DBには保存されないようにする
      • これらの属性はhas_secure_passwordで自動的に実装される
  • テストコードの追加/修正

    • @userの初期化時にpassword属性とpassword_confirmation属性も初期化する
    • passwordpassword_confirmationカラムの存在チェック
      • passwordpassword_confirmationはDB上に作成されるわけではない(仮想的な属性)
    • passwordの存在確認テスト
    • passwordpassword_confirmationの不一致テスト
spec/models/user_spec.rb
  before do
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

  it { should respond_to(:password) }
  it { should respond_to(:password_confirmation) }

  describe "when password is not present" do
    before do
      @user = User.new(name: "Example User", email: "user@example.com",
                       password: " ", password_confirmation: " ")
    end
    it { should_not be_valid }
  end

  describe "when password doesn't match confirmation" do
    before { @user.password_confirmation = "mismatch" }
    it { should_not be_valid }
  end
app/models/user.rbにhas_secure_passwordを追加
class User < ActiveRecord::Base
  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, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
end
  • has_secure_passwordを追加するだけでテストコードがパスできる
    • テストがOKになることを確認したら、上記のhas_secure_passwordをコメントアウトしておく
テストコードの実行
$ bundle exec rspec spec/

ユーザー認証

  • ユーザをメールアドレスとパスワードに基づいて取得する手段

    1. ユーザをメールアドレスで検索
      • `user = User.find_by(email: email)
    2. 受け取ったパスワードでユーザを認証
      • current_user = user.authenticate(password)
  • 認証に関するテストケース

    • authenticateが応答することの確認
    • パスワードが一致する場合と一致しない場合についてテストケースを記載
    • パスワードの長さの確認
spec/models/user_spec.rbに上記テストケースを追加
  it { should respond_to(:authenticate) }

  describe "with a password that's too short" do
    before { @user.password = @user.password_confirmation = "a" * 5 }
    it { should be_invalid }
  end

  describe "return value of authenticate method" do
    before { @user.save }
    let(:found_user) { User.find_by(email: @user.email) }

    describe "with valid password" do
      let(:user_for_invalid_password) { found_user.authenticate("invalid") }

      it { should_not eq user_for_invalid_password }
      specify { expect(user_for_invalid_password).to be_false }
    end
  end
  • letメソッド
    • テスト内でローカル変数を作成
    • let(:found_user) { User.find_by(email: @user.email) }の場合
      • found_userという変数を作成し、ブロック内の戻り値が代入される
  • specifyメソッド
    • itと同義
    • itを使用すると英語として不自然な場合に使用
      • ×:user: user with invalid password should be false
      • ○:specify: user with invalid password should be false
  • eq演算子
    • オブジェクト同士が同値であるかどうかを確認

ユーザモデルのパスワード認証の実装

  • パスワードの長さ検証

    • `length: { minimum: 6 }
  • passwordpassword_confirmationを追加

  • パスワードが存在することの確認

  • パスワードとパスワードの確認が一致することの確認

  • password_digestを比較してユーザを認証

app/models/user.rbにパスワード関連の設定を追加
class User < ActiveRecord::Base
  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,
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, length: { minimum: 6 }
end

ユーザの作成

ユーザデータを作成
$ rails c

# ユーザの作成
> User.create(name: "Michael Hartl", email: "mhartl@example.com", password: "foobar", password_confirmation: "foobar")

# パスワードの確認
> user = User.find_by(email: "mhartl@example.com")
> user.password_digest
 => "$2a$10$GEZwKUtE7ZqAkgsBhHmIrueLFPZhECEauTC2FqbIGsWg8rzGr3Mwq"

# authenticateメソッドの確認
# パスワードが誤っている場合はfalse、正しい場合はユーザオブジェクトを返す
> user.authenticate("invalid")
 => false
> user.authenticate("foobar")
 => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2014-08-24 15:11:15", updated_at: "2014-08-24 15:11:15", password_digest: "$2a$10$GEZwKUtE7ZqAkgsBhHmIrueLFPZhECEauTC2FqbIGsWg...">

変更内容のコミット

変更内容のコミットとmasterへのマージ
$ git branch
  filling-in-layout
  master
* modeling-users
  static-pages

$ git add .
$ git commit -m "Make a basic User model (including secure passwords)"

$ git checkout master
$ git merge modeling-users
11
12
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
11
12