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テーブルの構成について紹介してみます。
どんな構成か
結論から書きますが、以下のような構成になっています。
この構成のメリットは
- 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アドレスとユーザー名は保持しておきます。
このテーブルに保存しておくことで、上で作ったUserProfile
やUserAuthenticate
のレコードは削除することができます。
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_profile
とuser_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モデルの構成ができたと思います。
まだまだブラッシュアップの余地はあるかもですが、ひとまずこちらで終わろうと思います。