LoginSignup
73
74

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-17

Rails Tutorial「第6章 ユーザーのモデルを作成する」の内容になります

Userモデル

  • Railsでは、データモデルとして扱うデフォルトのデータ構造のことをモデル (Model) と呼ぶ
  • データベースとやりとりをするデフォルトのRailsライブラリはActive Recordと呼ぶ
  • Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っている
  • これらのメソッドを使うのに、リレーショナルデータベースで使うSQLを意識する必要がない
  • Railsにはマイグレーションという機能があり、データの定義をRubyで記述することができのでSQLのDDLを新たに学ぶ必要がありません

Userモデルを生成する

  • コントローラ名には複数形を使い、モデル名には単数形を用いる(コントローラはUsersでモデルはUser)
  • 属性の型情報も一緒に渡す。
$ rails generate model User name:string email:string
カラム
id integer
name string
email string

マイグレーションファイル

generateコマンドの結果のひとつとしてマイグレーションと呼ばれる新しいファイルが生成される

db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
  • マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まり
  • changeメソッドはcreate_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成
  • モデル名は単数形 (User) ですが、テーブル名は複数形 (users)
  • .timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム (Magic Columns)」を作成(あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプ)
カラム
id integer
name string
email string
created_at datetime
updated_at datetime

マイグレーションの適用

$ rails db:migrate

マイグレーション適用でdevelopment.sqlite3が更新される。
また、app/models/user.rbが作成される

マイグレーションを元に戻す

$ rails db:rollback
  • drop_tableが適用されるのでdb/schema.rbが空になる
  • あるカラムを削除する場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する

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

Railsコンソールでモデルの操作が可能

$ rails console --sandbox

sandboxをつけることで「データベースへの変更をコンソールの終了時にすべて “ロールバック” (取り消し) する」設定になる

User.new

User.newで新しいオブジェクトを生成

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

userオブジェクトが有効かどうかを確認

>> user.valid?
true

User.save

現時点ではまだデータベースにデータは格納されていません。
つまり、User.newはメモリ上でオブジェクトを作成しただけです。

データベースにUserオブジェクトを保存するためには、userオブジェクトからsaveメソッドを呼び出す必要があります。

>> user.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.8ms)  INSERT INTO "users" ("name", "email", "created_at",
  "updated_at") VALUES (?, ?, ?, ?)  [["name", "Michael Hartl"],
  ["email", "mhartl@example.com"], ["created_at", 2016-05-23 19:05:58 UTC],
  ["updated_at", 2016-05-23 19:05:58 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

saveメソッドは、成功すればtrueを、失敗すればfalseを返す。

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

saveメソッド後、idcreated_atupdated_atに値が入る。

User.create

Active RecordではUser.createでモデルの生成と保存を同時におこなう方法も提供されている

>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2016-05-23 19:18:46", updated_at: "2016-05-23 19:18:46">

User.createは、trueかfalseを返す代わりに、ユーザーオブジェクト自身を返す

User.destroy

>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">

>> foo.destroy
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 3]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
  • createと同じようにdestroyはそのオブジェクト自身を返しますが、その戻り値を使ってもう一度destroyを呼ぶことはできません。
  • 削除されたオブジェクトはまだメモリ上には残っている(>> foo)

検索

.find

>> User.find(1)
=> #<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">

.find_by

>> User.find_by(email: "mhartl@example.com")
=> #<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">

.first

>> User.first
=> #<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">

.all

>> User.all
=> #<ActiveRecord::Relation [#<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 id: 2, name: "A Nother",
email: "another@example.org", created_at: "2016-05-23 19:18:46",
updated_at: "2016-05-23 19:18:46">]>

その他、オブジェクトなので.lengthも使える

>> User.all.length
=> 2

更新

.save

>> 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をしないと更新されない

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

.update_attributes

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

validationとテスト駆動開発

