🤖

論理削除による退会機能

2024/12/06に公開

はじめに

エンジニア転職を目指しRuby on Railsを中心に学習中の初学者です。
備忘録として、躓いたことやケアレスミスも含め投稿します!誤っている箇所などありましたらご指摘いただけると幸いです

対象読者

  • Ruby on Rails初学者

記事概要

論理削除とは簡潔に伝えると表面上は削除され非表示になるがデータベース上では保管されたままになる方法です。

物理削除は完全にデータベースから消去されます。

今回の実装ですが結論から申し上げるとgemを使った方が早いです。

gemを利用した参考記事↓
https://qiita.com/sawvistlip/items/0414e6fa4c7602d26f18

私は論理削除のgemがあることを知らずにgemなしで実装しました。参考になれば幸いです。

記事の前提条件

  • usersモデルが作成済みであること
  • Soceryを利用したログイン機能を実装していること
  • デザインはbootstarapと一部scssで調整。

1. 退会申請フォームの作成

まず、退会申請フォームを作成します。ユーザーがこのフォームにアクセスすると、退会確認ページが表示され、退会の最終確認を行います。

手順

  1. ルーティングの設定

    config/routes.rb に退会申請用のルートを追加します。

    ruby
    コードをコピーする
    Rails.application.routes.draw do
      # 退会確認ページ
      get 'users/withdraw', to: 'users#withdraw'
      # 退会処理
      delete 'users/withdraw', to: 'users#destroy'
    end
    
    
  2. コントローラの作成

    退会確認ページと退会処理を担当するアクションを作成します。

    class UsersController < ApplicationController
      before_action :require_login, only: [:withdraw, :destroy]
    
      def withdraw
        # 退会確認ページを表示
      end
    
      def destroy
        # ユーザーを論理削除
        current_user.update_attribute(:deleted_at, Time.current)
    
        # Sorceryのセッションを終了し、ホームページにリダイレクト
        logout
        redirect_to root_path, notice: '退会が完了しました。'
      end
    end
    
    

【参考】私の実際の実装

# app/controllers/users_controller.rb
  def destroy
    if current_user
      current_user.update(deleted_at: Time.current) # 退会処理
      logout # ログアウト処理
      redirect_to root_path, notice: '退会処理が完了しました。'
    else
      redirect_to root_path, alert: '退会処理に失敗しました。'
    end
  end
 

notice:alert: に関しては下記を参照

https://railsguides.jp/action_controller_overview.html#flash

【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説 - Qiita

deleted_atというカラムを追加し、その時刻を設定することで、データベースには残しつつ、通常のユーザーとしては扱わないようにできます。

rails g migration AddDeletedAtIndexToUsers
class AddDeletedAtIndexToUsers < ActiveRecord::Migration[7.1]
  def change
    add_index :users, :deleted_at
  end
end

  1. ビューの作成

退会確認ページのビューを作成します。

app/views/users/withdraw.html.erb

erb
<div class="withdraw-container">
  <div class="withdraw-wrapper">
    <h1>退会確認</h1>
    <p>本当に退会しますか? この操作は元に戻せません。</p>
    <div class="withdraw-botton-list d-flex justify-content-center gap-3">
      <%= button_to '退会する', users_withdraw_path, method: :delete, data: { turbo_confirm: '本当に退会しますか?' }, class: "btn btn-primary" %>
      <%= button_to 'キャンセル', root_path, class: "btn btn-danger" %>
    </div>
  </div>
</div>

退会ボタンをクリックしたときにアラートを表示して、誤操作を防ぐために、data: { confirm: '本当に退会しますか?' } を使用します。このオプションはJavaScriptによる確認ダイアログを表示します。

  1. 退会処理の保護

論理削除の場合、アプリケーション全体で、deleted_at が設定されているユーザーを対象外とするために、モデルにスコープを設定することが一般的です。

app/models/user.rb


