「paranoia」で論理削除を実装する(Rails)

f:id:ryoutaku_jo:20190628235237p:plain

【結論】

・論理削除とは、データベースからデータそのものは消さず、削除フラグでデータが消えていることを表現する削除方法

・「paranoia」とは、Railsのgemの一種で、論理削除を簡単に実装できる

・関連モデルの論理削除/復元にも対応している

【目次】

【本題】

paranoiaについて

paranoiaとは、論理削除を簡単に実装できるgemです。

論理削除とは、データの削除を行った際に、データベースからデータそのものは消さず、削除フラグでデータが消えていることを表現する削除方法です。データは残っているので、簡単にデータを復元できるメリットがあります。

paranoiaによる論理削除を適用させたテーブルのレコードは、destroyを呼び出しても、レコードはデータベースから削除されませんが、表示されなくなります。

仕組みとしては、deleted_atというカラムを対象のテーブルに持たせて、削除時に日時を記録します。

そして、すべてのクエリにdeleted_atを持たないレコードのみを含める様にスコープを設定することで、削除後は検索できなくなります。

restoreメソッドを利用することで、deleted_atの情報が削除され、簡単に復元できます。

なお、特定のメソッドを利用することで、deleted_atを持つレコードも検索可能です。

また、dependent: :destroyで関連モデルのレコードも同時に削除される様になっている場合、関連モデルにもparanoiaを適用させることで、そちらも論理削除が行えます。さらに、復元も連動させる事が可能です。

gemの導入

まず、gemを導入します。Gemfileに以下を記述して、bundle installします。

gem 'paranoia'

なお、Rails3系であれば、バージョン1を使用する必要があります。

gem "paranoia", "~> 1.0"

Rails4系・5系であれば、バージョン2を使用します(Rails5系は、2.2以上)

gem "paranoia", "~> 2.2"

カラムの追加とマイグレーション

次にparanoiaを適用させるテーブルにdeleted_atカラムを追加します。

deleted_atは、datetime型で作成し、頻繁に参照するのでindexを貼っておきます。

bin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index
class AddDeletedAtToClients < ActiveRecord::Migration
  def change
    add_column :clients, :deleted_at, :datetime
    add_index :clients, :deleted_at
  end
end

こちらの内容でマイグレーションを行います。

モデルにacts_as_paranoidを追記

対象モデルにacts_as_paranoidを追記します。

class Client < ActiveRecord::Base
  acts_as_paranoid

  # ...
end

以上で、最低限の設定は完了です。これで論理削除が利用できる様になりました。

なお、deleted_at以外のカラムを使用したい場合は、それをオプションとして渡すことができます。

class Client < ActiveRecord::Base
  acts_as_paranoid column: :destroyed_at

  ...
end

また、クエリのデフォルトスコープにdeleted_atが入ってしまうのを止めたい場合は、以下の記述を追加します。

class Client < ActiveRecord::Base
  acts_as_paranoid without_default_scope: true

  ...
end

これにより、通常の検索時にはdeleted_atが条件に追加されなくなります。なお、オフになるのは検索時のみなのでdestroyを実行した場合は論理削除になります。

使用方法(削除、復元)

使用方法は至ってシンプルで、論理削除したいインスタンスに対して、destroyを使用するだけです。

これによってデータベースからレコードが消えることはありませんが、deleted_atに日時が記録され、普通に発行されたクエリでは、該当レコードが検索できなくなります。

>> client.deleted_at
# => nil
>> client.destroy
# => client
>> client.deleted_at
# => [current timestamp]

復元する場合はrestoreメソッドを利用します。復元はインスタンスに対してメソッドを使用するか、引数にidを指定してモデルオブジェクトに対してメソッドを使用する方法があります。

Client.restore(id)

client.restore

使用方法(削除データの検索)

論理削除したデータを検索したい場合は、with_deleted``only_deletedメソッドを利用します。

# 削除したデータを含めて
Client.with_deleted

# 削除したデータのみ
Client.only_deleted

これらはall``find``whereなどのクエリに付けることも可能です。

Client.all.with_deleted

without_default_scope: trueを利用している場合、allメソッドなどを使うと削除済みのデータもヒットしてしまいますが、削除済みデータを表示させたく無い時はwithout_deletedメソッドで対応できます。

Client.all.without_deleted

削除済みのデータか判定したい場合は、paranoia_destroyed?``deletedメソッドが有効です。

client.paranoia_destroyed?

client.deleted?

使用方法(関連モデルの論理削除/復元)

子モデルにdependent: :destroyを設定して、親モデルのレコードが削除された時に、子モデルも削除される様にしているケースも存在します。

この場合、何も設定していないと、子モデルは物理削除されてしまいます。

子モデルでも論理削除を適用させたい場合は、子モデルにもacts_as_paranoidを記述し、deleted_atカラムも追加する必要があります。

なおacts_as_paranoidを記述しただけ(deleted_atカラムを追加しない)では、マイグレーションでエラーが発生します。

rails aborted!                                                                                                                                                                                                                                                                   
ActiveModel::UnknownAttributeError: unknown attribute 'deleted_at' for ChildrenModelName.

論理削除する場合は、親モデルにdestroyを使用するだけで変わりありません。

なお、親モデルを復元した際に、子モデルを復元させる場合は、recursive: trueオプションを設定する必要があります。

Client.restore(id, recursive: true)

client.restore(recursive: true)

孫モデルが存在する場合も、同様です。

参考情報

GitHub - rubysherpas/paranoia: acts_as_paranoid for Rails 3, 4 and 5

【Rails】論理削除を実装するGemのparanoiaについて - TASK NOTES

【Rails】paranoiaで論理削除を実装する | RemoNote

《今日の学習進捗(3年以内に10000時間に向けて)》

ユーザー削除機能(論理削除)の実装を進める中で、冗長な箇所が多くなってしまい、リファクタリングに非常に苦慮した。

既存コードを汎用化させようとすると可変機能が正常に動作しなくなったりと、やはり開発初期より考慮すべき箇所が多くなっている事が、開発の難易度を高めていると考えている。

いくつかの実装パターンは思いつくが、どれも自分の中では一長一短あり、どの方法で実装すべきか判断に迷う場面に度々遭遇した。デザインパターンについて理解を深めていく段階なのだと感じている。

機能を追加するごとにアクションやビューファイルが増え、ビューの中身もIF文だらけで非常に気持ちが悪い・・・早くなんとかしたい・・・

また、一度煮詰まると、そこからの生産性は大抵最悪なので、諦めて切り上げる妥協も必要だと改めて感じた。

学習開始からの期間 :203日
今日までの合計時間:1976h
一日あたりの平均学習時間:9.8h
今日までに到達すべき目標時間:1854h
目標との解離:122h
「10,000時間」まで、

残り・・・「8024時間!」