deviseのユーザー登録機能を拡張する

# はじめに
deviseのデフォルトでは、emailとパスワードしかuserテーブルに保存されません。
今回は、ユーザー登録をする際に

- userテーブルのカラムの追加
- userテーブルに対して一対一のテーブルへの保存
- userテーブルに対して１対多のテーブルへの保存

のやり方を紹介します。

# 環境

- Rails 5.2.2.1
- Ruby 2.5.1
- devise 4.7.1

# ユーザー登録できるようにする

- テストアプリの作成

```
$ rails _5.2.2.1_ new devise_test -d mysql
```

- gemの追加

```ruby:Gemfile
gem 'pry-rails'
gem 'compass-rails', '3.1.0'
gem 'sprockets', '3.7.2'

gem 'haml-rails'
gem 'devise'
```

- Gemfileの中身を変更

```ruby:Gemfile
gem 'mysql2', '0.5.2'
gem 'sass-rails', '5.0.7'
```

- gemの反映

```
$ bundle install
```

- データベース作る

```
$ rake db:create
```

- deviseのインストール

```
$ rails g devise:install
```

- erbファイルをhamlに変換

```
$ rails haml:erb2haml
```

- userモデルの作成

```
$ rails g devise user
```

- userコントローラの作成

```
$ rails g controller users
```

- userコントローラを編集

```ruby:app/controllers/user_controller.rb
class UsersController < ApplicationController
  def index
  end
end
```

- ビューファイルの作成

app/views/users/にindex.html.hamlを作成

```ruby:app/views/users/index.html.haml
- if user_signed_in?
  %p
    ログインしています
  =link_to "ログアウト", destroy_user_session_path, method: :delete
- else
  %p
    ログインしていません
    %li
      =link_to "新規会員登録", new_user_registration_path, method: :get
    %li
      =link_to "ログイン", new_user_session_path, method: :get
```

- ルーティングの設定

```ruby:config/route.rb
Rails.application.routes.draw do
  devise_for :users
  root 'users#index'
end
```

- マイグレーションの実行

```
$ rake db:migrate
```

基本的なユーザー登録はここまで。
これでdeviseデフォルトのemail、パスワード保存ができるようになりました。

# deviseのデフォルト以外のカラムを追加する
例）登録時、userテーブルにnicknameを追加する

- nicknameを追加するマイグレーションファイルを生成

```
$ rails g migration AddNicknameToUsers
```

- マイグレーションファイルを編集

```ruby:db/migrate/2019*********_add_nickname_to_users.rb
class AddNicknameToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :nickname, :string
  end
end
```

- マイグレーションの実施

```
$ rake db:migrate
```

- application controllerを編集

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
  end
end
```

- deviseのビューを編集可能にする

```
$ rails g devise:views
```

- deviceのビューをhamlに変換する

```
$ rails haml:erb2haml
```

- 新規登録画面にnicknameの入力欄を追加する

ついでにform_forをRails5.1以降推奨のform_withに変える


```ruby:app/views/devise/registrations/new.html.haml
%h2 Sign up
= form_with model: @user, url: user_registration_path, id: 'new_user', class: 'new_user',local: true do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :nickname
    %br/
    = f.text_field :nickname, autofocus: true, autocomplete: "nickname"
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :password
    - if @minimum_password_length
      %em
        (#{@minimum_password_length} characters minimum)
    %br/
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    %br/
    = f.password_field :password_confirmation, autocomplete: "new-password"
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"
```
これで完了。
deviseのデフォルトのカラムに加え、nicknameも一緒に保存できるようになりました。

# 1:1のテーブルの情報を一緒に保存する
例）userテーブルは、１対１のprofileテーブルを持つものとする。
profileテーブルには以下のカラムがあるとする。

- 住んでいる国(country)
- ユーザーid(user_id)

ユーザー登録時に、住んでいる国も同時に登録する。


- profileモデルの生成

``` 
$ rails g model Profiles
```

- マイグレーションファイルの編集

```ruby:db/migrate/2019**********_create_profiles.rb
class CreateProfiles < ActiveRecord::Migration[5.2]
  def change
    create_table :profiles do |t|
      t.string :country
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```

- マイグレーションの実行

```
$rake  db:migrate
```

- userモデルを編集

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  accepts_nested_attributes_for :profile

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
```

- profileモデルを編集

```ruby:app/models/profile.rb
class Profile < ApplicationRecord
  belongs_to :user
end
```

- application controllerを編集

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, profile_attributes: [:country]])
  end
end
```

- deviceのコントローラーを表示

```
$ rails g devise:controllers users
```

- registration_controllerを編集

必要なところのコメントアウトを外し、createアクション内に追記
一番下に、privateとprofile_paramsを追加

```ruby:app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]
  before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  def new
    super
  end

  # POST /resource
  def create
    super
    @user = User.new(profile_params)
    @user.build_profile
    @user.save
  end

  # GET /resource/edit
  def edit
    super
  end

  # PUT /resource
  def update
    super
  end

  # DELETE /resource
  def destroy
    super
  end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  def cancel
    super
  end

  # protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  end

  # If you have extra params to permit, append them to the sanitizer.
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  end

  # The path used after sign up.
  def after_sign_up_path_for(resource)
    super(resource)
  end

  # The path used after sign up for inactive accounts.
  def after_inactive_sign_up_path_for(resource)
    super(resource)
  end

  private

  def profile_params
    params.permit(:sign_up, keys: [:nickname, profile_attributes: [:country]])
  end
