その他
    ホーム 技術発信 DoRuby 論理削除Gem(Paranoia)を自分好みにカスタマイズ

    論理削除Gem(Paranoia)を自分好みにカスタマイズ

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    DBのユニーク規制が、データの整合性を保つ最後の砦だと考えている。
    実装に注意したり、DBを直接書き換える場合に、注意すれば良いのだけれど、やっぱりバグやヒューマンエラーは付き物。
    バグやヒューマンエラーを考えると論理削除を使って、定期的にクリーニングがベストかな。
    論理削除のGemを探しましたが、日付を使う物が多く、かつNULLが使われるので、複合キーでのユニーク規制が掛からない。
    フラグを使う物もあったけど、0・1では一度削除したものと同じものを再度削除出来なくなる。
    辿り着いたのは、初期値0で、削除時にIDと同じ値を設定するというDB設計。
    ただ、これを満たすGemが見つからないので、良さそうなParanoiaの一部を修正する(モンキーパッチを充てる)事にしました。

    また、JOINされるケースでSQLが「WHERE “.`deleted` = 0」となり、「Mysql2::Error: Column ‘deleted’ in where clause is ambiguous:」で落ちる問題も対応しました。
    このバージョンの組み合わせだから動かない、という事なんだろうか?(謎)

    前提:Rails 4、Paranoia 2.0.2

    ●Paranoia追加

    編集:Gemfile

    # Use Paranoia
    gem 'paranoia', '2.0.2' # モンキーパッチ:config/initializers/extensions/paranoia.rb

    $ bundle install
    ===========================================
    Installing paranoia 2.0.2
    ===========================================
    ※「Your bundle is complete!」と表示されればOK

    ●Paranoiaカスタマイズ

    作成:config/initializers/extensions/paranoia.rb

    module Paranoia
      module Query
        def only_deleted
          # with_deleted.where.not(paranoia_column => nil)
          with_deleted.where.not(paranoia_column => 0)
        end
      end
    
      def restore!(opts = {})
        ActiveRecord::Base.transaction do
          run_callbacks(:restore) do
            # update_column paranoia_column, nil
            update_column paranoia_column, 0
            restore_associated_records if opts[:recursive]
          end
        end
      end
    
      private
    
      def touch_paranoia_column(with_transaction=false)
        if with_transaction
          # with_transaction_returning_status { touch(paranoia_column) }
          with_transaction_returning_status { update_attribute(paranoia_column, id) }
        else
          # touch(paranoia_column)
          update_attribute(paranoia_column, id)
        end
      end
    end
    
    class ActiveRecord::Base
      def self.acts_as_paranoid(options={})
        alias :really_destroy! :destroy
        alias :destroy! :destroy
        alias :delete! :delete
        include Paranoia
        class_attribute :paranoia_column
    
        # self.paranoia_column = options[:column] || :deleted_at
        self.paranoia_column = options[:column] || :deleted
        # default_scope { where(paranoia_column => nil) }
        def self.default_scope
          where(arel_table[paranoia_column].eq 0)
        end
    
        before_restore {
          self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers)
        }
        after_restore {
          self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers)
        }
      end
    end

    ●ベースモデル作成

    $ rails g model base
    ===========================================
    invoke active_record
    create db/migrate/20140707233955_create_bases.rb
    create app/models/base.rb
    invoke rspec
    create spec/models/base_spec.rb
    invoke factory_girl
    create spec/factories/bases.rb
    ===========================================
    ※エラーが表示されなければOK
    削除:db/migrate/20140707233955_create_bases.rb
    削除:spec/factories/bases.rb
    編集:app/models/base.rb

    class Base < ActiveRecord::Base
      self.abstract_class = true
      acts_as_paranoid
    end

    ●テストモデル作成

    $ rails g model test
    ===========================================
    invoke active_record
    create db/migrate/20140707234352_create_tests.rb
    create app/models/test.rb
    invoke rspec
    create spec/models/test_spec.rb
    invoke factory_girl
    create spec/factories/tests.rb
    ===========================================
    ※エラーが表示されなければOK
    編集:db/migrate/20140707234352_create_tests.rb

    class Tests < ActiveRecord::Migration
      def change
    	create_table :tests do |t|
          t.string :name, null: false
          t.timestamps
          t.integer :deleted, null: false, default: 0
        end
        add_index :tests, [:name, :deleted], unique: true
      end
    end

    $ rake db:migrate
    ※エラーが表示されなければOK
    編集:app/models/test.rb

    class Test < ActiveRecord::Base
    class Test < Base
    end

    ○動作確認

    $ rails cTest.create(name: ‘test’)
    test = Test.find_by_name(‘test’)
    test.blank?
    => false
    test.destroy
    => UPDATE文
    test = Test.find_by_name(‘test’)
    test.blank?
    => true
    exit

    ●テストモデル削除

    削除:app/models/test.rb
    削除:db/migrate/20140707234352_create_tests.rb
    削除:spec/models/test_spec.rb
    削除:spec/factories/tests.rb
    $ rake db:migrate:reset
    ※エラーが表示されなければOK

    記事を共有