という内容のものが業務で必要になり、処理を書きましたが勘違いでそんな仕様はどこにもなくお蔵入りになってしまって悔しいので
ここでさらします。
Gistではここに置いてあります。
real_delete_discreetly
本体です
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
module RealDeleteDiscreetly
extend ActiveSupport::Concern
def delete_discreetly
relation_exist = false
self.class.confirm_relations.each do |confirm_relation|
if destroyed_by_association
if destroyed_by_association.active_record == confirm_relation.to_s.classify.constantize
relation_exist = false
break
end
else
if reflection = self.class.reflections[confirm_relation].presence
send_method = case reflection.macro
when :has_many
:exists?
else
:present?
end
if send(confirm_relation).send(send_method)
relation_exist = true
break
end
else
raise ArgumentError, "#{confirm_relation} is not Reflection"
end
end
end
unless relation_exist
self.class.delete_all!(self.class.primary_key.to_sym => id)
end
end
module ClassMethods
def real_delete_discreetly(options = {})
class_attribute :confirm_relations
self.confirm_relations = options.delete(:confirm_relations)
acts_as_paranoid options
after_destroy :delete_discreetly
end
end
end
|
論理削除部分はact_as_paranoidに依存しています。
このソースはlibなどのオートロードの対象となるディレクトリに置く想定です。
該当のモデルでreal_delete_discreetlyすると
関連先が存在していたら論理削除、関連先が存在していなかったら物理削除
という動きをするようになるはずです。
こんな書き方になります。
1
2
3
4
|
class UserProfile < ActiveRecord::Base
belongs_to :user
real_delete_discreetly column: :del_flg, column_type: 'boolean', confirm_relations: [:user]
end
|
こうするとuserが存在する場合は論理削除。
userレコードが存在しない場合は物理削除になります。
confirm_relations以外の値はacts_as_paranoidで渡すものと同じものが指定できます。
今回の例では論理削除のカラムをデフォルトのdeleted_atじゃなくてdelete_flagで見ている例ですね。
rspecも書くつもりだったので各モデルで使えるshared_exampleもあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
shared_examples_for 'real delete discreetly' do |opts|
let(:base_instance) { opts[:base_instance] }
let(:confirm_relations) { opts[:confirm_relations] }
let(:paranoid_column) { opts[:paranoid_column] || :delete_flag }
let(:paranoid_column_type) { opts[:paranoid_column_type] || :boolean }
before do
@model = base_instance.dup
@model.save
case paranoid_column_type.to_sym
when :boolean
@from = false
@to = true
else
time = Time.now
Time.stub(:now).and_return(time)
@from = nil
@to = time
end
end
context 'relation exists' do
before do
confirm_relation = confirm_relations.first
case @model.class.reflections[confirm_relation].macro
when :has_many
@model.send(confirm_relation).send(:build)
else
@model.send("build_#{confirm_relation}")
end
end
it 'soft deleted' do
expect { @model.destroy }.to change { @model.send(paranoid_column) }.from(@from).to(@to)
end
it 'not real deleted' do
expect { @model.destroy }.to change(@model.class.unscoped, :count).by(0)
end
end
context 'when relation not exists' do
it 'real deleted' do
expect { @model.destroy }.to change(@model.class.unscoped, :count).by(-1)
end
end
end
|
これをspec_helperなどからrequireするなり読み込めるようにしてもらって
対象のモデルで
1
2
3
4
5
6
7
|
require 'rails_helper'
RSpec.describe UserProfile, type: :model do
include_examples 'real delete discreetly',
base_instance: UserProfile.create(user_id: nil_or_exist_id),
confirm_relations: [:user]
end
|
とかやると論理削除と物理削除のテストが走ります。
ただ冒頭でも述べた通りお蔵入りしたのでちゃんと動くかどうか細かい所までは見きれていないです。
何かの参考になれば幸いです。
親のレコードが消された時に連動してdependent: :destroyとかの指定で消える場合は、親のレコードってdestroyed_by_associationに格納されるんですね。
今回のコードを書いてて初めて知りました。