ユーザー登録(devise)と同時に別モデルにデータを同時保存したい場合の話。


はじめに

オンラインスクールにてRuby on Railsの学習をはじめ5ヶ月が経ちました。
リリースを前提に現在開発予定のアプリに実装した機能のアウトプットになります。
「Deviseのユーザー登録と同時に別モデルにデータを保存する方法」です。

具体的なユースケースの例は、下記のような場合です。
・ユーザー登録と同時に銀行口座が開設される。
・ユーザーは1つの銀行口座しか持つことができない。
・銀行口座IDとユーザーIDは紐付く。
・登録時の口座残高の初期値はnullではなく0円。

ユーザー登録と同時に何か一つ、自動で生成されるモノ。を実装する際はお役に立てるかと思います。

※今回は、バリデーションについては触れません。 ※今回は、アプリ作成・Devise導入・基本的なユーザー管理機能の説明は省きます。

導入

ユーザー管理機能の実装が終わっている前提。
最低限、ユーザー新規登録とログインはできる状態でこちらをお読み下さい。

まず前提として、deviseは基本的にGemで動くものなので、通常のMVCの流れとは違います。
”ユーザー登録と同時”に何か”別の動き”をさせるには、少なくともコントローラーに何かしら記述しなければいけません。
すなわち、”Userコントローラー”の”Createアクション”に追加で記述ができればなぁとなります。
しかし、ここで問題です。
私もスクールカリキュラム内で以下の通り学習しました。
「deviseの処理を行うコントローラーはGem内に記述されているため、編集することができません。」

ただ、deviseに使うストロングパラメーターの許可は、全てapplication_controller.rbに記述すれば使えるよ。という事でしたね。

しかし、色々と調べてみると、どうやら、deviseのコントローラの中身を細かくカスタマイズできるらしい。という事がわかったのでまずはそこから説明していきます。

本編

1. Usersコントローラの作成

$ rails g devise:controllers users # => usersコントローラの作成

上記コマンドで、usersコントローラを召喚します。
これを実行すれば、deviseに関係する全てのコントローラー及びアクションを自由に編集する為のコントローラーファイルがすべて作成されます。

2.銀行モデル・テーブルの作成

ユーザー登録時に同時に登録したい銀行口座モデル及び銀行口座テーブルの作成をします。

$ rails g model bank stock_quantity:integer user:references # =>Bankモデルの作成
$ # stock_quantityカラム:銀行残高 # user_idカラム→Userモデルから口座を呼び出す為の外部キー 
$ rails db:migrate

3.モデル同士のアソシエーション「1対1」を組む

app/models/user.rb
class User < ApplicationRecord
  has_one :bank, dependent: :destroy # ユーザーは1つの銀行口座しか開設できない
  accepts_nested_attributes_for :bank # ここがポイント

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

Userモデルの「accepts_nested_attributes_for :bank」によって、Userモデルを通して、Bankの値をDBに保存することが出来ます。

app/models/bank.rb
class Bank < ApplicationRecord
  belongs_to :user
end

4.application_controller.rbでストロングパラメーターの許可

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    # 新規登録の時(sign_upアクション)に許可するパラメーター
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, :gender_id, :last_name, :first_name, { bank_attributes: [:stock_quantity] }])
  end
end
# { bank_attributes: [:stock_quantity] }に注目
# これでユーザー登録時にBankモデルのパラメータの許可も追加しています。

5.registrations_controller.rbでストロングパラメーターの許可

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(bank_params)
    @user.build_bank
    @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 bank_params
    params.permit(:sign_up, keys: [:nickname, :gender_id, :last_name, :first_name, { bank_attributes: [:stock_quantity] }])
  end
end

・必要箇所のコメントアウトを削除
※今回は、新規登録のcreateアクションの箇所のみ。各自、必要なアクションの箇所を消してくださいね。

・createアクション内に同時保存の追記
「build_bank」は、Userモデルに定義した「has_one :bank」によって、Userモデルのインスタンスメソッドとして使えるようになっているメソッドです。
@bank = Bank.new」としても、ビューで@userにネストした@bankを参照することができません。

・一番下、privateメソッド配下にbank_paramsの定義を追加
※これを忘れると、先程application_contoroller.rbで記述したストロングパラメータの許可が降りません。

6.ルーティング編集

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  root to: 'banks#index'
end

ユーザー登録時に自前で用意したuserコントローラーを経由してくれるようにする。

7.新規登録のVIEWを編集

app/views/devise/registrations/new.html.erb
#form_with @user do |f| 配下(内部)に

#省略

# 必ずf.fields_forのヘルパーメソッドを使う
<%= f.fields_for :bank, Bank.new do |f| %>
<%= f.hidden_field :stock_quantity, value: 0 %> # hidden_fieldがミソ
<% end %>



ユーザー新規登録のVIEWファイルのform_with内に上記を追加する。

・この時必ずf.fields_forのヘルパーメソッドを使用する。
※form_with @user do |f|内にf.field_for :bank do |f|をネストする為。

・hidden_fieldヘルパーメソッドを使用するのがミソ
※今回の同時登録するBankモデルの情報はユーザー登録時にユーザーに入力させる項目ではないため。

・銀行口座の残高の初期値は0円の為、value:0とする。

おわりに

これで、ユーザー新規登録と同時に銀行口座が開設されます。
開設された口座には、必ず一人のユーザーが紐付きます。
そして、最初は残高0円からスタートします。