2008-09-12
Railsを業務システムに適用するなら、acts_as_paranoidの導入を検討しましょう。論理削除を実現。
タイトル変えました。まえのタイトルは「railsで論理削除をやるなら、acts_as_paranoid plugin」
基幹システムなんかは内部統制などの関係でデータを完全に消し去る訳にはいきません。となると削除フラグを利用した論理削除を実装する事になります。業務アプリを作ると99%*1で実装されるでしょう。
とても面倒。そんな面倒なロジックも、acts_as_paranoidがあれば簡単実装です。
まずはインストール。gitを使うので、git-coreを入れておきましょう。
$ cd RAILS_ROOT $ ruby script/plugin install git://github.com/technoweenie/acts_as_paranoid.git
言う事無し。
acts_as_paranoidを利用するには二つの事をするだけです。
- 論理削除を実装したいテーブルに deleted_at:datetime というカラムを作る。
- 対応するモデルに acts_as_paranoid 利用を宣言する。
簡単ですね。
利用例を紹介しますが、最初からacts_as_paranoidを使うことが決まっているのは幸せです。よって、今回は後からacts_as_paranoidを実装するケースを紹介します。
成績表アプリを作ります。と言っても名前と点数を格納するだけです。
$ ruby script/generate model Grade name:string score:integer $ rake db:migrate
はい、出来上がり。コントローラやビューは面倒なので作りません。
これだけでは空っぽのデータとか作りたい放題なので、ちと制限をかけましょう。
class Grade < ActiveRecord::Base validates_numericality_of :score, :only_integer => true, :greater_than_or_equal_to => 0, :less_than_or_equal_to => 100 end
scoreは整数で0以上100以下であること、という制限です。
さて、Gradeモデルに論理削除を実装します。まずはカラム作成から。
$ ruby script/generate migration AddDeleteFlagToGrade deleted_at:datetime $ rake db:migrate
次はacts_as_paranoid利用の宣言です。と言ってもclass定義の直後あたりにacts_as_paranoidって書くだけですが。
class Grade < ActiveRecord::Base acts_as_paranoid validates_numericality_of :score, :only_integer => true, :greater_than_or_equal_to => 0, :less_than_or_equal_to => 100 end
おしまい。30秒で出来ますね。
動作を確認してみます。
$ ruby script/console >> Grade.new(:name => 'fujisan1', :score => 10).save => true >> Grade.find(:all) => [#<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:13:04", updated_at: "2008-09-12 23:13:04", deleted_at: nil>] >> Grade.destroy_all => [#<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:13:04", updated_at: "2008-09-12 23:13:04", deleted_at: nil>] >> Grade.find(:all) => [] >> Grade.find_with_deleted(:all) => [#<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:13:04", updated_at: "2008-09-12 23:13:04", deleted_at: "2008-09-12 23:13:20">]
destroy_allしたタイミングでdeleted_atに値が入ります。この値の有無で論理削除されたか否かを判断しています。詳しくはRAILS_ENV=developmentで吐かれるSQL見てください。
また、acts_as_paranoid利用を宣言したモデルには find_with_deleted が実装されます。これで、削除済みデータを対象にfindする事も可能です。*2
acts_as_paranoidの良いところは
- findだけでなくfind_by_HOGEやcount/sumなど、ファインダーメソッド全てで自動的にdeleted_atの値確認をしてくれる
- destroy、destroy_allメソッドをSQLのDELETE文からUPDATE SET deleted_at = に置き換えてくれる
ところにあります。要するに、既存のコードの書き換えを極めて少なく済ませる事ができるのです。
$ ruby script/console
>> Grade.new(:name => 'fujisan1', :score => 10).save
=> true
>> Grade.new(:name => 'fujisan2', :score => 20).save
=> true
>> Grade.new(:name => 'fujisan3', :score => 100).save
=> true
>> Grade.count(:all)
=> 3
>> Grade.sum(:score)
=> 130
>> Grade.find_by_name('fujisan2').destroy
=> #<Grade id: 2, name: "fujisan2", score: 20, created_at: "2008-09-12 23:19:22", updated_at: "2008-09-12 23:19:22", deleted_at: nil>
>> Grade.count(:all)
=> 2
>> Grade.sum(:score)
=> 110
>> Grade.find_with_deleted(:all).size
=> 3
意図的にwith_deletedを呼ばない限り、論理削除されたデータは表に出てきません。
さて、本気で削除したくなるときもあるでしょう。その時は破壊的メソッドを呼びましょう。
$ ruby script/console
>> Grade.new(:name => 'fujisan1', :score => 10).save
=> true
>> Grade.new(:name => 'fujisan2', :score => 20).save
=> true
>> Grade.new(:name => 'fujisan3', :score => 100).save
=> true
>> Grade.find_by_name('fujisan1').destroy!
=> #<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:27:19", updated_at: "2008-09-12 23:27:19", deleted_at: nil>
>> Grade.find_with_deleted(:all)
=> [#<Grade id: 2, name: "fujisan2", score: 20, created_at: "2008-09-12 23:27:25", updated_at: "2008-09-12 23:27:25", deleted_at: nil>, #<Grade id: 3, name: "fujisan3", score: 100, created_at: "2008-09-12 23:27:31", updated_at: "2008-09-12 23:27:31", deleted_at: nil>]
destroy_all! destroy! がacts_as_paranoidによって実装されます。
ここまではacts_as_paranoidの基本的な機能を紹介しました。これに加えて、業務システムで使うコツを紹介。
一意制約に注意
acts_as_paranoidを利用しているモデルで一意制約を使用する場合、ちょっとしたコツが必要です。
まずはGradeモデルにnameで一意になるよう、モデル上で制約を加えてみます。
class Grade < ActiveRecord::Base acts_as_paranoid validates_numericality_of :score, :only_integer => true, :greater_than_or_equal_to => 0, :less_than_or_equal_to => 100 validates_uniqueness_of :name, :case_sensitive => false end
これで実装完了。試してみましょう。
$ ruby script/console >> Grade.new(:name => 'fujisan1', :score => 10).save => true >> Grade.find(1).destroy => #<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:33:42", updated_at: "2008-09-12 23:33:42", deleted_at: nil> >> Grade.find(:all) => [] >> Grade.new(:name => 'fujisan1', :score => 10).save => false
あれ、保存できませんでした。削除されているわけですから保存できても良さそうです。
残念な事に、acts_as_paranoidはsave(createやupdate)までは面倒を見てくれません。save時には全件を対象に一意の調査してしまうのです。
これを回避するには:scopeを指定してしまうのが良いでしょう。
class Grade < ActiveRecord::Base acts_as_paranoid validates_numericality_of :score, :only_integer => true, :greater_than_or_equal_to => 0, :less_than_or_equal_to => 100 validates_uniqueness_of :name, :case_sensitive => false, :scope => :deleted_at end
実行してみます。
$ ruby script/console >> Grade.new(:name => 'fujisan1', :score => 10).save => true >> Grade.find(1).destroy => #<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:36:51", updated_at: "2008-09-12 23:36:51", deleted_at: nil> >> Grade.find(:all) => [] >> Grade.new(:name => 'fujisan1', :score => 10).save => true >> Grade.find(:all) => [#<Grade id: 2, name: "fujisan1", score: 10, created_at: "2008-09-12 23:37:00", updated_at: "2008-09-12 23:37:00", deleted_at: nil>] >> Grade.find_with_deleted(:all) => [#<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:36:51", updated_at: "2008-09-12 23:36:51", deleted_at: "2008-09-12 23:36:54">, #<Grade id: 2, name: "fujisan1", score: 10, created_at: "2008-09-12 23:37:00", updated_at: "2008-09-12 23:37:00", deleted_at: nil>]
無事、意図通り動きました。
DBのunique indexを使う場合も制約条件にdeleted_atを加えれば同じ動作になるでしょう。
論理削除の復活
簡単です。deleted_at = nilです。
$ ruby script/console >> Grade.new(:name => 'fujisan1', :score => 10).save => true >> Grade.find(1).destroy => #<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:56:22", updated_at: "2008-09-12 23:56:22", deleted_at: nil> >> Grade.find(:all) => [] >> a = Grade.find_with_deleted(1) => #<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:56:22", updated_at: "2008-09-12 23:56:22", deleted_at: "2008-09-12 23:56:28"> >> a.deleted_at = nil => nil >> a.save => true >> Grade.find(1) => #<Grade id: 1, name: "fujisan1", score: 10, created_at: "2008-09-12 23:56:22", updated_at: "2008-09-12 23:56:42", deleted_at: nil>
同様なものにacts_as_soft_deletableがあります。こっちは、削除後のデータをアーカイブ用のテーブルを用意し、移動するものです。
次回はacts_as_soft_deletableをご紹介。
*1:fujisan3776調べ
*2:findのオプションで:with_deleted => trueを指定しても同じです。Model#find(:all, :with_deleted => true)
- 3 http://reader.livedoor.com/reader/
- 3 http://www.google.co.jp/search?hl=ja&client=firefox-a&rls=org.mozilla:ja:official&hs=1D&q=iui+page制御&btnG=検索&lr=lang_ja
- 3 http://www.google.co.jp/search?hl=ja&q=virtualbox+ブリッジ&btnG=Google+検索&lr=&aq=f&oq=
- 3 http://www.google.co.jp/search?q=Rails+state&lr=lang_ja&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:ja-JP-mac:official&client=firefox
- 3 http://www.google.co.jp/search?q=acts_as_state_machine&lr=lang_ja&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:ja-JP-mac:official&client=firefox-a
- 2 http://b.hatena.ne.jp/hassie/
- 2 http://fastladder.com/reader/
- 2 http://search.yahoo.co.jp/search?p=ora-12705&search.x=1&fr=top_ga1&tid=top_ga1&ei=UTF-8
- 2 http://www.google.co.jp/search?q=active+resource&sourceid=navclient-ff&ie=UTF-8&rlz=1B3GGGL_jaJP290JP290
- 2 http://www.google.com/reader/view/