end
```

- ビューにcountryの入力フォームを作る

fields_forを使う

```ruby:app/views/devise/registrations/new.html.haml
%h2 Sign up
= form_with model: @user, url: user_registration_path, id: 'new_user', class: 'new_user',local: true do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :nickname
    %br/
    = f.text_field :nickname, autofocus: true, autocomplete: "nickname"
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :password
    - if @minimum_password_length
      %em
        (#{@minimum_password_length} characters minimum)
    %br/
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    %br/
    = f.password_field :password_confirmation, autocomplete: "new-password"
  = f.fields_for :profile, Profile.new do |i|
    .field
      = i.label :country
      %br/
      = i.text_field :country
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"
```

これで１対１のテーブル情報であるcountryを保存できる。


# 1:多のテーブルの情報を一緒に保存する
例）userテーブルは、１対多のcreditcardテーブルを持つものとする。
creditcardテーブルには以下のカラムがあるとする。

- カードナンバー(credit_number)
- ユーザーid(user_id)

ユーザー登録時に、クレジットカードも同時に一枚分登録する。
基本的には１対１とほぼ変わらない。

- クレジットカードモデルを作る

```
$ rails g model Creditcards
```

- マイグレーションファイルを編集

```ruby:db/migrate/2019**********_create_creditcards.rb
class CreateCreditcards < ActiveRecord::Migration[5.2]
  def change
    create_table :creditcards do |t|
      t.string :credit_number, null: false
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```

- マイグレーションの実行

```
$ rake db:migrate
```

- userモデルを編集

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  accepts_nested_attributes_for :profile

  has_many :creditcards, dependent: :destroy
  accepts_nested_attributes_for :creditcards
  
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
         
end
```

- Creditcardモデルを編集

```ruby:app/models/creditcard.rb
class Creditcard < ApplicationRecord
  belongs_to :user
end
```

- applicatuib_controllerを編集

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, profile_attributes: [:country], creditcards_attributes: [:credit_number]])
  end
end
```

- registration_controllerを編集

```ruby:app/controllers/users/registrations_controller.rb
createアクションとprofile_paramsに、追加の記述を行う

 def create
    super
    @user = User.new(profile_params)
    @user.build_profile
    @user.creditcards.build
    @user.save
  end

def profile_params
    params.permit(:sign_up, keys: [:nickname, profile_attributes: [:country], creditcards_attributes: [:credit_number]])
end
```

- ビューにcountryの入力フォームを作る

```ruby:app/views/devise/registrations/new.html.haml
%h2 Sign up
= form_with model: @user, url: user_registration_path, id: 'new_user', class: 'new_user',local: true do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :nickname
    %br/
    = f.text_field :nickname, autofocus: true, autocomplete: "nickname"
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :password
    - if @minimum_password_length
      %em
        (#{@minimum_password_length} characters minimum)
    %br/
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    %br/
    = f.password_field :password_confirmation, autocomplete: "new-password"
  = f.fields_for :profile, Profile.new do |i|
    .field
      = i.label :country
      %br/
      = i.text_field :country
  = f.fields_for :creditcards, Creditcard.new do |j|
    .field
      = j.label :credit_number
      %br/
      = j.text_field :credit_number
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"
```
以上で１対多のテーブル情報を保存できるようになります。

# fields_for周りでエラーが出ている人へ
以下のようなエラーが出ている場合の対処

```
LoadError in Devise::RegistrationsController#new
Unable to autoload constant モデル名？, expected ファイルのディレクトリ to define it

もしくは

NameError in Devise::Registrations#new
howing ファイルのディレクトリ where line #2 raised:
uninitialized constant ActionView::CompiledTemplates::モデル名？
```
理由は単純で、両方ともfield_forの中に入れているモデル名が間違っています。
私が実際に遭遇した時は、アンダーバー「_」が入っているパターンでした。
具体的には、

- テーブル名は「delivary_address」
- モデル名は「DeliveryAdderss」

したがって、field_forの記述の仕方は

```ruby
f.fields_for :delivery_address, DeliveryAddress.new do |i|
```
となります。
モデル名が分からない！という方は、Railsの構造が下記のようになっているので参照してください。

```ruby:app/models/テーブル名
class モデル名 < ApplicationRecord
  処理色々
end
```
# 参考
https://qiita.com/params_bird/items/96922cae0c7a82ff157c
https://qiita.com/hiroweb/items/74867433ab5091713521#_reference-c32bb385189fe7807ff2
https://qiita.com/tmzkysk/items/6349d8d860c5982771ff