class User < ApplicationRecord
  scope :active, -> { where(deleted_at: nil) }

  # デフォルトスコープとして使用する場合
  default_scope { active }
end

これで、User.active またはデフォルトスコープを使用することで、論理削除されたユーザーを自動的に除外できます。

補足

default_scope は、Rails のモデルにおいて、クエリが実行される際にデフォルトで適用される条件を設定するためのメソッドです。これを使うと、全てのクエリに対して特定の条件を自動的に追加できます。

default_scope の使い方

以下のように default_scope を定義します。

ruby
コードをコピーする
class User < ApplicationRecord
  # 'deleted_at' が nil のレコードのみを取得する
  default_scope { where(deleted_at: nil) }
end

この設定の効果

上記の設定を行うと、User モデルを使ってデータを取得する際に、自動的に deleted_atnil のレコード(論理削除されていないユーザー)だけが対象になります。

例えば、User.allUser.find(1) などの通常のクエリを実行した場合、deleted_atnil であるユーザーだけが結果に含まれます。

ruby
コードをコピーする
User.all
# SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL

default_scope のメリット

  • 一貫性: アプリケーション全体で論理削除されたデータを除外することが容易になります。
  • コードの簡略化: 毎回 where(deleted_at: nil) のような条件を書かなくても良くなります。

default_scope の注意点

  • 無効化が難しい: 特定のクエリで default_scope を無視したい場合、unscoped を使う必要がありますが、これが少し面倒です。

    ruby
    コードをコピーする
    User.unscoped.all
    # SELECT "users".* FROM "users"
    
    
  • 意図しない動作: 一部の操作(特に削除や更新)で、default_scope によって意図しないデータが除外されてしまう可能性があります。特に複雑なクエリを作成するときに影響を与えることがあります。

デフォルトでスコープを設定したいが、特定の操作では簡単に無効化したい場合は、明示的にスコープを定義して、それを使うようにする方が管理が楽です。

ruby
コードをコピーする
class User < ApplicationRecord
  scope :active, -> { where(deleted_at: nil) }
end

これにより、必要に応じてスコープを選択して使うことができ、柔軟性が高まります。

default_scope は便利ですが、使いどころに注意が必要です。

2. 退会済みユーザーのログイン制限

1. User モデルの修正

退会済みのユーザーをログインできないようにするため、User モデルにログイン制限のロジックを追加します。具体的には、deleted_at カラムが設定されている場合はログインを拒否するようにします。


class User < ApplicationRecord

  authenticates_with_sorcery!

  # ログイン可能なユーザーかどうかを判定
  def active_for_authentication?
    # Sorceryの標準のメソッドが利用可能であることを確認する
    if respond_to?(:super)
      super && deleted_at.nil?
    else
      # Sorceryが期待する標準的なメソッドがない場合のカスタム実装
      !deleted_at.present?
    end
  end
  
  # ログイン実行時に表示するメッセージを設定
  def inactive_message
    if deleted_at.present?
      :deleted_account
    else
      super
    end
  end
end

  • active_for_authentication? メソッドは、ユーザーがログインできるかどうかを判定します。deleted_at カラムが nil でない場合はログインを拒否します。
  • inactive_message メソッドは、ログインしようとしたときに表示されるメッセージを設定します。deleted_account はカスタムメッセージのキーです。

2. config/locales にエラーメッセージを追加

config/locales/ja.yml ファイルに、deleted_account メッセージの翻訳を追加します。

yaml
コードをコピーする
ja:
  sorcery:
    session:
      deleted_account: "このアカウントは退会済みです。再登録が必要です。"

  1. ApplicationController の確認

ApplicationControllercurrent_user メソッドが正しく実装されているか確認します。このメソッドは、ログインしているユーザーを返す役割を果たします。

ruby
コードをコピーする
class ApplicationController < ActionController::Base
  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
end

データベース容量を減らす為には物理削除の方が良いかと思います。どちらを選ぶかはサービスの趣旨に合わせてご検討ください

Discussion