Discard gem入門 - 論理削除を正しく理解しよう!
はじめに
Railsを使用してWebアプリケーションを開発していると、「データを削除したいが、完全に削除するのは怖い」というケースはありません?ユーザーが誤って削除ボタンを押してしまった場合や、後で削除したデータを復元する必要が生じた場合など、物理的にデータベースからレコードを削除してしまうと取り返しがつきません。
そこで登場するのが「論理削除」という概念です。本記事では、論理削除の基本概念から、Railsで論理削除を簡単に実装できるDiscard gemの使い方まで、初学者の方にもわかりやすく解説していきます。
論理削除とは何か
物理削除と論理削除の違い
まず、データの削除方法には大きく分けて2つの方法があります。
物理削除(Physical Delete)
# データベースからレコードを完全に削除
user.destroy
# DELETE FROM users WHERE id = 1
論理削除(Logical Delete)
# レコードは残したまま、削除フラグを立てる
user.update(deleted_at: Time.current)
# UPDATE users SET deleted_at = '2025-01-15 10:30:00' WHERE id = 1
なぜ論理削除が必要なのか
論理削除が必要な理由は以下の通りです:
- データの復元が可能: 誤って削除されたデータを復元できる
- 監査証跡の保持: いつ、誰がデータを削除したかの記録を残せる
- 関連データの整合性: 他のテーブルから参照されているデータを安全に「削除」できる
- 法的要件への対応: 一定期間データを保持する必要がある場合
論理削除の基本的な実装方法
従来の手動実装
Discard gemを使わない場合、以下のような実装が一般的でした:
class User < ApplicationRecord
# deleted_flag カラム(boolean)を使用
scope :active, -> { where(deleted_flag: false) }
scope :deleted, -> { where(deleted_flag: true) }
def soft_delete
update(deleted_flag: true)
end
def restore
update(deleted_flag: false)
end
end
# 使用例
User.active.all # 削除されていないユーザーのみ取得
user.soft_delete # ユーザーを論理削除
従来実装の問題点
- 実装の複雑さ: 毎回スコープを意識してクエリを書く必要がある
- 忘れやすさ: 削除チェックを忘れて削除済みデータを取得してしまう
- 一貫性の欠如: プロジェクト内で実装方法がバラバラになりがち
- 削除日時の管理: boolean フラグでは「いつ削除されたか」がわからない
Discard gemの紹介
Discard gemとは
Discard gemは、Railsアプリケーションで論理削除を簡単に実装できるgemです。Active Recordモデルに論理削除機能を追加し、一貫性のある方法で削除されたレコードを管理できます。
インストールと基本設定
Gemfileに追加
gem 'discard', '~> 1.2'
マイグレーションファイルの作成
class AddDeletedAtToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :deleted_at, :datetime
add_index :users, :deleted_at
end
end
モデルでの設定
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
end
主要な機能の概要
Discard gemは以下の機能を提供します:
-
自動的なスコープ生成:
kept
、discarded
、with_discarded
スコープが自動生成される -
簡単な削除・復元:
discard
、undiscard
メソッドでレコードの削除・復元が可能 -
柔軟なカラム設定: デフォルトでは
deleted_at
だが、任意のカラム名を指定可能
Discard gemの基本的な使い方
モデルの設定方法
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
# その他のモデル定義
validates :name, presence: true
has_many :posts, dependent: :destroy
end
レコードの削除(discard)
user = User.find(1)
# 論理削除の実行
user.discard
# => UPDATE users SET deleted_at = '2025-01-15 10:30:00' WHERE id = 1
# 削除状態の確認
user.discarded? # => true
user.kept? # => false
削除の取り消し(undiscard)
# 論理削除されたユーザーを復元
user.undiscard
# => UPDATE users SET deleted_at = NULL WHERE id = 1
# 復元状態の確認
user.discarded? # => false
user.kept? # => true
基本的なスコープの使い方
Discard gemは以下のスコープを自動的に提供します:
# 削除されていないレコードのみ取得
User.kept.all
# => SELECT * FROM users WHERE deleted_at IS NULL
# 削除されたレコードのみ取得
User.discarded.all
# => SELECT * FROM users WHERE deleted_at IS NOT NULL
# 削除状態に関係なく全レコードを取得
User.with_discarded.all
# => SELECT * FROM users
# 削除されたレコードも含めて特定のレコードを検索
User.with_discarded.find(1)
kept スコープとdefault_scopeの理解
kept スコープの役割
kept
スコープは、論理削除されていないレコード(deleted_at
がNULL
のレコード)のみを取得するためのスコープです。
# 以下は同じ意味
User.kept.all
User.where(deleted_at: nil).all
default_scope -> { kept } の意味と効果
default_scope
を使用することで、そのモデルに対するすべてのクエリに自動的に条件を適用できます:
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
# すべてのクエリで削除されていないレコードのみを対象とする
default_scope -> { kept }
end
これにより、以下のような効果が得られます:
# 通常のクエリでも、自動的にdeleted_atがNULLの条件が追加される
User.all
# => SELECT * FROM users WHERE deleted_at IS NULL
User.where(active: true)
# => SELECT * FROM users WHERE deleted_at IS NULL AND active = true
User.find(1)
# => SELECT * FROM users WHERE deleted_at IS NULL AND id = 1
実際のSQLクエリでの動作確認
実際にRailsコンソールで動作を確認してみましょう:
# モデルの定義
class Product < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
default_scope -> { kept }
end
# 商品を作成
product1 = Product.create(name: "商品A", price: 1000)
product2 = Product.create(name: "商品B", price: 2000)
# 1つの商品を論理削除
product1.discard
# 通常のクエリ(削除されていない商品のみ取得)
Product.all.to_sql
# => "SELECT \"products\".* FROM \"products\" WHERE \"products\".\"deleted_at\" IS NULL"
# 削除された商品も含めて取得
Product.with_discarded.all.to_sql
# => "SELECT \"products\".* FROM \"products\""
実践的な使い方とベストプラクティス
関連モデルとの連携
論理削除を使用する際は、関連モデルとの整合性も考慮する必要があります:
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
end
class Post < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
belongs_to :user
has_many :comments, dependent: :destroy
# ユーザーが削除されていない投稿のみを取得するスコープ
scope :from_active_users, -> { joins(:user).merge(User.kept) }
end
バリデーションとの組み合わせ
論理削除されたレコードに対してもバリデーションが実行されることがあります:
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
validates :email, presence: true, uniqueness: true
# 削除されたユーザーのemailは一意性制約から除外
validates :email, uniqueness: { conditions: -> { kept } }
end
パフォーマンスの考慮事項
論理削除を使用する際のパフォーマンス最適化:
# deleted_atカラムにインデックスを追加
class AddIndexToDeletedAt < ActiveRecord::Migration[7.0]
def change
add_index :users, :deleted_at
# 複合インデックスも考慮
add_index :users, [:deleted_at, :created_at]
end
end
# 大量のデータがある場合は、削除されたレコードを定期的にアーカイブ
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
# 一定期間経過した削除レコードを物理削除
scope :old_discarded, -> { discarded.where('deleted_at < ?', 1.year.ago) }
end
注意点とよくあるトラブル
default_scopeの副作用
default_scope
は便利ですが、予期しない動作を引き起こすことがあります:
class User < ApplicationRecord
include Discard::Model
self.discard_column = :deleted_at
default_scope -> { kept }
end
# 以下のクエリでも、削除されていないユーザーのみがカウントされる
User.count # 削除されたユーザーは含まれない
# 全ユーザー数を取得したい場合
User.with_discarded.count # 削除されたユーザーも含む
削除されたレコードが必要な場合の対処法
削除されたレコードにアクセスする必要がある場合:
# 削除されたユーザーを含めて検索
user = User.with_discarded.find(params[:id])
# 削除されたユーザーのデータを表示する場合の条件分岐
if user.discarded?
render :show_deleted_user
else
render :show_user
end
テストでの注意点
テストコードでも論理削除を考慮する必要があります:
RSpec.describe User, type: :model do
describe '#discard' do
let(:user) { create(:user) }
it 'ユーザーが論理削除されること' do
expect { user.discard }.to change { user.reload.discarded? }.from(false).to(true)
end
it '論理削除されたユーザーは通常のクエリで取得されないこと' do
user.discard
expect(User.all).not_to include(user)
expect(User.with_discarded.all).to include(user)
end
end
end
まとめ
Discard gemを使うメリット
- 簡単な実装: 数行のコードで論理削除機能を追加できる
- 一貫性: プロジェクト全体で統一された論理削除の実装が可能
- 豊富な機能: スコープ、メソッドが自動的に提供される
- 保守性: 標準的な実装により、コードの保守が容易
導入時の検討事項
- パフォーマンス: 大量のデータがある場合はインデックス設計を検討
- データ設計: 削除されたデータの保持期間やアーカイブ戦略を決定
- 既存システムとの互換性: 既存の削除ロジックとの整合性を確認
- チーム内での共有: 論理削除の概念と実装をチーム全体で理解
Discard gemを使用することで、安全で保守しやすい論理削除機能をRailsアプリケーションに簡単に導入できます。適切に活用して、より堅牢なアプリケーションを構築しましょう。
Discussion