この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。
0.前提条件
X(旧Twitter)のクローンサイトの制作過程で退会機能を実装した時に内容を修正した過程についてご紹介したいと思います。
フロントエンド:React(JavaScript) 19.1.0
バックエンド:Ruby on Rails(Ruby) 7.0.0 APIモード
インフラ:Docker
PC;Mac Book Air M2チップ
1.実装概要
アカウント登録済みのWebサービスから退会(利用停止)するための機能を実装します。
- 退会対象アカウントは物理削除ではなく、論理削除
- 論理削除機能はdiscardのgemを使って実装する
- 退会後はWebサービスを利用できないように、退会 → サインアウトの順番で実行する
X(旧Twitter)本家ではユーザーが退会すると、1ヶ月間は論理削除としてデータ復活可能な期間が設けられており、その後に物理削除で完全に消去するようです。
論理削除期間中も対象のユーザーデータは閲覧できないようです。
2.実装内容
discardのgemをインストールしていきます。
gem 'discard', '~> 1.4'
Dockerを利用していたので、下記のコマンドでインストールします。apiはサービス名
docker compose exec api bundle install
class AddDiscardedAtToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :discarded_at, :datetime
add_index :users, :discarded_at
end
end
userモデルのカラムにdiscarded_atを加えており、論理削除の日時を意味しています。
これで論理削除の準備が整いました。
後述するuserモデルに関わるモデルにも同じようにdiscarded_atカラムを追加していますが、内容は同じなので掲載・説明は割愛します。
users_controllerにdiscarを使った論理削除メソッドを追加しました。
def discard
if current_api_v1_user.discard
render json: { status: 'SUCCESS', message: 'have soft-deleted user successfully' }
else
render json: { status: 'ERROR', message: 'User not soft-deleted' }
end
end
以下はuserのモデルファイルに実装したコードを抜粋しています。
# discard(gem)を使用できるように設定
include Discard::Model
# デフォルトの取得内容を変更。これによりdiscardで論理削除されたデータは含まない。
default_scope -> { kept }
# ログインしてくるユーザーが論理削除されていないことを確認
# これにより論理削除されたユーザーはログインできない
def active_for_authentication?
super && kept?
end
# groupモデルを除いてユーザーが論理削除されたら関連するデータも論理削除する
after_discard do
passive_notifications.discard_all
active_notifications.discard_all
relations.discard_all
entries.discard_all
bookmarks.discard_all
comments.discard_all
favorites.discard_all
messages.discard_all
retweets.discard_all
tweets.discard_all
end
default_scopeはデータベースから値を取得する時の範囲で、keptはdiscardのメソッドであり、論理削除したデータを対象から外す意味になります。
active_for_notification?はdevise_token_authのメソッドでログインユーザーを認証する時に動きます。既存の内容に加えてkeptかどうか(論理削除されていこと)をログインの条件にしています。
after_discardブロックではdiscardがuserモデルで実行された後に他の関連するモデルも削除するように実装しています。ユーザーだけ論理削除して投稿などの対象ユーザーに関わるデータが残っていると正しく表示できずにエラーの原因となるからです。
groupモデルは対象から外していますが、これは複数のユーザーが属するデータなので、片方のユーザーが論理削除されただけでグループを削除するともう片方のユーザーに影響がでるので残すようにしています。
# discard(gem)を使用できるように設定
include Discard::Model
# デフォルトの取得内容を変更。これによりdiscardで論理削除されたデータは含まない。
default_scope -> { kept }
# ユーザーデータを取得する時に論理削除されたユーザーを含まない
scope :from_active_users, -> { joins(:user).merge(User.kept) }
関連するモデルの代表でtweetモデルの実装内容を紹介します。他は同じなので割愛します。
上の2行はuserモデルと同じなので解説は割愛します。
最後の行ではuserを検索するクエリ内容を指定することで、論理削除されたユーザーを省くようにしています。

devise_token_authのサインアウト機能をそのまま使っていると論理削除されたユーザーは対象に含まれないので、エラーが発生してしまいます。
というのも、userモデル側で論理削除されたユーザーは省くように実装しているからです。
Started DELETE "/api/v1/auth/sign_out" for 192.168.65.1 at 2025-10-07 12:46:22 +0000
Processing by DeviseTokenAuth: :SessionsController#destroy as HTML
User Load (36.7ms) SELECT "users" * FROM "users" WHERE "users" "discarded_at" IS NULL AND "users" "uid" = $1
LIMIT $2 [l"uid", "guy@sample.com"],
["LIMIT"
', 111
Completed 404 Not Found in 69ms (Views: 0.2ms | ActiveRecord: 36.7ms | Allocations: 1266)
WHERE "users" "discarded_at" IS NULLのクエリ条件が肝で、論理削除したユーザーを含まないクエリ検索になってしまっているのがエラーの原因です。
これを受けて、サインアウトを管理するsessions_contorller.rbのdestroyメソッドを上書きすることにしました。
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'api/v1/sessions'
}
ルーティングで個別に作成するsessions_contollerを使えるように記述。
※この記述がないと、devise_token_authに組み込まれたdestroyメソッドを使うことになります。
module Api
module V1
class SessionsController < DeviseTokenAuth::SessionsController
def destroy
client_id = request.headers['client']
uid = request.headers['uid']
request.headers['access-token']
# 論理削除されたユーザーも含むようにwith_discardedを加える
@user = User.with_discarded.find_by(uid:)
# トークンはclien_idごとに管理されているので、client_idをキーとしてトークンを探す
if @user && @user.tokens[client_id]
# deleteすることでトークン情報のみを全て削除できる
@user.tokens.delete(client_id)
# トークン情報を削除した状態で保存
@user.save!
render json: { status: 'SUCCESS', message: 'have logged out successfully' }
else
render json: { status: 'ERROR', message: 'User not found' }
end
end
end
end
end
destroyメソッドの中でインスタンス変数@userに対象のユーザーを格納していますが、with_discardedを使うことで論理削除されたユーザーも含んだユーザー群から検索するように実装しています。
// 退会後にサインアウトしてローカルストレージを空にする
const handleDiscardAccount = async () => {
// 退会処理としてユーザーを論理削除する
const response1 = await axiosInstance.delete("/users");
console.log(response1.data);
close();
// 論理削除したユーザーをサインアウトさせる
const response2 = await axiosInstance.delete("/auth/sign_out");
console.log(response2.data);
localStorage.clear();
navigate("/");
};
実際に退会処理する場合、ユーザーを論理削除して終わりではなく、サインアウトまで連続して行うように実装しました。実サービスで退会したユーザーが残ってサービスを利用するは好ましくないので、サインアウトまで完了させるようにしています。
以上がユーザー退会機能の実装内容となります。
最後まで読んで頂きありがとうございました。
参考にしたサイト
discard
devise-token-authを用いたログイン・ログアウト機能の作成②
Rails で Devise と論理削除を両立する方法
Railsで論理削除(soft delete)を実装する(discard gem利用)
railsで退会処理を実装する際の注意(discard gem採用