5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

私が考えるDeviseを使った肥大化させないUserモデルの構成

Posted at

Railsで認証機能を作る際にDeviseを使うことが多いと思います。

DeviseでUserテーブルを作った際に、以下の例のように色々なカラムを追加してUserテーブルが肥大化していくのをよく見かけます。

class DeviseCreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :first_name,         null: false, default: ""
      t.string :last_name,          null: false, default: ""
      t.text   :profile,             
      t.string :telephone,           
      t.integer :age,              
      t.integer :role_id,
      t.integer :company_id,
      t.datetime :deleted_at,

      ## Database authenticatable
      t.string :email,              null: false
      t.string :encrypted_password, null: false

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

      ## Rememberable
      t.datetime :remember_created_at

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      t.timestamps null: false
    end

  end
end

上記のような実装によって

  • Userテーブルが肥大化してコードが色々な責務を持ったUserモデルが出来上がる
  • deleted_atによる論理削除を実装したために、Emailをユニークにできなくなる

となってしまっていることがあります。

この記事では私なりに検討した、この問題を防ぐUserテーブルの構成について紹介してみます。

どんな構成か

結論から書きますが、以下のような構成になっています。

スクリーンショット 2021-08-04 15.42.17.png

この構成のメリットは

  • Userテーブルが肥大化しない
  • Deviseをカスタマイズする作業が少ない
  • Emailはユニークにできる
  • 退会したユーザーの情報はとっておくことができる

みたいな点です。

※本実装よりもさらに責務を分割したテーブル構成も実装したこともありました。
が、Deviseのデフォルトから大きくハズレたために、実装が複雑になることもあったので、その点の考慮をいれた実装になっています。

以降この構成で扱っている、各モデルについて詳しく書いていきます。

各Modelについて

Userモデル(User model)

Userモデルはid, created_at, updated_atのみです。
このモデルがあらゆるユーザーに関するテーブルの親になります。
子になることはありません。
このテーブルのレコードは、更新、削除を基本行いません。
ユーザーのSignUp時(=認証情報のレコード作るとき)に一緒に作られる想定です。

# == Schema Information
#
# Table name: users
#
#  id         :bigint           not null, primary key
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class User < ApplicationRecord
  # Relations
  has_one :user_authenticate
  has_one :user_profile
  has_many :blogs
  has_many :left_users

  # Methods
  def active?
    self.user_authenticate && self.user_profile
  end

  def already_left?
    self.left_users.precent?
  end
end

Userの認証情報モデル(UserAuthenticate model)

認証に必要なカラムが存在するテーブルです。
Deviseでつくられたカラムに加えて、上で作ったUserテーブルとのリレーションに使うuser_idカラムがあります。これ以上のカラムの追加は基本しません。
たとえば、名前やプロフィールなどは認証に必要ないので、このテーブルには追加せず、別テーブルにします。

# == Schema Information
#
# Table name: user_authenticates
#
#  id                     :bigint           not null, primary key
#  user_id                :bigint           not null
#  email                  :string(255)      default(""), not null
#  encrypted_password     :string(255)      default(""), not null
#  reset_password_token   :string(255)
#  reset_password_sent_at :datetime
#  remember_created_at    :datetime
#  confirmation_token     :string(255)
#  confirmed_at           :datetime
#  confirmation_sent_at   :datetime
#  unconfirmed_email      :string(255)
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
#
# Indexes
#
#  index_user_authenticates_on_confirmation_token    (confirmation_token) UNIQUE
#  index_user_authenticates_on_email                 (email) UNIQUE
#  index_user_authenticates_on_employee_id           (user_id) UNIQUE
#  index_user_authenticates_on_reset_password_token  (reset_password_token) UNIQUE
#
# Foreign Keys
#
#  fk_rails_...  (user_id => users.id)
#
class UserAuthenticate < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
  # Relations
  belongs_to :user

  # Callbacks
  ## SignUp時、user_idはnilなので、生成を行います。
  before_validation :set_user

  # Methods

  private

  def set_user
    self.user = User.new if user.nil?
  end
end

このテーブルをもっと細かく、EmailやPasswordを別テーブルに分ける案もありますが、そうなるとDeviseを結構カスタマイズする必要が出てきて大変なため、このテーブルを細かく分離することもしていません。

