Rails

【Rails】論理削除を簡単に実装することができるgem「discard」の使い方についてまとめてみた

Rails

 

今回はRailsで論理削除機能を簡単に実装できるGem、discrardの使い方について簡単にまとめてみました。論理削除はDBから実際に削除するわけではないため、ユーザーの退会処理などデータの復元の可能性がある場合に有効です。

Image from Gyazo

論理削除とは

論理削除とは、削除されたというフラグを対象データに付与し、あたかも削除されたかのような振る舞いをする削除方法です。論理削除は実際にはDBから削除されるわけではないため、データを保管したり、誤って削除してしまったデータの復元が可能になります。

論理削除と対照的な削除として、物理削除があります。物理削除はDBからそのままデータを削除することになります。destoryやdeleteメソッドが物理削除に該当します。

Gem「discard」とは

discardは論理削除を簡単に実装することができるGemです。

Soft deletes for ActiveRecord done right. A simple ActiveRecord mixin to add conventions for flagging records as discarded.

参照: discard

Gem「discard」の使い方

簡易的なアプリケーションを作成しながらdiscardの使い方について見ていきます。

アプリケーションの作成

% rails new discard-app -d mysql
% rails db:create

今回のアプリケーションの概要は以下になります。

  • アプリケーション名: discard-app
  • Ruby: 2.6.5
  • Rails: 6.0.4.1
  • DB: MySQL

discardの導入

gem 'discard', '~> 1.2'
% bundle install

Postモデルの作成

% rails g model post title:string
% rails db:migrate

Postモデルにdiscarded_atカラムを追加

Postモデルに論理削除された日時が保存される、discarded_atカラムをdatetime型で追加します。その後、マイグレートを実行しましょう。

% rails generate migration add_discarded_at_to_posts discarded_at:datetime:index
% rails db:migrate

作成されたマイグレーションファイル

class AddDiscardedAtToPosts < ActiveRecord::Migration[6.0]
  def change
    add_column :posts, :discarded_at, :datetime
    add_index :posts, :discarded_at
  end
end


マイグレート後のスキーマファイル

ActiveRecord::Schema.define(version: 2021_10_03_103659) do

  create_table "posts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
    t.string "title"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.datetime "discarded_at"
    t.index ["discarded_at"], name: "index_posts_on_discarded_at"
  end

end

precisionオプションの追加

先ほど作成したdiscarded_atカラムにprecisionオプションを付与します。

class AddDiscardedAtToPosts < ActiveRecord::Migration[6.0]
  def change
    add_column :posts, :discarded_at, :datetime, precision: 6
    add_index :posts, :discarded_at
  end
end
% rails db:migrate:reset
Rails
【Rails】Rails6からtimestampsにデフォルトで追加されたprecisionオプションまとめRails6からtimestampsにデフォルトで追加されるようになったオプション、precisionオプションについて簡単にまとめたので紹介しています。より細かくデータを保管したい、日時データの精度を高めたいといった場合はprecisionオプションの付与はマストにした方が良さそうです。...

マイグレート後のスキーマファイル

ActiveRecord::Schema.define(version: 2021_10_03_104454) do

  create_table "posts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
    t.string "title"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.datetime "discarded_at", precision: 6
    t.index ["discarded_at"], name: "index_posts_on_discarded_at"
  end

end

seed_fuを用いたデータ投入

続いてseed_fuを用いてPostモデルにデータを投入します。

gem 'seed-fu'
% bundle install
% mkdir db/fixtures
% mkdir db/fixtures/development
% touch db/fixtures/development/01_post.rb
# db/fixtures/development/01_post.rb

Post.seed do |s|
  s.id    = 1
  s.title = 'おはよう'
end

Post.seed do |s|
  s.id    = 2
  s.title = 'こんにちは'
end

Post.seed do |s|
  s.id    = 3
  s.title = 'こんばんは'
end

Post.seed do |s|
  s.id    = 4
  s.title = 'おやすみなさい'
end
% rails db:seed_fu
Rails
【Rails】初期データの追加や管理が楽にできるseed_fuの使い方についてまとめてみたseed_fuを用いた初期データの投入方法について解説しています。seed_fuを用いることで、初期データの追加や管理がかなり便利になるため、seeds.rbで初期データを管理しているアプリケーションがあればseed_fuに移行してみてはいかがでしょうか。...

Postモデルにdiscardできることを宣言

次にPostモデルにdiscardできることを宣言します。

class Post < ApplicationRecord
  include Discard::Model
end

コンソール上で論理削除とその復元

ここまででdiscardを用いた論理削除が可能になりました。

試しにコンソール上で論理削除を実行してみます。

% rails c

seed_fuで作成したデータが4件あることを確認
> Post.all.count
# => 4

論理削除実行
> Post.first.discard
# => true

