論理削除指定のモデルをhas_manyで参照し、かつnested_attributes_forにallow_destroyを指定するも物理削除になる。

環境はこちら
activerecord(4.1.6)
rails4 acts as _paranoid (0.1.4)

has manyのリレーション先でacts as paranoid指定しているのですが、
nested
attributes for リレーション先, allow destroy : true
で何故か物理削除になっていまったので調べてみました。

結論としては

lib/active record/associations/has many association.rb
の112行目で
records.each(&:destroy!)
とdestroy!で消していて、acts
as _paranoidのdestroy!は物理削除になるためでした。

何故destroyに!がついているのか 上記が定義されているdelete recordsメソッドを遡ってみます。
delete
recordsメソッドはlib/active record/associations/collection association.rb
のremove _recordsの中で呼ばれています(ソースでは492行目)

1
2
3
4
5
6
7
8
9
  # lib/active_record/associations/collection_association.rb
  def remove_records(existing_records, records, method)
    records.each { |record| callback(:before_remove, record) }

    delete_records(existing_records, method) if existing_records.any?
    records.each { |record| target.delete(record) }

    records.each { |record| callback(:after_remove, record) }
  end

んでこれはどこで呼ばれるかというとここで呼ばれています(ソースでは485行目)

1
2
3
4
5
6
7
8
9
10
11
12
  # lib/active_record/associations/collection_association.rb
  def delete_or_destroy(records, method)
    records = records.flatten
    records.each { |record| raise_on_type_mismatch!(record) }
    existing_records = records.reject { |r| r.new_record? }

    if existing_records.empty?
      remove_records(existing_records, records, method)
    else
      transaction { remove_records(existing_records, records, method) }
    end
  end

transactionで囲まれています。
ということでdestroyに失敗した時にロールバックするために!をつけているようです。

なのでparanoidのdestroy!をalias method chainで物理削除にしてしまうか。
上記のrecords.each(&:destroy!)をなんとか書き換えてあげると治りそうですね。

今回はこうやって直しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module ActiveRecord
  module Associations
    module HasManyAssociationPatch
      def delete_records(records, method)
        if method == :destroy
          records.each do |record|
            raise ActiveRecord::ActiveRecordError unless record.destroy
          end
          update_counter(-records.length) unless inverse_updates_counter_cache?
        else
          super
        end
      end
    end
  end
end

ActiveRecord::Associations::HasManyAssociation.send(:prepend, ActiveRecord::Associations::HasManyAssociationPatch)

今回ソース読んでいて知ったんですけど、has manyって削除する時に
before
remove
after _remove
ってコールバックが発生するんですね。知らなかった。

Comments