概要
サインアップ機能では、フォームで入力されたユーザー情報をデータベースに登録する。
データベースの構築、テーブルのマイグレーション、モデルによるDBの操作を整理する。
データベースの準備
「データベースの準備」の要領でプロジェクトで扱うデータベースを準備する。
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 ... 
| 1 2 3 4 5 6 7 8 | [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
| 1 2 | class User < ApplicationRecord end | 
同時に、dbディレクトリーにマイグレーションファイルが生成される。
ファイル名は20210320060906_create_users.rbで、日時がUSTとなっている。
db/20210320060906_create_users.rb
| 1 2 3 4 5 6 7 8 9 10 11 | 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コマンドを実行すると、マイグレーションファイルの内容に従ってテーブルが作成される。
| 1 2 3 4 5 | [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
| 1 2 3 4 5 6 7 8 9 | 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
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | <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
| 1 2 3 4 5 6 7 8 9 | class UsersController < ApplicationController   .....   private   def user_params     params.require(:user).permit(:name, :email, :password)   end end | 
データ保存の枠組み
フォームの入力データを単に保存する、というだけの手続きであれば、以下の処理になる。
- フォームからデータがPOSTされる
- それがsign_up_processにルーティングされる
- sign_up_processアクション内で
- フォームに入力されたパラメーターで新たなUserモデルのインスタンスを生成
- そのインスタンスの内容でデータベースに書き込み
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 | 
フォームへの入力後、サインアップのボタンを押した後のテーブルの内容を確認すると、データは登録されている。
| 1 2 3 4 5 6 7 | 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モデルへのバリデーションの実装は以下の通り。
| 1 2 3 4 5 6 7 8 9 10 11 12 | 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
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .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つ登録した結果。
| 1 2 3 4 5 6 7 | 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モデルに以下を追記する。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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でコールバックを設定していて、モデルの内容でデータベースのレコードが新規作成されるときにパスワードが暗号化される。
以下はテストデータを登録した結果。
| 1 2 3 4 5 6 7 | 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 | +----+--------+-----------------------+----------------------------------+---------------------+---------------------+ | 






