Skip to content

Commit cb417b4

Browse files
authored
Merge pull request #485 from Dynamoid/update-associations-on-object-delete
Update associations on object delete
2 parents a902a08 + d59a67b commit cb417b4

File tree

6 files changed

+161
-4
lines changed

6 files changed

+161
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
## Fixes
1515

1616
* [#480](https://github.com/Dynamoid/dynamoid/pull/480) Repair `.consistent`/`.delete_all`/`.destroy_all` calls directly on a model class
17+
* [#484](https://github.com/Dynamoid/dynamoid/pull/484) Fix broken foreign keys after model deleting (@kkan)
1718
* Fixes in Readme.md: [#470](https://github.com/Dynamoid/dynamoid/pull/470) (@rromanchuk), [#473](https://github.com/Dynamoid/dynamoid/pull/473) (@Rulikkk)
1819

1920
---

lib/dynamoid/associations/association.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def declaration_field_type
5858
:set
5959
end
6060

61+
def disassociate_source
62+
Array(target).each do |target_entry|
63+
target_entry.send(target_association).disassociate(source.hash_key) if target_entry && target_association
64+
end
65+
end
66+
6167
private
6268

6369
# The target class name, either inferred through the association's name or specified in options.

lib/dynamoid/associations/many_association.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ def initialize(*args)
2525
# @private
2626
# @since 0.2.0
2727
def find_target
28-
Array(target_class.find(source_ids.to_a))
28+
return [] if source_ids.empty?
29+
30+
Array(target_class.find(source_ids.to_a, raise_error: false))
2931
end
3032

3133
# @private

lib/dynamoid/associations/single_association.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def setter(object)
2828
# unsaved changes will be saved. Doesn't delete an associated model from
2929
# DynamoDB.
3030
def delete
31-
target.send(target_association).disassociate(source.hash_key) if target && target_association
31+
disassociate_source
3232
disassociate
3333
target
3434
end
@@ -90,7 +90,7 @@ def empty?
9090

9191
# @private
9292
def associate(hash_key)
93-
target.send(target_association).disassociate(source.hash_key) if target && target_association
93+
disassociate_source
9494
source.update_attribute(source_attribute, Set[hash_key])
9595
end
9696

@@ -109,7 +109,7 @@ def disassociate(_hash_key = nil)
109109
def find_target
110110
return if source_ids.empty?
111111

112-
target_class.find(source_ids.first)
112+
target_class.find(source_ids.first, raise_error: false)
113113
end
114114

115115
def target=(object)

lib/dynamoid/persistence.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,10 @@ def delete
802802
@destroyed = true
803803

804804
Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
805+
806+
self.class.associations.each do |name, options|
807+
send(name).disassociate_source
808+
end
805809
rescue Dynamoid::Errors::ConditionalCheckFailedException
806810
raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
807811
end

spec/dynamoid/persistence_spec.rb

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,10 @@ def log_message
25592559
end
25602560
end
25612561

2562+
context 'destroy' do
2563+
# TODO: adopt test cases for the `delete` method
2564+
end
2565+
25622566
context 'delete' do
25632567
it 'uses dumped value of sort key to call DeleteItem' do
25642568
klass = new_class do
@@ -2597,6 +2601,146 @@ def log_message
25972601
expect { a1.destroy }.to_not raise_error
25982602
end
25992603
end
2604+
2605+
context 'when model has associations' do
2606+
context 'when belongs_to association' do
2607+
context 'when has_many on the other side' do
2608+
let!(:source_model) { User.create }
2609+
let!(:target_model) { source_model.camel_case.create }
2610+
2611+
it 'disassociates self' do
2612+
expect do
2613+
source_model.delete
2614+
end.to change { CamelCase.find(target_model.id).users.target }.from([source_model]).to([])
2615+
end
2616+
2617+
it 'updates cached ids list in associated model' do
2618+
source_model.delete
2619+
expect(CamelCase.find(target_model.id).users_ids).to eq nil
2620+
end
2621+
2622+
it 'behaves correctly when associated model is linked with several models' do
2623+
source_model2 = User.create
2624+
target_model.users << source_model2
2625+
2626+
expect(CamelCase.find(target_model.id).users.target).to contain_exactly(source_model, source_model2)
2627+
source_model.delete
2628+
expect(CamelCase.find(target_model.id).users.target).to contain_exactly(source_model2)
2629+
expect(CamelCase.find(target_model.id).users_ids).to eq [source_model2.id].to_set
2630+
end
2631+
2632+
it 'does not raise exception when foreign key is broken' do
2633+
source_model.update_attributes!(camel_case_ids: ['fake_id'])
2634+
2635+
expect { source_model.delete }.not_to raise_error
2636+
expect(CamelCase.find(target_model.id).users.target).to eq []
2637+
end
2638+
end
2639+
2640+
context 'when has_one on the other side' do
2641+
let!(:source_model) { Sponsor.create }
2642+
let!(:target_model) { source_model.camel_case.create }
2643+
2644+
it 'disassociates self' do
2645+
expect do
2646+
source_model.delete
2647+
end.to change { CamelCase.find(target_model.id).sponsor.target }.from(source_model).to(nil)
2648+
end
2649+
2650+
it 'updates cached ids list in associated model' do
2651+
source_model.delete
2652+
expect(CamelCase.find(target_model.id).sponsor_ids).to eq nil
2653+
end
2654+
2655+
it 'does not raise exception when foreign key is broken' do
2656+
source_model.update_attributes!(camel_case_ids: ['fake_id'])
2657+
2658+
expect { source_model.delete }.not_to raise_error
2659+
expect(CamelCase.find(target_model.id).sponsor.target).to eq nil
2660+
end
2661+
end
2662+
end
2663+
2664+
context 'when has_many association' do
2665+
let!(:source_model) { User.create }
2666+
let!(:target_model) { source_model.books.create }
2667+
2668+
it 'disassociates self' do
2669+
expect do
2670+
source_model.delete
2671+
end.to change { Magazine.find(target_model.title).owner.target }.from(source_model).to(nil)
2672+
end
2673+
2674+
it 'updates cached ids list in associated model' do
2675+
source_model.delete
2676+
expect(Magazine.find(target_model.title).owner_ids).to eq nil
2677+
end
2678+
2679+
it 'does not raise exception when cached foreign key is broken' do
2680+
books_ids_new = source_model.books_ids + ['fake_id']
2681+
source_model.update_attributes!(books_ids: books_ids_new)
2682+
2683+
expect { source_model.delete }.not_to raise_error
2684+
expect(Magazine.find(target_model.title).owner).to eq nil
2685+
end
2686+
end
2687+
2688+
context 'when has_one association' do
2689+
let!(:source_model) { User.create }
2690+
let!(:target_model) { source_model.monthly.create }
2691+
2692+
it 'disassociates self' do
2693+
expect do
2694+
source_model.delete
2695+
end.to change { Subscription.find(target_model.id).customer.target }.from(source_model).to(nil)
2696+
end
2697+
2698+
it 'updates cached ids list in associated model' do
2699+
source_model.delete
2700+
expect(Subscription.find(target_model.id).customer_ids).to eq nil
2701+
end
2702+
2703+
it 'does not raise exception when cached foreign key is broken' do
2704+
source_model.update_attributes!(monthly_ids: ['fake_id'])
2705+
2706+
expect { source_model.delete }.not_to raise_error
2707+
end
2708+
end
2709+
2710+
context 'when has_and_belongs_to_many association' do
2711+
let!(:source_model) { User.create }
2712+
let!(:target_model) { source_model.subscriptions.create }
2713+
2714+
it 'disassociates self' do
2715+
expect do
2716+
source_model.delete
2717+
end.to change { Subscription.find(target_model.id).users.target }.from([source_model]).to([])
2718+
end
2719+
2720+
it 'updates cached ids list in associated model' do
2721+
source_model.delete
2722+
expect(Subscription.find(target_model.id).users_ids).to eq nil
2723+
end
2724+
2725+
it 'behaves correctly when associated model is linked with several models' do
2726+
source_model2 = User.create
2727+
target_model.users << source_model2
2728+
2729+
expect(Subscription.find(target_model.id).users.target).to contain_exactly(source_model, source_model2)
2730+
source_model.delete
2731+
expect(Subscription.find(target_model.id).users.target).to contain_exactly(source_model2)
2732+
expect(Subscription.find(target_model.id).users_ids).to eq [source_model2.id].to_set
2733+
end
2734+
2735+
it 'does not raise exception when foreign key is broken' do
2736+
subscriptions_ids_new = source_model.subscriptions_ids + ['fake_id']
2737+
source_model.update_attributes!(subscriptions_ids: subscriptions_ids_new)
2738+
2739+
expect { source_model.delete }.not_to raise_error
2740+
expect(Subscription.find(target_model.id).users_ids).to eq nil
2741+
end
2742+
end
2743+
end
26002744
end
26012745

26022746
describe '.import' do

0 commit comments

Comments
 (0)