Skip to content

Commit 74383da

Browse files
authored
MONGOID-3291 Clear inverse associations when setting across all association types (#5288)
* MONGOID-3291 add tests for has_many, belongs_to, fix has_many * MONGOID-3291 test HABTM fix belongs_to * MONGOID-3291 remove HABTM test * MONGOID-3291 add has_many test * MONGOID-3291 dont clear belongs_to with has_any inverse * MONGOID-3291 fix belongs_to association resolving * MONGOID-3291 use create instead of new * MONGOID-3291 add embeds_one test * MONGOID-3291 fix tests * MONGOID-3291 add param to inverse * MONGOID-3291 reformat * MONGOID-3291 fix tests * MONGOID-3291 pend test * MONGOID-3291 remove associated only when relation is in memory * MONGOID-3291 fix embeds_one * MONGOID-3291 use methods instead of is_a? * MONGOID-3291 implement embeds_many and embedded_in * Revert "MONGOID-3291 use methods instead of is_a?" This reverts commit 48e344b. * MONGOID-3291 fix tests * MONGOID-3291 fix test * MONGOID-3291 remove embeds_many specific method and use the general one * MONGOID-3291 generalize methods * MONGOID-3291 clean up association methods * MONGOID-3291 further abstraction * MONGOID-3291 update test descriptions
1 parent 86707f9 commit 74383da

File tree

14 files changed

+430
-9
lines changed

14 files changed

+430
-9
lines changed

lib/mongoid/association/bindable.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,53 @@ def check_inverse!(doc)
6060
end
6161
end
6262

63+
# Remove the associated document from the inverse's association.
64+
#
65+
# @param [ Document ] doc The document to remove.
66+
def remove_associated(doc)
67+
if inverse = _association.inverse(doc)
68+
if _association.many?
69+
remove_associated_many(doc, inverse)
70+
elsif _association.in_to?
71+
remove_associated_in_to(doc, inverse)
72+
end
73+
end
74+
end
75+
76+
# Remove the associated document from the inverse's association.
77+
#
78+
# This method removes the associated on *_many relationships.
79+
#
80+
# @param [ Document ] doc The document to remove.
81+
# @param [ Symbol ] inverse The name of the inverse.
82+
def remove_associated_many(doc, inverse)
83+
# We only want to remove the inverse association when the inverse
84+
# document is in memory.
85+
if inv = doc.ivar(inverse)
86+
# This first condition is needed because when assigning the
87+
# embeds_many association using the same embeds_many
88+
# association, we delete from the array we are about to assign.
89+
if _base != inv && (associated = inv.ivar(_association.name))
90+
associated.delete(doc)
91+
end
92+
end
93+
end
94+
95+
# Remove the associated document from the inverse's association.
96+
#
97+
# This method removes associated on belongs_to and embedded_in
98+
# associations.
99+
#
100+
# @param [ Document ] doc The document to remove.
101+
# @param [ Symbol ] inverse The name of the inverse.
102+
def remove_associated_in_to(doc, inverse)
103+
# We only want to remove the inverse association when the inverse
104+
# document is in memory.
105+
if associated = doc.ivar(inverse)
106+
associated.send(_association.setter, nil)
107+
end
108+
end
109+
63110
# Set the id of the related document in the foreign key field on the
64111
# keyed document.
65112
#
@@ -134,6 +181,7 @@ def bind_inverse(doc, inverse)
134181
# @param [ Document ] doc The document to bind.
135182
def bind_from_relational_parent(doc)
136183
check_inverse!(doc)
184+
remove_associated(doc)
137185
bind_foreign_key(doc, record_id(_base))
138186
bind_polymorphic_type(doc, _base.class.name)
139187
bind_inverse(doc, _base)

lib/mongoid/association/embedded/embedded_in/binding.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def bind_one
2626
if _base.embedded_many?
2727
_target.do_or_do_not(_association.inverse(_target)).push(_base)
2828
else
29+
remove_associated(_target)
2930
_target.do_or_do_not(_association.inverse_setter(_target), _base)
3031
end
3132
end

lib/mongoid/association/embedded/embeds_many/binding.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Binding
1919
def bind_one(doc)
2020
doc.parentize(_base)
2121
binding do
22+
remove_associated(doc)
2223
doc.do_or_do_not(_association.inverse_setter(_target), _base)
2324
end
2425
end

lib/mongoid/association/embedded/embeds_one/buildable.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,25 @@ module Buildable
2525
#
2626
# @return [ Document ] A single document.
2727
def build(base, object, _type = nil, selected_fields = nil)
28-
return object unless object.is_a?(Hash)
29-
if _loading? && base.persisted?
30-
Factory.execute_from_db(klass, object, nil, selected_fields, execute_callbacks: false)
28+
if object.is_a?(Hash)
29+
if _loading? && base.persisted?
30+
Factory.execute_from_db(klass, object, nil, selected_fields, execute_callbacks: false)
31+
else
32+
Factory.build(klass, object)
33+
end
3134
else
32-
Factory.build(klass, object)
35+
clear_associated(object)
36+
object
37+
end
38+
end
39+
40+
private
41+
42+
def clear_associated(doc)
43+
if doc && (inv = inverse(doc))
44+
if associated = doc.ivar(inv)
45+
associated.substitute(nil)
46+
end
3347
end
3448
end
3549
end

lib/mongoid/association/referenced/belongs_to/binding.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def bind_one
2828
if _base.referenced_many?
2929
_target.__send__(inverse).push(_base)
3030
else
31+
remove_associated(_target)
3132
_target.set_relation(inverse, _base)
3233
end
3334
end

spec/integration/associations/scope_option_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
it 'initially associates the documents in-memory' do
5050
expect(trainer1.animal).to eq animal2
5151
expect(trainer2.animal).to eq animal3
52-
expect(animal1.trainer).to eq trainer1
52+
expect(animal1.trainer).to be_nil
5353
expect(animal2.trainer).to eq trainer1
5454
expect(animal3.trainer).to eq trainer2
5555
end

spec/mongoid/association/embedded/embedded_in/binding_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@
5858
end
5959

6060
it "does nothing" do
61-
expect(name).to receive(:namable=).never
61+
expect(name).to receive(:namable=).with(person).never
62+
expect(name).to receive(:namable=).with(nil).once
6263
binding.bind_one
6364
end
6465
end

spec/mongoid/association/embedded/embedded_in/buildable_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,58 @@
3333
end
3434
end
3535
end
36+
37+
context 'when the object is already associated with another object' do
38+
39+
context "when inverse is embeds_many" do
40+
41+
let(:appointment1) do
42+
Appointment.new
43+
end
44+
45+
let(:appointment2) do
46+
Appointment.new
47+
end
48+
49+
let(:person) do
50+
Person.create!
51+
end
52+
53+
before do
54+
appointment1.person = person
55+
appointment2.person = person
56+
end
57+
58+
it 'does not clear the object of its previous association' do
59+
expect(appointment1.person).to eq(person)
60+
expect(appointment2.person).to eq(person)
61+
expect(person.appointments).to eq([appointment1, appointment2])
62+
end
63+
end
64+
65+
context "when inverse is embeds_one" do
66+
67+
let(:scribe1) do
68+
Scribe.new
69+
end
70+
71+
let(:scribe2) do
72+
Scribe.new
73+
end
74+
75+
let(:owner) do
76+
Owner.create!
77+
end
78+
79+
before do
80+
scribe1.owner = owner
81+
scribe2.owner = owner
82+
end
83+
84+
it 'clears the object of its previous association' do
85+
expect(scribe1.owner).to be_nil
86+
expect(scribe2.owner).to eq(owner)
87+
end
88+
end
89+
end
3690
end

spec/mongoid/association/embedded/embeds_many/buildable_spec.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,116 @@
103103
end
104104
end
105105
end
106+
107+
context 'when the object is already associated with another object' do
108+
109+
context "when using <<" do
110+
111+
let(:person1) do
112+
Person.new
113+
end
114+
115+
let(:person2) do
116+
Person.new
117+
end
118+
119+
let(:appointment) do
120+
Appointment.new
121+
end
122+
123+
before do
124+
person1.appointments << appointment
125+
person2.appointments << appointment
126+
end
127+
128+
it 'clears the object of its previous association' do
129+
expect(person1.appointments).to eq([])
130+
expect(person2.appointments).to eq([appointment])
131+
end
132+
end
133+
134+
context "when using concat" do
135+
136+
let(:person1) do
137+
Person.new
138+
end
139+
140+
let(:person2) do
141+
Person.new
142+
end
143+
144+
let(:appointment) do
145+
Appointment.new
146+
end
147+
148+
before do
149+
person1.appointments.concat([appointment])
150+
person2.appointments.concat([appointment])
151+
end
152+
153+
it 'clears the object of its previous association' do
154+
expect(person1.appointments).to eq([])
155+
expect(person2.appointments).to eq([appointment])
156+
end
157+
end
158+
159+
context "when using =" do
160+
161+
let(:person1) do
162+
Person.new
163+
end
164+
165+
let(:person2) do
166+
Person.new
167+
end
168+
169+
let(:appointment) do
170+
Appointment.new
171+
end
172+
173+
let(:apts) { [ appointment ] }
174+
175+
before do
176+
person1.appointments = apts
177+
person2.appointments = apts
178+
expect(apts).to eq([ appointment ])
179+
end
180+
181+
it 'clears the object of its previous association' do
182+
expect(person1.appointments).to eq([])
183+
expect(person2.appointments).to eq([appointment])
184+
end
185+
end
186+
187+
context "when using = on the same document twice" do
188+
189+
let(:person1) do
190+
Person.new
191+
end
192+
193+
let(:person2) do
194+
Person.new
195+
end
196+
197+
let(:appointment1) do
198+
Appointment.new
199+
end
200+
201+
let(:appointment2) do
202+
Appointment.new
203+
end
204+
205+
let(:apts) { [ appointment1, appointment2 ] }
206+
207+
before do
208+
person1.appointments = apts
209+
person1.appointments = person1.appointments.reverse
210+
expect(apts).to eq([ appointment1, appointment2 ])
211+
end
212+
213+
it 'clears the object of its previous association' do
214+
expect(person1.appointments).to eq([ appointment2, appointment1 ])
215+
end
216+
end
217+
end
106218
end

spec/mongoid/association/embedded/embeds_one/buildable_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,29 @@
7878
end
7979
end
8080
end
81+
82+
context 'when the object is already associated with another object' do
83+
84+
let(:owner1) do
85+
Owner.create!
86+
end
87+
88+
let(:owner2) do
89+
Owner.create!
90+
end
91+
92+
let(:scribe) do
93+
Scribe.new
94+
end
95+
96+
before do
97+
owner1.scribe = scribe
98+
owner2.scribe = scribe
99+
end
100+
101+
it 'clears the object of its previous association' do
102+
expect(owner1.scribe).to be_nil
103+
expect(owner2.scribe).to eq(scribe)
104+
end
105+
end
81106
end

0 commit comments

Comments
 (0)