こんにちは、Tochiです。 Railsの論理削除といえば、1社目ではdeletedカラムを追加してupdate処理で管理していました。 しかし世の中にはDiscardなる論理削除を簡単に管理できるgemがあると知りました。その中で、Discardができた背景がとても面白かったので歴史を紐解きながら内部実装を見ていきたいなと思います。
そもそも論理削除って??
「論理削除」とは、あるアイテムを即座に完全に削除する物理削除と異なり、 アイテムを保留場所に移動し削除されているように見せるアプローチを取るものです。 保留場所に移動したアイテムは、物理削除まえに一定期間保持され、手動または自動で削除されるものです。 (もちろん、永続的に保持しておくことも可能です)
論理削除であれば実際にはデータベースから削除されることはなく、復元も可能です。 メールのゴミ箱なども、いわゆる論理削除にあたります
論理削除はどのように実装される?
一般的なアプローチとしては、deleted_at/discarded_atのタイムスタンプ・カラムを個別に追跡します。論理削除されたレコードはそうでないレコードと同じテーブルに保持し、スコープクエリで区別することができます。
user = User.where(deleted_at: nil)
Discardより少し前に登場したParanoia
Discardより少し前の時代には、
Paranoiaというgemが流行していました。
これは、Rails3 ~ 5用のact_as_paranoid
を再実装したもので、内部実装が整備されたものらしいです。
しかし、このParanoiaには問題がありました。
それは、既存メソッド(delete
メソッド / destroy
メソッド)を上書きしてしまうのです。
実際に見てみます。
def paranoia_delete raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? if persisted? # if a transaction exists, add the record so that after_commit # callbacks can be run add_to_transaction update_columns(paranoia_destroy_attributes) elsif !frozen? assign_attributes(paranoia_destroy_attributes) end self end alias_method :delete, :paranoia_delete
- レコードが読み取り専用であれば、ActiveRecord::ReadOnlyRecord 例外が発生します。
- レコードがデータベースに保存されているものであれば、削除用の属性を使って update_columns メソッドが呼ばれます。(論理削除フラグとなるカラムを更新)
- レコードがデータベースに保存されていない場合かつ凍結されていない場合、assign_attributes メソッドが呼ばれ、論理削除用の属性がレコードに割り当てられます。
- 最後に self を返してメソッドが終了します。
-
alias_method :delete, :paranoia_delete
の一行でエイリアスをつけています。
5の処理で、deleteメソッドにエイリアスが付けられています。
またデフォルトスコープが変わってきます。
User.all
としたときに、発行されるSQLは以下の通りになります。
SELECT `users`.* FROM `users` WHERE (`users`.`deleted_at` IS NULL)
論理削除されたものは暗黙的に取得しなくなります。 このような実装について、Discardの作者はというと、、、、
・ I've worked with and have helped maintain paranoia for a while. I'm convinced it does the wrong thing for most cases.
・These overrides of the default ActiveRecord behaviour, while nice and magical on the surface, can and do lead to some rather confusing debugging sessions.
【翻訳】
・Paranoiaおよびacts_as_paranoidは、ほとんどのケースで誤った結果を生むと確信しています。
・デフォルトの ActiveRecord 動作のこれらのオーバーライドは、表面的には素晴らしく魔法のように見えますが、かなり混乱するデバッグ セッションを引き起こす可能性があり、実際に発生します。
Discardはどうなん?
Discardは、レコードに破棄済みのフラグをつける規則を追加する単純なAtiveRecordミックスインです。 公式ドキュメントはこちらです。 github.com
どんなふうに使うのか
事前準備
使い方はとても簡単です。
論理削除を施したい対象のモデルにDiscard::Model
をインクルードします。
class Post < ActiveRecord::Base include Discard::Model end
Post
モデルに対応するPosts
テーブルには以下のようにインデックスとカラムを追加します。
class AddDiscardToPosts < ActiveRecord::Migration[5.0] def change add_column :posts, :discarded_at, :datetime add_index :posts, :discarded_at end end
これで準備は完了です。
論理削除の実施方法
公式ドキュメントにあるこちらのコードを見るとよくわかります。
Post.all # => [#<Post id: 1, ...>] Post.kept # => [#<Post id: 1, ...>] Post.discarded # => [] post = Post.first # => #<Post id: 1, ...> post.discard # => true post.discard! # => Discard::RecordNotDiscarded: Failed to discard the record post.discarded? # => true post.undiscarded? # => false post.kept? # => false post.discarded_at # => 2017-04-18 18:49:49 -0700 Post.all # => [#<Post id: 1, ...>] Post.kept # => [] Post.discarded # => [#<Post id: 1, ...>]
これだけみてもデフォルトスコープが変わってないことがわかります。 実際に内部実装を見ていきます。
クラスメソッド(kept・undiscarded・discarded・with_discarded...)
module Discard module Model extend ActiveSupport::Concern included do class_attribute :discard_column self.discard_column = :discarded_at scope :kept, ->{ undiscarded } scope :undiscarded, ->{ where(discard_column => nil) } scope :discarded, ->{ where.not(discard_column => nil) } scope :with_discarded, ->{ unscope(where: discard_column) } define_model_callbacks :discard define_model_callbacks :undiscard end end
かなりわかりやすいですね。。 シンプルにそれぞれのスコープで、discard_columnをどう扱うかを定義しているだけです。
インスタンスメソッド(discarded・undiscarded)
def discard return false if discarded? run_callbacks(:discard) do update_attribute(self.class.discard_column, Time.current) end end
def undiscard return false unless discarded? run_callbacks(:undiscard) do update_attribute(self.class.discard_column, nil) end end
説明不要なくらいシンプルです。
気づき
ParanoiaとDiscardの内部実装を見ていて、先日読んだ「UNIXという考え方」という本に書いてあったことを思い出しました。
この本はUNIXがなぜ他のOSより優れているかを設計思想の観点から述べた本です。その中にこんなことが書いてありました。
- 小さなプログラムはわかりやすい
- 小さなプログラムは保守しやすい
- 小さなプログラムは組み合わせしやすい
今回、ParanoiaとDiscardを比較していたときにDiscardのシンプルさには少し衝撃を覚えました。同じようなことをしようとしても、ここまで内部実装の美しさやシンプルさが変わるのかと、、、
本で読んでも「まあ、小さい方がいいわな」と思っていましたが 実際に当事者になると、この設計思想がいかに重要でありがたいことかを見にしみて感じました。。。
まとめ
内部実装見るのは結構勉強なりました!!!