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

Railsで認証機能を作る際に[Devise](https://github.com/heartcombo/devise)を使うことが多いと思います。

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


```ruby
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/c613375e-b7ff-2188-242b-60d46204f5b7.png)


この構成のメリットは

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

みたいな点です。

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

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

## 各Modelについて

### Userモデル(User model)

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

```rb
# == 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`カラムがあります。これ以上のカラムの追加は基本しません。**
たとえば、名前やプロフィールなどは認証に必要ないので、このテーブルには追加せず、別テーブルにします。


```rb
# == 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で作った認証テーブルと分離されていることで肥大化が防げています。

```rb
# == 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制約を保つことができます。**

```rb
  
# == 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の重複を防ぐことが可能になっています。**

```rb
# == 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制約をつけられないデメリットがあります。

```rb
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モデルの構成ができたと思います。

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