概要
サインアップ機能では、フォームで入力されたユーザー情報をデータベースに登録する。
データベースの構築、テーブルのマイグレーション、モデルによるDBの操作を整理する。
掲示板の第1段階へ
データベースの準備
「データベースの準備」の要領でプロジェクトで扱うデータベースを準備する。
database.yml
development用のデータベースのみ準備し、testとproductionではデータベースを作成しない設定とする。
config/database.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /var/lib/mysql/mysql.sock development: <<: *default database: ex_bbs_development test: <<: *default # database: ex_bbs_test production: <<: *default # database: ex_bbs_production # username: ex_bbs # password: <%= ENV['EX_BBS_DATABASE_PASSWORD'] %> |
DB作成
rails db:create
でデータベースを作成し、rails db:console
でmysqlコンソールに入り、データベースが作成されていることを確認。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
[vagrant@vagrant ex_bbs]$ rails db:create Created database 'ex_bbs_development' [vagrant@vagrant ex_bbs]$ rails dbconsole; Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 6 Server version: 5.5.62 MySQL Community Server (GPL) Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show databases; +----------------------------+ | Database | +----------------------------+ | information_schema | | codecamp_bbs | | codecamp_photo_development | | ex_bbs_development | | mysql | | performance_schema | | testdb | +----------------------------+ 7 rows in set (0.00 sec) |
モデルの生成とマイグレーション
モデルを生成し、これに対応したテーブルをマイグレーションにより生成する(基本的な方法はこちら)。
第1段階の最初で設定したデータベースのカラムを指定してモデルを生成する。id
、created_at
、updated_at
の3つはRailsが自動的に付加するので、以下の3つのみ指定する。
name:string
email:string
password:string
モデル名、モデルファイル名などの対応は以下の通り。
モデル名 |
user |
モデルクラス名 |
User |
モデルファイル名 |
user.rb |
テーブル名 |
users |
モデルの生成には、rails generate model
コマンドを使う。
rails generate model model_name col1:type col2:type ...
|
[vagrant@vagrant ex_bbs]$ rails generate model user name:string email:string password:string Running via Spring preloader in process 27642 invoke active_record create db/migrate/20210320060906_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml |
app/models
ディレクトリーにモデルファイルuser.rb
が生成される。
app/models/user.rb
|
class User < ApplicationRecord end |
同時に、dbディレクトリーにマイグレーションファイルが生成される。
ファイル名は20210320060906_create_users.rbで、日時がUSTとなっている。
db/20210320060906_create_users.rb
|
class CreateUsers < ActiveRecord::Migration[5.1] def change create_table :users do |t| t.string :name t.string :email t.string :password t.timestamps end end end |
rails db:maigrate
コマンドを実行すると、マイグレーションファイルの内容に従ってテーブルが作成される。
|
[vagrant@vagrant ex_bbs]$ rails db:migrate == 20210320060906 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0032s == 20210320060906 CreateUsers: migrated (0.0034s) ============================= |
rails dbconsoleコマンドを実行すると既にプロジェクトのデータベースを使う状態になっていて、usersテーブルが作成されているのが確認できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
mysql> show tables; +------------------------------+ | Tables_in_ex_bbs_development | +------------------------------+ | ar_internal_metadata | | schema_migrations | | users | +------------------------------+ 3 rows in set (0.00 sec) mysql> desc users; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | | email | varchar(255) | YES | | NULL | | | password | varchar(255) | YES | | NULL | | | created_at | datetime | NO | | NULL | | | updated_at | datetime | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.00 sec) |
フォームデータのデータベースへの登録
フォームデータの取得
モデルによるフォーム入力への変更
フォーム入力の確認ではURL(アクション)を指定してデータを送信した。
モデルを使う場合には、モデルのインスタンスを生成して、そのインスタンスを介してデータを扱う。そのために以下の2点を変更する。
まずフォームでモデルを指定するため、その実行前、sign_upで空のUserクラスのインスタンスを準備する。結果はコントローラーのインスタンス変数@user
に格納し、フォームがあるビューでも利用可能なようにしておく。
app/controllers/users_controller.rb
|
class UsersController < ApplicationController def sign_up @user = User.new render layout: 'application_signed_out' end ..... end |
次に、フォーム側でモデルに入力データを格納するように変更する。form_with
の場合は:model
キーで@user
を指定する(form_for
を使う場合は単に第1引数に@user
を指定する)。
app/views/users/sign_up.html.erb
|
<div class="sign_up_page"> <h1>EX-BBS</h1> <div class="sign_up_form"> <h2>サインアップページ</h2> <%= form_with model: @user, url: sign_up_process_path, local: true do |f| %> <%= f.text_field :name, placeholder: "ユーザー名" %> <%= f.email_field :email, placeholder: "メールアドレス" %> <%= f.password_field :password, placeholder: "パスワード" %> <button>サインアップ</button> <p>ユーザー登録済みの方は<%= link_to "サインインへ", sign_in_path %></p> <% end %> </div> </div> |
モデルのパラメーターの一括取得
モデルによるフォームデータの取得を参考に、require.permit
によるメソッドを準備しておく。
app/controllers/users_controller.rb
|
class UsersController < ApplicationController ..... private def user_params params.require(:user).permit(:name, :email, :password) end end |
データ保存の枠組み
フォームの入力データを単に保存する、というだけの手続きであれば、以下の処理になる。
- フォームからデータがPOSTされる
- それがsign_up_processにルーティングされる
- sign_up_processアクション内で
- フォームに入力されたパラメーターで新たなUserモデルのインスタンスを生成
- そのインスタンスの内容でデータベースに書き込み
|
class UsersController < ApplicationController ..... def sign_up_process user = User.new(user_params) user.save end ..... private def user_params params.require(:user).permit(:name, :email, :password) end end |
フォームへの入力後、サインアップのボタンを押した後のテーブルの内容を確認すると、データは登録されている。
|
mysql> select * from users; +----+--------+----------------------+----------+---------------------+---------------------+ | id | name | email | password | created_at | updated_at | +----+--------+----------------------+----------+---------------------+---------------------+ | 1 | 悟空 | monkey@taustation.jp | monkey | 2021-03-20 21:02:37 | 2021-03-20 21:02:37 | +----+--------+----------------------+----------+---------------------+---------------------+ 1 row in set (0.00 sec) |
入力データの検証と書き込み
入力データのバリデーション
入力データのバリデーション処理を記述する。検証内容は以下の通り。
|
:name |
:email |
:password |
存在 |
空ではない |
空ではない |
空ではない |
一意性 |
一意 |
一意 |
一意 |
長さ |
20文字以下 |
80文字以下 |
6文字以上 |
書式制約 |
なし |
あり |
なし |
モデルのデータのバリデーションは、モデルクラス内で以下を宣言する。実行はsave
、create
、update
の時に実行される。
validates: 対象カラム, 検証条件, 検証条件, ...
user
モデルへのバリデーションの実装は以下の通り。
|
class User < ApplicationRecord VALID_EMAIL_REGEX = /\A([\w+-]+.?[\w+-]+)+@([\w-]+.?[\w-]+)+.[\w-]+\z/i validates :name, presence: { message: "名前を入力してください" }, \ uniqueness: { message: "名前が既に使われています" }, \ length: { maximum: 20, message: "名前の長さは20文字以内です" } validates :email, presence: { message: "メールアドレスを入力してください" }, \ uniqueness: { message: "メールアドレスが登録済みです" }, \ format: { with: VALID_EMAIL_REGEX, message: "メールアドレスが適切ではありません" } validates :password, presence: { message: "パスワードを入力してください" }, \ uniqueness: { message: "パスワードが使われています" }, \ length: { minimum: 6, message: "パスワードを6文字以上にしてください" } end |
メールアドレスについては正規表現によりパターンチェックしている。
- ローカル部は単語形成文字(
a-z, _
)2文字以上で間に連続しないドットをはさんでよい
- ドメインは単語形成文字で間に連続しないドットをはさんでよく、最後にTLD(top level domain)が必要
エラー表示
モデルにvalidatesを定義すると、user.saveでデータベースに登録する前にバリデーションが実行される。
検証結果が適正であればデータは登録され、user.save
の戻り値はtrue
。不適正であればデータは登録されず、user.save
の戻り値はfalse
になる。
適正に登録された場合はトップページに遷移し、登録成功した旨のメッセージを一定時間表示する。一方不適正な場合は再度サインアップページを表示し、入力が不適正な旨のメッセージを一定時間表示する。
ページ遷移の際に一定時間メッセージを表示して消す機能についてはflushの使い方を参照。
コントローラーでの処理
- フォームからPOSTされたデータをで受け取り、
user_params
メソッドを介してモデルを生成
- テストのため、
valid?
メソッドでバリデーション実行
- 検証の結果が適正か不適正かによって
flash.now
に保存
- 共通レイアウトを指定し、適正なら
sign_in
を、不適正ならsign_up
をレンダリング
app/controllers/users_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
class UsersController < ApplicationController def sign_up @user = User.new render layout: 'application_signed_out' end def sign_in render layout: 'application_signed_out' end def sign_up_process @user = User.new(user_params) if @user.valid? flash.now[:success] = "ユーザー登録しました" render :sign_in, layout: 'application_signed_out' else flash.now[:danger] = @user.errors.messages.values.flatten.join("<br>") render :sign_up, layout: 'application_signed_out' end end ..... end |
flash.now
を使っている理由は以下の通り。
- エラー後の再入力で、直前に入力した値をフォームに入れておきたい
- そのためにはインスタンス変数
@user
を使う必要がある
- ただし
redirect_to
でアクションから実行すると@user
がnil
に初期化されてしまう
- そこで
render
で@user
の内容を保持したい
- そうすると、
flash
ではその次のアクションまで内容が保持されて余計な表示がされることがある
- そこで適正・不適正とも
flash.now
とrender
を使った
また、17行目の処理は以下の通り。
@user.errors.messages
はメッセージタイプをキーとするハッシュ
- ハッシュの値は検証結果のメッセージの配列
- まず
values
でmessages
の全てのメッセージタイプの値を取り出す(メッセージ配列を要素とする配列)
flatten
で1次元のメッセージの配列にする
join
で配列要素を文字列に結合(区切りは<br>
タグ)
レイアウトでの表示
- JQueryを使うのでCDNから読み込み(9行目)
flash.now
が設定されていればメッセージを表示
- 表示する
p
要素のクラスをタイプごとに設定
- メッセージ中の
<br>
タグを有効にするため、html_safe
を適用
- 3秒後にメッセージをスライドアップするJavaScriptを記述
app/views/layouts/application_signed_out.html.erb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
<!DOCTYPE html> <html> <head> <title>ExBbs</title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'https://code.jquery.com/jquery-3.5.0.min.js' %> </head> <body> <% if flash.now.present? %> <% flash.each do |type, message| %> <p class="flash_message flash_message_<%= type %>"><%= message.html_safe %></p> <% end %> <% end %> <%= yield %> <script> window.setTimeout(() => { $(".flash_message").slideUp(); }, 3000); </script> </body> </html> |
スタイルの設定
- .
flash_message
で共通のスタイルを定義
success/danger
に応じて設定されたクラスごとに色を変える
app/assets/stylesheets/common.scss
|
.flash_message { margin-bottom: 10px; padding: 10px; border: 2px solid; text-align: center; font-size: 16px; &_success { background-color: #9df; color: #00f; } &_danger { background-color: #fcc; color: #f00; } } |
DB書き込み処理
@user.valid?
でメッセージの内容や挙動を確認したら、ここを@user.save
に書き換える。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
class UsersController < ApplicationController ..... def sign_up_process @user = User.new(user_params) if @user.save flash.now[:success] = "ユーザー登録しました" render :sign_in, layout: 'application_signed_out' else flash.now[:danger] = @user.errors.messages.values.flatten.join("<br>") render :sign_up, layout: 'application_signed_out' end end ..... end |
データを1つ登録した結果。
|
mysql> SELECT * FROM users; +----+--------+-----------------------+----------+---------------------+---------------------+ | id | name | email | password | created_at | updated_at | +----+--------+-----------------------+----------+---------------------+---------------------+ | 3 | 悟空 | monkey@taustation.com | monkey | 2021-03-22 21:33:59 | 2021-03-22 21:33:59 | +----+--------+-----------------------+----------+---------------------+---------------------+ 1 row in set (0.00 sec) |
以降、重複する内容のデータで登録しようとするとuniqueness
で弾かれる。
パスワードの暗号化
パスワードの暗号化はDigest::MD5を使う。user
モデルに以下を追記する。
|
class User < ApplicationRecord before_create :encrypt_password ..... def encrypt_password self.password = User.password_digest(password) end def self.password_digest(password) salt = "Qc7XGCLIRTzAG7r3Wo7H7Ui++jr0" pass_digest = Digest::MD5.hexdigest(password) salt_digest = Digest::MD5.hexdigest(salt) Digest::MD5.hexdigest(pass_digest + salt_digest) end end |
before_save
でコールバックを設定していて、モデルの内容がデータベースに書き込まれるときにパスワードが暗号化される。
当初これをbefore_save としていたが、これだと後述のデータ更新のときにもコールバックが実行され、既にハッシュ化されているパスワードが更にハッシュ化されて登録されていまう。このため、これをbefore_create に変更。 |
before_create
でコールバックを設定していて、モデルの内容でデータベースのレコードが新規作成されるときにパスワードが暗号化される。
以下はテストデータを登録した結果。
|
mysql> SELECT * FROM users; +----+--------+-----------------------+----------------------------------+---------------------+---------------------+ | id | name | email | password | created_at | updated_at | +----+--------+-----------------------+----------------------------------+---------------------+---------------------+ | 3 | 悟空 | monkey@taustation.com | monkey | 2021-03-22 21:33:59 | 2021-03-22 21:33:59 | | 4 | 八戒 | piglet@taustation.com | 96131de181864ba18294e01a88c39318 | 2021-03-22 23:05:18 | 2021-03-22 23:05:18 | +----+--------+-----------------------+----------------------------------+---------------------+---------------------+ |