Rails Tutorial 5 Modeling users
Chapter 6/ Modeling users | Ruby on Rails Tutorial (Rails 5) | Softcover.io
ユーザー登録ページを作っていく。
ユーザー用のデータモデルの作成と、データを保存する手段の確保について学ぶ。
データベースの移行
generate modelというコマンドを使ってnameやemailといった属性を付けたUserモデルを作成する。
$ rails generate model User name:string email:string Running via Spring preloader in process 225848 invoke active_record create db/migrate/20161023011602_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml
これでマイグレーションファイルが出来上がる。
マイグレーションを適用することでSQLiteデータベースが出来上がる。
$ rails db:migrate == 20161023011602 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0019s == 20161023011602 CreateUsers: migrated (0.0021s) =============================
以下を実行するだけでマイグレーションの取り消しも可能。
$ rails db:rollback == 20161023011602 CreateUsers: reverting ====================================== -- drop_table(:users) -> 0.0015s == 20161023011602 CreateUsers: reverted (0.0041s) =============================
もう一度マイグレーションを適用すれば元に戻る。
$ rails db:migrate == 20161023011602 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0093s == 20161023011602 CreateUsers: migrated (0.0094s) =============================
modelファイル
app/models/
ディレクトリにuser.rb
というファイルができている。
class User < ApplicationRecord end
Rails 4.2 ではUserクラスはActiveRecord::Baseを継承しているが、Rails 5 では ApplicationRecordを継承している。(ApplicationRecord は ActiveRecord::Base を継承している。)
ということでここからActiveRecord::Baseについて学んでいく。
ユーザオブジェクトの作成
Railsコンソールを使用してデータモデルを調べていくが、コンソールをサンドボックスモードで起動すればデータベースを変更することなく操作を試すことができるそうな。
$ rails console --sandbox Running via Spring preloader in process 451803 Loading development environment in sandbox (Rails 5.0.0.1) Any modifications you make will be rolled back on exit >>
今回はRailsコンソール起動時にRailsの環境を自動的に読み込み、モデルも自動的に読み込むため以下を実行するだけで新しいユーザオブジェクトができる。
>> User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
引数なしだと全ての属性はnilを返す。
Active Recordの設計に基づき、オブジェクトの属性を設定するための初期化ハッシュ (hash) を引数に取ることもできる。
>> 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>
「有効性 (Validity)」を確認することで、データベースにデータがあるかどうかによらず、オブジェクトが有効かどうかを確認することができる。
>> user.valid?
=> true
save
メソッドを呼び出してデータベースにUserオブジェクトを保存する。
>> user.save (0.1ms) SAVEPOINT active_record_1 SQL (0.5ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", 2016-10-24 00:41:30 UTC], ["updated_at", 2016-10-24 00:41:30 UTC]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true
save
を実行することで初めて created_at や updated_at が更新される。
new
, save
をいっぺんに実行する create
。
>> User.create(name: "A Nother", email: "another@example.org") (5.1ms) SAVEPOINT active_record_1 SQL (3.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "A Nother"], ["email", "another@example.org"], ["created_at", 2016-10-24 21:40:33 UTC], ["updated_at", 2016-10-24 21:40:33 UTC]] (0.1ms) RELEASE SAVEPOINT active_record_1=> #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2016-10-24 21:40:33", updated_at: "2016-10-24 21:40:33">
create
はオブジェクト自身を返すのでそれを変数に代入できる。
destroy
は create
の逆。
>> foo = User.create(name: "Foo", email: "foo@bar.com") (0.1ms) SAVEPOINT active_record_1 SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Foo"], ["email", "foo@bar.com"], ["created_at", 2016-10-24 21:41:33 UTC], ["updated_at", 2016-10-24 21:41:33 UTC]] (0.1ms) RELEASE SAVEPOINT active_record_1 => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-10-24 21:41:33", updated_at: "2016-10-24 21:41:33"> >> foo => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-10-24 21:41:33", updated_at: "2016-10-24 21:41:33"> >> foo.destroy (0.1ms) SAVEPOINT active_record_1 SQL (0.1ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]] (0.0ms) RELEASE SAVEPOINT active_record_1 => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-10-24 21:41:33", updated_at: "2016-10-24 21:41:33"> >> foo => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-10-24 21:41:33", updated_at: "2016-10-24 21:41:33"> >> foo.valid? => true
find
でユーザの id による検索が可能。
>> User.find(1) User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2016-10-24 00:41:30", updated_at: "2016-10-24 00:41:30">
find_by
で id 以外の属性を使った検索が可能。
>> User.find_by(email: "mhartl@example.com") User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2016-10-24 00:41:30", updated_at: "2016-10-24 00:41:30">
ユーザーオブジェクトの更新には、属性を個別に代入するか、update_attributes を使用する。
前者は別途 save が必要で、後者は保存までをまとめて実行する。
>> user.email = "mhartl@example.net" => "mhartl@example.net" >> user.save (0.1ms) SAVEPOINT active_record_1 SQL (0.1ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "mhartl@example.net"], ["updated_at", 2016-10-24 22:27:09 UTC], ["id", 1]] (0.0ms) RELEASE SAVEPOINT active_record_1 => true >> user.update_attributes(name: "The Dude", email: "dude@abides.org") (0.1ms) SAVEPOINT active_record_1 SQL (1.6ms) UPDATE "users" SET "email" = ?, "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["email", "dude@abides.org"], ["updated_at", 2016-10-26 03:26:00 UTC], ["name", "The Dude"], ["id", 1]] (0.1ms) RELEASE SAVEPOINT active_record_1
ユーザの検証
モデルの属性が取れる値に制約を与えて、意図しない値の混入を防ぐ。
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
$ rails test:models
でモデルに関するテストのみ実行できる。
$ rails test:models Started with run options --seed 50541 1/1: [=========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.02565s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
Validating presence
存在性を検証する。
User の name が空だとそのユーザは有効でない、とするためのテストを書く。
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
assert_not で有効でないことを確認することができる。この時点ではテストは失敗するので成功になるようコードを修正していく。
と言ってもvalidates
メソッドの引数にpresence: true
を与えるだけ。
app/models/user.rb
class User < ApplicationRecord validates :name, presence: true end
$ rails c --sandbox Running via Spring preloader in process 85201 Loading development environment in sandbox (Rails 5.0.0.1) Any modifications you make will be rolled back on exit >> user = User.new(name: "", email: "mhartl@example.com") => #<User id: nil, name: "", email: "mhartl@example.com", created_at: nil, updated_at: nil> >> user.valid? => false
errors オブジェクトを見ればどの検証が失敗したのかがわかる。
>> user.errors.full_messages => ["Name can't be blank"]
save は自然と失敗する。
>> user.save (0.1ms) SAVEPOINT active_record_1 (0.0ms) ROLLBACK TO SAVEPOINT active_record_1 => false
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
Length validation
各属性の長さを制限する。
名前は適当に50を上限とし、メールアドレスは、ほとんどのデータベースで文字列の上限を255としていることから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 @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
Format validation
メールアドレスのフォーマット検証を行う。
有効なメールアドレスと無効なメールアドレスをいくつか用意して、バリデーション内のエラーを検知していく。
まずは有効なメールアドレス。
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 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 test "email validation should be 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"
と、assert の第2引数にエラーメッセージを追加することで、どのメールアドレスでエラーになったかがわかる。
続いて無効なメールアドレス。
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
メールアドレスのフォーマット検証は、formatオプションを使う。
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
これで無効なメールアドレスは無効として扱われるようになりテストが通る。
$ rails test:models Started with run options --seed 30805 7/7: [=========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.03859s 7 tests, 15 assertions, 0 failures, 0 errors, 0 skips
Uniqueness validation
メールアドレスの重複を許さないように一意性を強制する。
User.new
ではメモリ上にRubyオブジェクトを作るだけだが、一意性のテストのためには実際にレコードをデータベースに登録する必要があるそうな。
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
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 となり成功するが、データベースレベルでの一意性を保証する必要があるそう。
マイグレーションでインデックスを追加する。
migration ジェネレーターを使用してマイグレーションを作成する。
$ rails generate migration add_index_to_users_email Running via Spring preloader in process 510723 invoke active_record create db/migrate/20161106170056_add_index_to_users_email.rb
db/migrate/20161106170056_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0] def change add_index :users, :email, unique: true end end
add_index
メソッドを使用して users テーブルの email 属性にインデックスをつける。
マイグレートを実行する。
$ rails db:migrate == 20161106170056 AddIndexToUsersEmail: migrating ============================= -- add_index(:users, :email, {:unique=>true}) -> 0.0101s == 20161106170056 AddIndexToUsersEmail: migrated (0.0102s) ====================
テストDB用のサンプルデータを含む fixtures が一意性に反しているためテストが失敗する。
fixtures は一旦空にしておくことでテストが通るようになる。
あとは、データベースアダプタが大文字小文字を区別するインデックスを使っている場合に備えてユーザーをデータベースに保存する前に全て小文字に変えてやる。
app/models/user.rb
class User < ApplicationRecord before_save { self.email = email.downcase } ・ ・ ・ end
before_save コールバックで self の email を小文字にかえる。
Adding a secure password
セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存します。
ということでセキュアパスワードを追加していく。
User モデルに下記のように has_secure_password
を追加するだけ。
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
ただし、has_secure_password
を使用するにはモデルに password_digest
という属性を持っている必要がある。
password_digest
カラムを追加するためのマイグレーションを作成する。
$ rails generate migration add_password_to_users password_digest:string Running via Spring preloader in process 73555 invoke active_record create db/migrate/20161109220735_add_password_to_users.rb
マイグレーションを実行する。
$ rails db:migrate == 20161109220735 AddPasswordToUsers: migrating =============================== -- add_column(:users, :password_digest, :string) -> 0.0044s == 20161109220735 AddPasswordToUsers: migrated (0.0045s) ======================
has_secure_password
を使ってパスワードをハッシュ化するのに必要な bcrypt
をインストールする。(Rails 5 tutorial では 3.1.11 を使用しているので合わせる。)
source 'https://rubygems.org' gem 'rails', '5.0.0.1' gem 'bcrypt', '3.1.11' . . .
$ bundle install Fetching gem metadata from https://rubygems.org/ Fetching version metadata from https://rubygems.org/ Fetching dependency metadata from https://rubygems.org/ Resolving dependencies... Using rake 11.2.2 Using concurrent-ruby 1.0.2 Using i18n 0.7.0 Using minitest 5.9.0 Using thread_safe 0.3.5 Using builder 3.2.2 Using erubis 2.7.0 Using mini_portile2 2.1.0 Using pkg-config 1.1.7 Using rack 2.0.1 Using nio4r 1.2.1 Using websocket-extensions 0.1.2 Using mime-types-data 3.2016.0521 Using arel 7.1.1 Using ansi 1.5.0 Using execjs 2.7.0 Installing bcrypt 3.1.11 with native extensions Using sass 3.4.22 Using bundler 1.12.5 Using byebug 9.0.0 Using coderay 1.1.1 Using coffee-script-source 1.10.0 Using method_source 0.8.2 Using thor 0.19.1 Using debug_inspector 0.0.2 Using ffi 1.9.14 Using formatador 0.2.5 Using rb-fsevent 0.9.7 Using lumberjack 1.0.10 Using nenv 0.3.0 Using shellany 0.0.1 Using slop 3.6.0 Using guard-compat 1.2.1 Using multi_json 1.12.1 Using ruby-progressbar 1.8.1 Using puma 3.4.0 Using tilt 2.0.5 Using spring 1.7.2 Using sqlite3 1.3.11 Using turbolinks-source 5.0.0 Using tzinfo 1.2.2 Using nokogiri 1.6.8 Using rack-test 0.6.3 Using sprockets 3.7.0 Using websocket-driver 0.6.4 Using mime-types 3.1 Using autoprefixer-rails 6.5.0.1 Using uglifier 3.0.0 Using coffee-script 2.4.1 Using rb-inotify 0.9.7 Using notiffany 0.1.1 Using pry 0.10.4 Using guard-minitest 2.4.4 Using minitest-reporters 1.1.9 Using turbolinks 5.0.1 Using activesupport 5.0.0.1 Using loofah 2.0.3 Using mail 2.6.4 Using bootstrap-sass 3.3.6 Using listen 3.0.8 Using rails-dom-testing 2.0.1 Using globalid 0.3.7 Using activemodel 5.0.0.1 Using jbuilder 2.4.1 Using rails-html-sanitizer 1.0.3 Using guard 2.13.0 Using spring-watcher-listen 2.0.0 Using activejob 5.0.0.1 Using activerecord 5.0.0.1 Using actionview 5.0.0.1 Using actionpack 5.0.0.1 Using actioncable 5.0.0.1 Using actionmailer 5.0.0.1 Using railties 5.0.0.1 Using sprockets-rails 3.2.0 Using rails-controller-testing 0.1.1 Using coffee-rails 4.2.1 Using jquery-rails 4.1.1 Using web-console 3.1.1 Using rails 5.0.0.1 Using sass-rails 5.0.6 Bundle complete! 22 Gemfile dependencies, 81 gems now installed. Gems in the group production were not installed. Use `bundle show [gemname]` to see where a bundled gem is installed.
has_secure_password
は仮想的なpassword属性とpassword_confirmation属性に対してバリデーションする機能があるため、これを通すようにテストを書き換える。
test/models/user_test.rb
def setup @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end
これでテストが通る。
Minimum password standards
パスワードの最小文字数を設定する。
まずテストを書き換える。
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
テストが通るように制約を加える。
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 validates :password, presence: true, length: { minimum: 6 } end
これでテストが通る。
Creating and authenticating a user
Rails コンソールで実際にユーザを作成してみる。
今回は作成したユーザを後で使用するのでサンドボックスは使用せず、実際にデータベースに反映する。
$ rails c Running via Spring preloader in process 87215 Loading development environment (Rails 5.0.0.1)
create を使う。
>> User.create(name: "Michael Hartl", email: "mhartl@example.com", password: "foobar", password_confirmation: "foobar") (0.1ms) begin transaction User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]] SQL (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", 2016-11-09 23:41:18 UTC], ["updated_at", 2016-11-09 23:41:18 UTC], ["password_digest", "$2a$10$DBDRkhzw2K1ufegQ7VKtlu5G8UUXRhlozjcXLk4AW/KfzA51adpQq"]] (9.9ms) commit transaction => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2016-11-09 23:41:18", updated_at: "2016-11-09 23:41:18", password_digest: "$2a$10$DBDRkhzw2K1ufegQ7VKtlu5G8UUXRhlozjcXLk4AW/K...">
これで保存されたので、下記のようにしてハッシュ化されたパスワードが見れる。
>> user = User.find_by(email: "mhartl@example.com") User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2016-11-09 23:41:18", updated_at: "2016-11-09 23:41:18", password_digest: "$2a$10$DBDRkhzw2K1ufegQ7VKtlu5G8UUXRhlozjcXLk4AW/K..."> >> user.password_digest => "$2a$10$DBDRkhzw2K1ufegQ7VKtlu5G8UUXRhlozjcXLk4AW/KfzA51adpQq"
User モデルに has_secure_password
を追加したので、authenticate
メソッドが使用できる。
>> user.authenticate("not_the_right_password") => false >> user.authenticate("foobaz") => false >> user.authenticate("foobar") => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2016-11-09 23:41:18", updated_at: "2016-11-09 23:41:18", password_digest: "$2a$10$DBDRkhzw2K1ufegQ7VKtlu5G8UUXRhlozjcXLk4AW/K..."> >> !!user.authenticate("foobar") => true
これでパスワード認証の準備ができた。
デプロイ
これまで通りの要領で bitbucket 及び heroku にプッシュするが、本番環境でUserモデルを使うためには、heroku runコマンドを使ってHeroku上でもマイグレーションを走らせる必要がある。
$ git push heroku $ heroku run rails db:migrate
まとめ
- マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
- Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
- Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
- よくあるバリデーションには、存在性・長さ・フォーマットなどがある
- 正規表現は謎めいて見えるが非常に強力である
- データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
次章以降で、ユーザーを作成するためのユーザー登録フォームを作成していく。