discarded_atに論理削除された日時が保存される
> Post.first
# => #<Post id: 1, title: "おはよう", created_at: "2021-10-05 06:09:48", updated_at: "2021-10-05 06:31:54", discarded_at: "2021-10-05 06:31:54">

論理削除された日時
> Post.first.discarded_at
# => Tue, 05 Oct 2021 06:31:54 UTC +00:00

論理削除が実行されたか確認
> Post.first.discarded?
# => true

論理削除されたレコード一覧を返す
> Post.discarded
# => #<ActiveRecord::Relation [#<Post id: 1, title: "おはよう", created_at: "2021-10-05 06:09:48", updated_at: "2021-10-05 06:31:54", discarded_at: "2021-10-05 06:31:54">]>

実際にDBから削除されたわけではないので4が返る
> Post.all.count
# => 4

既に論理削除済みのためfalseが返る
> Post.first.discard
# => false

論理削除失敗時に例外を発生
Post.first.discard!
# => Discard::RecordNotDiscarded (Failed to discard the record)

上記で実行した論理削除に関するメソッド

  • 論理削除を実行: discard
  • 論理削除が実行されたかをtrueもしくはfalseで返す: discarded?
  • 論理削除されたレコード一覧を返す: discarded
  • 論理削除の失敗時に例外を発生: discard!

 

続いて先ほど論理削除したレコード(Post.first)を復元してみます。

論理削除されたレコードを復元
> Post.first.undiscard
# => true

論理削除されていないかチェック
> Post.first.undiscarded?
# => true

復元済みのためnilが返る
> Post.first.undiscard
# => nil

復元失敗時に例外を発生
> Post.first.undiscard!
# => Discard::RecordNotUndiscarded (Failed to undiscard the record)

論理削除されていないレコードを一覧で返す
> Post.undiscarded
# => #<ActiveRecord::Relation [#<Post id: 1, title: "おはよう", created_at: "2021-10-05 06:09:48", updated_at: "2021-10-05 07:46:23", discarded_at: nil>, #<Post id: 2, title: "こんにちは", created_at: "2021-10-05 06:09:48", updated_at: "2021-10-05 06:09:48", discarded_at: nil>, #<Post id: 3, title: "こんばんは", created_at: "2021-10-05 06:09:48", updated_at: "2021-10-05 06:09:48", discarded_at: nil>, #<Post id: 4, title: "おやすみなさい", created_at: "2021-10-05 06:09:48", updated_at: "2021-10-05 06:09:48", discarded_at: nil>]>

上記で実行した論理削除に関するメソッド

  • 論理削除されたレコードを復元: undiscard
  • 復元されたかをtrueもしくはfalseで返す: undiscarded?
  • 復元失敗時に例外を発生: undiscard!
  • 論理削除されていないレコード一覧を返す: undiscarded

コントローラー上で論理削除とその復元

先ほどはコンソール上で論理削除とその復元方法を試しました。

コントローラー上で論理削除とその復元を実行する際は以下のように記述します。

class PostsController < ApplicationController
  def update
    @post.undiscard
    redirect_to users_url, notice: "Post undiscarded"
  end

  def destroy
    @post.discard
    redirect_to users_url, notice: "Post removed"
  end
end

 

ここまでがGem「discard」の基本的な使い方になります。

論理削除されたデータを返さない

Post.allでは論理削除されたレコードも返すことを前項で確認しました。

> Post.all.count
# => 4

> Post.first.discard
# => true

> Post.all.count
# => 4

 

しかし実際には論理削除されたものは返したくないことが大半です。その場合、モデルにdefault_scopeを追加することで解決できます。

class Post < ApplicationRecord
  include Discard::Model
  default_scope -> { kept }
end
> Post.all.count
# => 4

> Post.first.discard
# => true

論理削除されたものを返さない
> Post.all.count
# => 3

 

default_scopeを定義し、論理削除されたものも全て取得したい場合、以下のようにwith_discardedをチェーンさせることで取得が可能になります。

> Post.with_discarded.count
# => 4

default_scopeを不用意に使うとメンテ性が悪くなってしまう恐れがあります。使用する際は、十分に検討してから導入しましょう。

参考: Railsのdefault_scopeをどうしても使いたい時

まとめ

  • 論理削除とは、削除されたというフラグを対象データに付与し、あたかも削除されたかのような振る舞いをする削除方法のこと
  • 物理削除とは、DBからそのままデータを削除すること
  • discardは論理削除を簡単に実装することができるGem
  • 論理削除を実行するモデルには、discarded_atカラムをdatetime型で追加する
  • 論理削除されたものを返さない場合、モデルにdefault_scopeを追加する

参考

 

 

今回はRailsで論理削除機能を簡単に実装できるGem、discrardの使い方について簡単にまとめてみました。ユーザーの退会処理など物理処理ではなく論理削除がふさわしい場合があるため、そういった場合は導入を検討してみてください。