Userプロフィールモデル(UserProfile model)

認証には関係のない、ユーザーの情報を扱うモデルです。
たとえば、名前やプロフィールテキスト、年齢といったカラムが該当します。
また上で作ったUserテーブルとのリレーションに使うuser_idカラムも存在します
これらのカラムが、先ほどDeviseで作った認証テーブルと分離されていることで肥大化が防げています。

# == Schema Information
#
# Table name: user_profiles
#
#  id            :bigint           not null, primary key
#  user_id       :bigint           not null
#  name(名前)    :string(255)      not null
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#
# Indexes
#
#  index_user_profiles_on_user_id  (user_id) UNIQUE
#
# Foreign Keys
#
#  fk_rails_...  (user_id => users.id)
#
class UserProfile < ApplicationRecord
  # Relations
  belongs_to :user
end

Userが親である子テーブル(Blogs)について(Blog model)

たとえばUserが親である子テーブルを作る場合の例として、Blogsテーブルを例に取ります。

Blogsテーブルのリレーションは、UserProfileやUserAuthenticateとのリレーションではなく、Userモデルとリレーションをさせます。
Userモデルは削除されない想定ですので、Blogsテーブルにあるuser_idはnot null制約を保つことができます。

  
# == Schema Information
#
# Table name: blogs
#
#  id              :bigint           not null, primary key
#  user_id         :bigint           not null
#  title(タイトル) :string(255)      not null
#  created_at      :datetime         not null
#  updated_at      :datetime         not null
#
# Indexes
#
#  index_blogs_on_user_id  (user_id)
#
# Foreign Keys
#
#  fk_rails_...  (user_id => users.id)
#
class Blog < ApplicationRecord
  # Relations
  belongs_to :user
end

退会したユーザーを扱うモデル(LeftUser model)

退会したユーザーの情報は、別途このテーブルに保存を行います。
残しておきたい情報をカラムとして追加します。
今回の例では、Emailアドレスとユーザー名は保持しておきます。

このテーブルに保存しておくことで、上で作ったUserProfileUserAuthenticateのレコードは削除することができます。
UserAuthenticateが削除できるため、Emailの重複を防ぐことが可能になっています。

# == Schema Information
#
# Table name: left_users
#
#  id                   :bigint           not null, primary key
#  user_id              :bigint           not null
#  email(emailアドレス)  :string(255)      default(""), not null
#  name(ユーザー名)      :string(255)      default(""), not null
#  created_at           :datetime         not null
#  updated_at           :datetime         not null
#
# Indexes
#
#  index_left_users_on_user_id  (user_id)
#
# Foreign Keys
#
#  fk_rails_...  (user_id => users.id)
#
class LeftUser < ApplicationRecord

  # Relations
  belongs_to :user
end

退会処理の具体例

たとえば、退会処理を実装するときのコード例を示します。
以下のように、left_usersテーブルに必要な情報を作成してuser_profileuser_authenticateは削除する想定です。

user_authenticate(ユーザーの認証情報)を残しておくと、退会したユーザーとのEmailの重複があり得てしまいます。Emailの重複があり得ると、Uniq制約をつけられないデメリットがあります。

class UserProfiles::LeavesController < ApplicationController
  before_action :authenticate_user_authenticate!

  def destroy
    user_profile = UserProfile.find(params[:user_profile_id])
    user = user_profile.user
    ActiveRecord::Base.transaction do
      LeftUser.create(user: self, email: self.user_authenticate.email, name: self.user_profile.name)
      self.user_profile.destroy
      self.user_authenticate.destroy
    end
    reset_session
    redirect_to user_profiles_path, notice: "退会完了しました"
  rescue => e
    redirect_to user_profile_path(id: user_profile.id), alert: "退会に失敗しました"
  end
end

おしまい

さて、これで私の考えたDeviseを使ったUserテーブルの解説はおしまいです。
この構成で

  • Userテーブルが肥大化しない
  • Deviseをカスタマイズする作業が少ない
  • Emailはユニークにできる
  • 退会したユーザーの情報はとっておくことができる

の条件を満たしたUserモデルの構成ができたと思います。

まだまだブラッシュアップの余地はあるかもですが、ひとまずこちらで終わろうと思います。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?