Rails で論理削除を簡単に実装できる paranoia の使用方法です。論理削除自体の是非には触れません。

paranoiaのインストール

Gemfileに以下を追加してbundle installを実行してください。

gem paranoia

paranoiaの使用方法

論理削除を実装したいモデルにdeleted_atカラムを追加します。

$ ./bin/rails g model user name age:integer deleted_at:datetime
$ ./bin/rails db:migrate

モデルファイルにacts_as_paranoidの一文を記述してください。

class User < ApplicationRecord
  acts_as_paranoid
end

以上で準備は完了です。実際に試してみます。

# 通常の where によるクエリですが条件に deleted_at が追加されています。
> User.where(age > ?, 20)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."deleted_at" IS NULL AND (age > 20)
#=> #<ActiveRecord::Relation [#<User id: 1, name: "Jonh", age: 30, deleted_at: nil, created_at: "2017-08-13 08:35:19", updated_at: "2017-08-13 08:37:33">]>

# destroy を実行すると物理削除ではなく論理削除になります。
> user = User.find(1)
> user.destroy
  SQL (1.3ms)  UPDATE "users" SET "deleted_at" = 2017-08-13 08:36:04.864701, "updated_at" = 2017-08-13 08:36:04.864875 WHERE "users"."id" = ?  [["id", 1]]
> user.deleted_at
=> Sun, 13 Aug 2017 08:36:04 UTC +00:00

# 論理削除後に検索しても取得されません。
> User.where(age > ?, 20)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."deleted_at" IS NULL AND (age > 20)
=> #<ActiveRecord::Relation []>

# really_destroy! を実行すると従来の物理削除クエリが発行されます。
> user.really_destroy!
  SQL (1.5ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]

以下は状況に応じた使用方法です。

論理削除カラム名の変更

paranoia のデフォルトではdeleted_atが論理削除のカラム名として使われます。違うカラム名を使用したい場合はcolumnオプションで指定して下さい。

class User < ApplicationRecord
  acts_as_paranoid column: :destroyed_at
end

デフォルトスコープの追加をオフ

論理削除は使いたいがクエリのデフォルトスコープではオフにしておきたい場合もあります。without_default_scopetrueにしましょう。

class User < ApplicationRecord
  acts_as_paranoid without_default_scope: true
end

クエリの検索を実行してもdeleted_atが条件に含まれません。

> User.where(age > ?, 20)
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE (age > 20)

オフになるのは検索時なのでdestroyを実行した場合は論理削除になります。

> user.destroy
   (0.1ms)  begin transaction
  SQL (0.4ms)  UPDATE "users" SET "deleted_at" = 2017-08-13 09:18:06.256660, "updated_at" = 2017-08-13 09:18:06.256829 WHERE "users"."id" = ?  [["id", 2]]

削除済みのレコードも検索

User.allを実行するとdeleted_at IS NULLの条件が含まれますが、論理削除済みのレコードを取得したい場合はwith_deletedで検索します。without_deletedwithout_default_scope: trueを指定している場合にUser.alldeleted_atを参照しなくなるので逆に論理削除を除きたいときに使用します。

> User.all
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."deleted_at" IS NULL
> User.with_deleted
  User Load (0.1ms)  SELECT  "users".* FROM "users"
> User.without_deleted
  User Load (2.0ms)  SELECT  "users".* FROM "users" WHERE "users"."deleted_at" IS NULL

allだけでなく、whereなどのクエリに付けることも可能です。

> User.where('age > ?', 20).with_deleted
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE (age > 20)

削除済みのレコードのみを検索したければonly_deletedを使用します。

> User.only_deleted
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE ("users"."deleted_at" IS NOT NULL)

論理削除済みかどうかの判定

deleted?またはparanoia_destroyed?で削除済みレコードか否かを判定できます。

> user = User.only_deleted.first
=> #<User id: 2, name: "Jonh", age: 30, deleted_at: "2017-08-13 09:18:06", created_at: "2017-08-13 09:04:15", updated_at: "2017-08-13 09:18:06">
> user.deleted?
=> true
> user.paranoia_destroyed?
=> true

論理削除から復旧させる

論理削除されたレコードを元に戻すにはrestoreを使用します。復数のIDを配列で指定する場合は、存在しなければActiveRecord::RecordNotFoundが発生するので気を付けましょう。

> user.restore
  SQL (0.4ms)  UPDATE "users" SET "deleted_at" = NULL, "updated_at" = 2017-08-13 09:44:52.206675 WHERE "users"."id" = ?  [["id", 2]]
> User.restore(2)  # 配列で復数のID指定も可
  SQL (0.3ms)  UPDATE "users" SET "deleted_at" = NULL, "updated_at" = 2017-08-13 09:45:22.548918 WHERE "users"."id" = ?  [["id", 2]]