ファーストビュー画像
ヘッダーロゴ
ホームアイコン
>
>
【Rails/devise】会員登録時に関連付けられたモデルを同時に作成する
バックエンド

【Rails/devise】会員登録時に関連付けられたモデルを同時に作成する

作成日2024/07/28
更新日2024/08/01
アイキャッチ
# Ruby on Rails

今回は複数のユーザーが同じ企業アカウントのデータへのアクセスを共有する必要があるサービスがあると想定してUser(企業の社員)を登録時に紐づくAccount(企業)を同時に作成する登録フォームを作成します。

Rails7.1 + ESbuild + Tailwind CSSでの環境構築、 RSpecの導入が完了している前提で進めていきます。
環境構築の記事はこちら、RSpecの導入の記事はこちらで紹介しています。

Accountモデルの作成

accountsテーブルの作成

string型のnameカラムを持つAccountモデルを作成します。

rails g model Account name:string

nameカラムは必須にするため、作成されたマイグレーションファイルを以下のように編集します。

class CreateAccounts < ActiveRecord::Migration[7.1]
  def change
    create_table :accounts do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end

編集が完了したらmigrateします。

rails db:migrate

バリデーションの追加

Accountモデルにバリデーション定義も追加します。

class Account < ApplicationRecord
  validates :name, presence: true
end

deviseを導入

gemを追加 

Gemfiledeviseを追加します。

gem "devise"
bundle install

インストール

gemのインストール後、deviseのインストールを行います。

rails g devise:install

config/environments/development.rb, config/environments/test.rbにメーラーのデフォルトURLオプションを指定します。

Rails.application.configure do
  # 省略  
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
end

Userモデルの作成

deviseを使ってAccountに紐付けるUserモデルを作成します。
追加のカラムとしてfrist_name, last_name, アカウント内での役割であるrole, サービス全体の管理者を判別するadminカラムを指定しています。

rails g devise User account:references first_name:string last_name:string role:integer admin:boolean

migrationファイルに以下の追記をします。

  • メール認証を行うため、Confirmableのコメントを外す
  • accountとの関連付けにnot null制約、外部キー制約を追加
  • adminカラムにデフォルト値を追加
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # ↓コメントアウトを外す↓
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable
      # ↑コメントアウトを外す↑

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      t.integer :role, null: false, default: 0 # デフォルト値を追加
      t.references :account, null: false, foreign_key: true # not null制約、外部キー制約を追加
      t.string :first_name
      t.string :last_name
      t.boolean :admin, null: false, default: false # デフォルト値を追加

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

編集が完了したらmigrateします。

rails db:migrate

user.rbconfirmableの記述を追加し、enumroleを定義します。

今回は一般ユーザーであるgeneralとオーナーユーザーであるownerを定義しています。
後に、オーナーのみがアカウントにユーザーを招待できる機能を実装予定です。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  enum role: { general: 0, owner: 1 }
end

AccountモデルとUserモデルの関連付けの設定を行います。

class Account < ApplicationRecord
  validates :name, presence: true

  has_many :users, dependent: :destroy
end
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  belongs_to :account
  accepts_nested_attributes_for :account

  enum role: { general: 0, owner: 1 }
end

今回は1つのフォームでAccountUserを同時に作成するため、user.rbaccepts_nested_attributes_for :accountを定義していきます。

これにより、User作成時にAccountの属性をaccount_attributesとして渡すことができます。

ログイン後のダッシュボードを作成

Dashboardコントローラー作成

ログインした後に表示される仮のダッシュボードページを作成しておきます。

rails g controller Dashboard show

ログイン時未ログイン時のルートパスを定義

routes.rbを以下のように編集します。

未ログイン時はログイン画面、ログイン時はダッシュボード画面をルート画面としています。

Rails.application.routes.draw do
  devise_for :users

  authenticate :user do
    root to: "dashboard#show", as: :user_root
  end

  devise_scope :user do
    root to: "devise/sessions#new"
  end
end

registrationsをカスタマイズ

User作成時にAccountを作成できるようにデフォルトのregistrationsコントローラーをカスタマイズしていきます。

deviseをインストールしただけではregistrationsコントローラーはプロジェクト内に作成されないので、追加で作成します。

rails g devise:controllers users -c=registrations

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
    build_resource
    resource.build_account # userに紐づくaccountを初期化
    yield resource if block_given?
    respond_with resource
  end

  # POST /resource
  # def create
  #   super
  # 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

  def build_resource(hash = {})
    self.resource = resource_class.new_with_session(hash, session)
    self.resource.role = :owner
  end

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [
      account_attributes: [:name]
    ])
  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
end

カスタマイズ内容について軽く説明します。
デフォルトのregistrations_controllerの内容は公式のGitHubから確認できます。

newアクション

デフォルトのnewアクションにresource.build_accountの記述を追加して、userに紐づくaccountを初期化します。

def new
  build_resource
  resource.build_account
  yield resource if block_given?
  respond_with resource
end

build_resourceメソッド

build_resourceメソッドにself.resource.role = :ownerを追加して登録フォームからの作成はデフォルトでオーナーの役割を持つユーザーになるように設定します。

def build_resource(hash = {})
  self.resource = resource_class.new_with_session(hash, session)
  self.resource.role = :owner
