patorashのブログ

方向性はまだない

deviseで論理削除を実装するときの手順をまとめておく

新しいRailsアプリを作るたびにやっている気がするので、一旦まとめておこうと思います。

今回公開する手順は、削除済みのメールアドレスで再登録可能な論理削除の実装方法です。

Deviseとは?

Deviseはアカウント認証管理のgemです。Webアプリケーションにほぼ必須である認証機能をほぼ網羅しています。

  • アカウントの新規作成
  • メールアドレスの確認
  • パスワード忘れの問い合わせ
  • パスワード間違えすぎたらアカウントロック
  • その他諸々

github.com

論理削除とは?

論理削除とは、レコードは物理的に削除しないけれど、削除済ということにする、ということです。 物理的に削除してしまうと、そのアカウントに紐づいたデータが全て削除されてしまったり、データに紐づいていたユーザが誰だったかわからなくなり、困ったことになります。 そこで、論理削除することで、ログインはできないけれど関連データを表示するときには使いたい、などがあります。

Deviseで論理削除するには?

Wikiに書いてある方法だと再登録不可能

Deviseは非常に便利なのですが、デフォルトでは論理削除に対応していないため、カスタマイズする必要があります。

一応、GitHubリポジトリWikiには、論理削除のやり方が書いてあります。

How to: Soft delete a user when user deletes account · heartcombo/devise Wiki · GitHub

それを翻訳してくださっている記事がQiitaにあります。

【翻訳】deviseで論理削除を実装する方法 - Qiita

ただし、この方法だと削除済みのメールアドレスで再登録しようとしたらできません。

そこで、今回は削除済みのメールアドレスで再登録可能な論理削除の実装方法をまとめておきます。

実装手順

deviseとkakurenbo-putiをインストールする。

Gemfileにdeviseとkakurenbo-putiを追加します。kakurenbo-putiは、論理削除機能を追加するためのgemです。論理削除機能を追加するgemは多々あるのですが、default scopeを上書きする等して論理削除済アカウントを除外するような動作をするものや、Railsのアップデートに伴って動作しなくなるものがあり、非常に辛いことになるのですが、kakurenbo-putiはdefault scopeを上書きせずにあくまでも機能を追加するだけなので副作用の心配がありません。

github.com

では、追加しましょう。

gem 'devise'
gem 'devise-i18n' # <-手軽に日本語化したければ…
gem 'kakurenbo-puti'

bundle install します。

deviseをセットアップする

deviseをセットアップします。今回はUserモデルとします。論理削除のみに集中するので、他の設定については公式を参照してください。

$ bin/rails generate devise:install
$ bin/rails generate devise User

これで、deviseの各種ファイルとUserモデルのファイルとマイグレーションファイルが追加されました。まだマイグレーションしないでください。

マイグレーションファイルを修正する

deviseのデフォルトのマイグレーションだと、論理削除を考慮した構成になっていませんので、論理削除用のカラムの追加と、インデックスの設定の修正を行います。

具体的には、以下を追加します。

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      # 略
      # kakurenbo-puti
      t.datetime :soft_destroyed_at
      # 略
    end

    # 元々あるemailをユニークにするindexはコメントアウトor削除する
    # add_index :users, :email, unique: true

    # 代わりに、soft_destoryed_atがNULLであることを条件にした部分indexを追加する
    add_index :users, :email, unique: true, where: '(soft_destroyed_at IS NULL)'
    add_index :users, :soft_destroyed_at
  end
end

修正が終わりましたら、マイグレーションを実行します。

$ bin/rails db:migrate

kakurenbo-putiをセットアップする

Userモデルでkakurenbo-putiの機能が使えるようにするため、修正します。

メールアドレスで再登録可能な実装をする場合、deviseのvalidatableはemailのみでユニークであることを検証しようとするため、コメントアウトして、代わりに自分でvalidationを追加する必要があります。

class User < ApplicationRecord

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable,
         :registerable,
         :recoverable,
         :rememberable,
         # :validatable, # <- emailのみでユニーク制約を検証してしまうのでコメントアウトする
         :confirmable,
         :lockable,
         :timeoutable

  soft_deletable # <- kakurenbo-putiを使えるようにする

  # 論理削除に対応するため、validationをカスタマイズする
  validates :email, presence: true, length: { maximum: 255 }
  validates_uniqueness_of :email, scope: :soft_destroyed_at
  validates_format_of :email, with: Devise.email_regexp, if: :will_save_change_to_email?
  validates :password, presence: true, confirmation: true, length: { in: Devise.password_length }, on: :create
  validates :password, confirmation: true, length: { in: Devise.password_length }, allow_blank: true, on: :update

  # 略
end

これで、Userモデルに論理削除用の機能が追加されました。

kakurenbo-putiで追加されるメソッド

論理削除機能が追加されることで、以下のようなメソッドが使えます。

ActiveRecord::Relation
User.all # 論理削除済ユーザも含んだ全てのユーザを取得
User.without_soft_destroyed # 論理削除済ユーザを除外して取得
User.only_soft_destroyed # 論理削除済ユーザのみ取得
Userモデル
user = User.without_soft_destroyed.first
user.soft_destroyed? # => false
user.soft_destroy # => 論理削除を実行
user.soft_destroyed? # => true
user.restore # => 復元を実行
user.soft_destroyed? # => false

他にも、callbackや破壊的メソッドなどもありますので詳しくはkakurenbo-putiのページをご参照ください。

ログイン時の処理で論理削除済ユーザを除外する

あとは、実際にログイン処理を行うときに利用されるメソッドであるfind_for_database_authenticationを上書きします。

参考になる公式のWikiはこちら。こちらはユーザ名でログインの実装なのですが、やっていることはfind_for_database_authenticationの上書きです。

How To: Allow users to sign in using their username or email address · heartcombo/devise Wiki · GitHub

再び、Userモデルを修正します。やることは、without_soft_destroyedを挟むことで、論理削除済ユーザを除外することです。

class User < ApplicationRecord
  # 略

  # データベース認証時に使われるメソッドを上書きして、
  # without_soft_destroyedを追加する
  def self.find_for_database_authentication(warden_conditions)
    conditions = warden_conditions.dup
    self.without_soft_destroyed.where(conditions.to_h).first
  end

  # 略
end

アカウント削除のactionを上書きする

あとは、アカウント削除処理を上書きしてdestroyする箇所をsoft_destroyにする必要があります。

まず、上書き用のコントローラーを作成しておきます。

$ rails generate devise:controllers users

これで、app/controllers/users配下にコントローラーができました。まだこれは使われる状態ではありません。config/routes.rbに設定する必要があります。

では、config/routes.rbでdeviseのregistrationsのコントローラーに先ほど生成されたコントローラーを指定します。

Rails.application.routes.draw do
  # 略
  devise_for :users, controllers: {
      registrations:      'users/registrations',
  }
  # 略
end

最後に、app/controllers/users/registrations_controller.rbを編集します。

class Users::RegistrationsController < Devise::RegistrationsController
  # 略
  
  # DELETE /resource
  def destroy
    resource.soft_destroy # <- 論理削除を実行
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    set_flash_message :notice, :destroyed
    yield resource if block_given?
    respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
  end

  # 略
end

まとめ

これで、deviseに論理削除を実装することができました。 deviseとkakurenbo-putiを合わせて使うと、論理削除の実装の手間がかなり軽減されます。 それでは、よい論理削除ライフを。