2010-12-01

Ruby on Rails における論理削除

Ruby on Rails の model において論理削除を可能にする方法を調べた.

論理削除された行であることを示す列による

Plugin acts_as_paranoidRuby on Rails ステップアップ講座 - 大場寧子 (2007-11-26) により紹介されていた.私は試験していない.acts_as_paranoid とした model の表に列 deleted_at を追加し,この列が null でなく論理削除した時刻がある行は論理削除されているとする.

作者は i quit using acts_as_paranoid long ago. i tend to use `hidden` and `visible` scopes for my models (Feb 23rd, 2009) と述べている.scope を用いたやりかたについては後で述べる.

別のやりかた:削除して別の表に入れておく

Plugin acts_as_trashable では migrate に ActsAsTrashable::TrashRecord.create_table を追加することにより,表 trash_records が追加される.なお,追加に対応してこの表を削除するようなメソッドは提供されていない.論理削除した行は,表から削除されて表 trash_records に格納される.このやりかたを採った理由について,Another method of solving the issue that this plugin addresses is to add a status flag on your model and consider records with the flag set to be deleted (for example see ActsAsParanoid[http://ar-paranoid.rubyforge.org/]). This is definitely a safer method of keeping the records since the data is never actually deleted. However, it presents a more complicated solution and you'll need to guard against ever accidentally showing a deleted record. The best method to use will depend on your application. と述べている (README).複数の models を acts_as_trashable としたばあい,論理削除された行はすべて表 trash_records に格納される.

また,Finally, this plugin has a companion plugin ActsAsRevisionable[http://actsasrevisions.rubyforge.org/] which allows you to keep a history of revisions to records each time they get updated. Using both together gives you a more robust restoration system. They are not packaged together because some applications may not have a use for the revisioning but do need the protection against accidental deletion. とあるように (README),改訂履歴の保持を可能にする別の Plugin acts_as_revisionable と併用できる.

Plugin acts_as_soft_deletable も私は試験していない.README によれば,acts_as_soft_deletable とした model の表の論理削除行を格納する別の表を作成する点は acts_as_trashable と同様.違いは,複数の models の表それぞれの論理削除行を格納するためにそれぞれ別の表を用意する点にある.

Acts_as_paranoid の問題点を Acts_as_paranoid takes the approach of using a deleted_at flag in the models table. If the deleted_at column has a value, the row is considered 'deleted'. The problem with this approach is that all finds on the model have to exclude 'deleted' rows. This turns out be be a challenge. Acts_as_paranoid patches the ActiveRecord internals to accomplish this, but it is fragile and could break with future changes to ActiveRecord. Also, some of the more exotic finds currently don't work (has_many :through with polymorphism as of March 2008) and supporting them means running on an upgrade treadmill to keep up with the evolution of ActiveRecord. と述べている (README).

また,Before using this with a new Rails 2.3 app, you may want to consider using the new default_scope feature (or named_scopes) with a deleted_at flag. See http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping for a discussion about this. と,default_scope について言及している.

ふたたび,論理削除された行であることを示す列による--- scope を用いて

default_scope を用いたやりかたについては,Ryan's Scraps: What's New in Edge Rails: Default Scoping (November 18, 2008;コメント),with_scope with scope — err.the_blog (November 28, 2006;コメント) で議論されている.最大の問題 (と私が考えたの) は Killing is_paranoid (2009/10/13;Plugin is_paranoidUsing default_scope to recreate acts_as_paranoid [2009/03/22] にて発表された) で指摘されたもので,About default_scope not being default-enough for is_paranoid: Imagine you have a product model with a default scope that specifies that only products with in_stock = true should be selected (we don’t want to show out-of-stock products by default). Cool, that works on simple finds. Now let’s imagine we have categories which have many products. Category.find(:first, :include => :products) works, but it doesn’t respect the default_scope on the eager-loading of products so you get out-of-stock items. ほか (引用した例では Category) から参照されている行 (引用した例では Product) を論理削除したばあいの挙動がおかしいということで,物理削除ならば null とされるべき参照が該当する.コンポジション関係であれば親を論理削除したときに子も同時に論理削除できるので,このような問題は生じない.

Easy « paranoid  with Rails 3 and state_machine « Slainer68's Blog (25/08/2010) は state_machine と併用したやりかたを示している.前述の問題を解決しているかは不明 (I know there were some problems with default_scoping on Rails 2, don’t know if all these problems have been fixed on Rails 3.).Rail Spikes: Using acts_as_archive instead of soft delete (February 26, 2010) とこれに対するコメントはそれぞれ Plugins acts_as_archive ならびに acts_as_archival を紹介している.acts_as_archival は,親の "archive" によって子も "archive" する.また,destroy ほか ActiveRecord メソッドのオーバーライドはしないため,acts_as_archival とされた表がほかから参照されていたとき,Category.find(:first, :include => :products) のような問い合わせに対して archived な行も unarchived な行も区別しないのは変わらないが,Product.find() の結果とは矛盾しない.acts_as_archive は削除行を別の表に入れる.

とりあえずの結論

コンポジション関係しかない表の親子には Plugin is_paranoid のような方法が適当と思う.このやりかたの plugin で現在もメンテされているものが見つからないのが問題である.Plugin acts_as_archival では,表がこれにあたるか調べてから必要に応じて archived な行と unarchived な行を区別しなければならない.しかし,コンポジション関係でない参照では,これよりほかにやりかたがないように思える.

削除して別の表に入れておくやりかたは「なんとなく」好きになれない.理由を追及できたら追記したいと思う.