end

configure_sign_up_paramsメソッド

configure_sign_up_paramsメソッドを編集して、パラメータとしてでaccountの属性であるnameを受け取れるように設定しています。

def configure_sign_up_params
  devise_parameter_sanitizer.permit(:sign_up, keys: [
    account_attributes: [:name]
  ])
end

カスタマイズした内容を反映できるようroutes.rbの内容を編集します。

Rails.application.routes.draw do
  devise_for :users,
    controllers: {
      registrations: "users/registrations"
    }

  # 省略
end

viewsをカスタマイズ

登録画面は現状だとUser作成用になっているのでAccountname属性の欄追加と、デザインのカスタマイズができるようにdevise関連のviewファイルを生成します。

rails g devise:views

tailwindcss-formsをインストール

フォームのデザインを整えるためにTailwind CSSのプラグインであるtailwindcss-formsをインストールします。

yarn add @tailwindcss/forms

tailwind.config.jsでプラグインの読み込みを行います。

module.exports = {
  // 省略

  plugins: [
    require('@tailwindcss/forms'),
  ]
}

登録画面のカスタマイズ

以下の内容で登録画面を編集します。

<div class="flex flex-col justify-center mt-12">
  <h2 class="text-center text-3xl font-extrabold text-gray-900">
    Sign up
  </h2>
  <div class="bg-white mt-8 mx-auto w-full max-w-lg p-8 shadow rounded-lg">
    <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |form| %>
      <%= form.fields_for :account do |account_fields| %>
        <%= render "devise/shared/error_messages", resource: resource %>

        <div class="mb-4">
          <%= account_fields.label :name, class: "block text-gray-700 text-sm font-bold mb-2" %>
          <%= account_fields.text_field :name, autofocus: true, class: "shadow border-gray-200 rounded w-full" %>
        </div>

        <div class="mb-4">
          <%= form.label :email, class: "block text-gray-700 text-sm font-bold mb-2" %>
          <%= form.email_field :email, autocomplete: "email", class: "shadow border-gray-200 rounded w-full" %>
        </div>

        <div class="mb-4">
          <%= form.label :password, class: "block text-gray-700 text-sm font-bold mb-2" %>
          <%= form.password_field :password, autocomplete: "new-password", class: "shadow border-gray-200 rounded w-full" %>
        </div>

        <div class="mb-6">
          <%= form.label :password_confirmation, class: "block text-gray-700 text-sm font-bold mb-2" %>
          <%= form.password_field :password_confirmation, autocomplete: "new-password", class: "shadow border-gray-200 rounded w-full" %>
        </div>

        <%= form.button "Sign Up", class: "font-bold py-2 px-4 mb-4 rounded bg-blue-500 hover:bg-blue-700 text-white w-full" %>

        <p class="text-center text-sm text-gray-600 max-w">
          Already have an account?
          <%= link_to "Sign in", new_user_session_path, class: "font-medium text-blue-600 hover:text-blue-800" %>
        </p>
      <% end %>
    <% end %>
  </div>
</div>

スタイルが反映されていれば完了です。

fields_forを用いることでUserの属性と同時にAccountの属性であるnameaccount_nameとしてパラメータに含めることができます。

動作確認

実際に登録してみます。

ログを見てみると、パラメータの"user"の値の中にaccount_attributesという形でaccountの属性が含まれています。

Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"account_attributes"=>{"name"=>"TEST01 inc."}, "email"=>"user@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "button"=>""}

コンソールで確認すると、user, accountが紐づいた状態で作成されています。

irb(main):009> User.count
  User Count (1.1ms)  SELECT COUNT(*) FROM "users"
=> 1
irb(main):010> Account.count
  User Count (1.1ms)  SELECT COUNT(*) FROM "accounts"
=> 1
irb(main):011> User.last.account.id == Account.last.id
  User Load (3.7ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
  Account Load (0.5ms)  SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2  [["id", 4], ["LIMIT", 1]]
  Account Load (0.2ms)  SELECT "accounts".* FROM "accounts" ORDER BY "accounts"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> true

テストを追加

最後にRSpecを追加します。

spec/system/usersディレクトリを追加し、registrations_spec.rbファイルを以下の内容で作成します。

require "rails_helper"

RSpec.describe "Registrations", type: :system do
  context "When the information is entered correctly" do
    it "A new user can be registered, and an account linked to user is created" do
      visit new_user_registration_path
      fill_in "user_account_attributes_name", with: "TEST inc."
      fill_in "user_email", with: "user@example.com"
      fill_in "user_password", with: "password"
      fill_in "user_password_confirmation", with: "password"

      expect do
        click_button "Sign Up"
        expect(page).to have_current_path new_user_session_path
      end.to change(User, :count).by(1).and change(Account, :count).by(1)

      user = User.last
      account = Account.last
      expect(user.account).to eq(account)
    end
  end
end

正常にテストが実行できれば完了です。

Registrations
  When the information is entered correctly
    A new user can be registered, and an account linked to user is created

Finished in 0.93563 seconds (files took 1.04 seconds to load)
1 example, 0 failures

参考

https://book.hotwiringrails.com/chapters/2

share on
xアイコンfacebookアイコンlineアイコン