Userオブジェクトが有効か

  • setupは各テストが走る直前で実行される
  • @userはインスタンス変数
  • valid?は有効かを判定
test/model/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

test実行

$ rails test:models

validate presence

ユーザーがデータベースに保存される前に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

  test "email should be present" do
    @user.email = ""
    assert_not @user.valid?
  end
end
app/models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true
  validates :email, presence: true
end

error messageを表示

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

>> user.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

>> user.errors.messages[:email]
=> ["can't be blank"]

validate length

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 lond" 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
app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

validates format mail-address

有効なメールフォーマットと無効なメールフォーマットをテスト

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[uesr@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[uesr@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com foo@bar..com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end
end
app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
end

メールアドレスが有効かを調べ正規表現

正規表現 意味
/\A[\w+-.]+@[a-z\d-.]+.[a-z]+\z/i (完全な正規表現)
/ 正規表現の開始を示す
\A 文字列の先頭
[\w+-.]+ 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す
@ アットマーク
[a-z\d-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す
. ドット
[a-z]+ 英小文字を少なくとも1文字以上繰り返す
\z 文字列の末尾
/ 正規表現の終わりを示す
i 大文字小文字を無視するオプション

validates unique

dupメソッド

メールアドレスの一意性を検証するために、メモリ上だけでなく実際にレコードをデータベースに登録する必要がある。
まずは重複したメールアドレスからテストする。

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 shuold be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end
end
  • @userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテスト
  • dupは同じ属性を持つデータを複製するためのメソッド
  • @userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
  • メールアドレスでは大文字小文字が区別されなし。したがって大文字を区別しないように@user.email.upcaseと記述

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 }

emailにindexを追加で重複を回避

emailにindexを追加

$ rails generate migration add_index_to_users_email

メールアドレスの一意性のマイグレーションを定義

  • usersテーブルのemailカラムにインデックスを追加するadd_index
  • オプションのunique: trueを指定することで一意性を強制
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

データベースをマイグレート

$ rails db:migrate

fixture内のサンプルデータを削除

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

email属性を小文字に変換する

  • 文字列は同一であると解釈されるべきなので、データベースに保存される直前にすべての文字列を小文字に変換
  • データベースに保存する前にemail属性をbefore_saveで強制的に小文字に変換
  • selfは現在のユーザーを指す
  • こちらでも可。before_save { email.downcase! }
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 }
 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 saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    assert_equal mixed_case_email.downcase, @user.reload.email
  end
end

パスワードのセキュア化

  • ユーザーにパスワードとパスワードの確認を入力させ、ハッシュ化したものをデータベースに保存
  • ハッシュ化とはハッシュ関数を使って入力されたデータを元に戻せないデータにする
  • ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較の手順

has_secure_password

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

password_digestの追加

 $ rails generate migration add_password_digest_to_users password_digest:string

※to_usersにすることで、usersテーブルにカラムを追加するマイグレーションがRailsによって自動的に作成される

マイグレーションを実行

$ rails db:migrate

bcryptのinstall

  • has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要
  • パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされない

''' ruby:Gemfile
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
```

$ bundle 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: { case_sensitive: false }
  has_secure_password
end

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

パスワードの最小文字数設定

  • パスワードとパスワード確認に対して同時に代入
  • 空のパスワードを入力させないために存在性のバリデーション追加
  • has_secure_passwordメソッドは存在性のバリデーションもするが、新しくレコードが追加されたときだけに適用される
  • そのためパスワード更新用にpresence: 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",
                     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の作成と認証

データベースに新規ユーザーを1人作成

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

データベースに保存されたかを確認

has_secure_passwordの効果を確認

console
>> user = User.find_by(email: "mhartl@example.com")

>> user.password_digest
=> "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3yOcVFzb6oK"

authenticateメソッドの確認

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

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

# ログインするときは論理値で判定
>> !!user.authenticate("foobar")
=> true
73
74
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
73
74