diff --git a/lib/mongoid/association/accessors.rb b/lib/mongoid/association/accessors.rb index b4101b852e..aef9de5b46 100644 --- a/lib/mongoid/association/accessors.rb +++ b/lib/mongoid/association/accessors.rb @@ -251,6 +251,31 @@ def without_autobuild Threaded.exit_execution("without_autobuild") end + # Is the current code executing within an association setter? + # + # @example Is setter active? + # document.in_setter? + # + # @return [ true | false ] If within a setter. + def in_setter? + Threaded.executing?(:in_setter) + end + + # Yield to the block with setter flag enabled. + # + # @example Execute within setter context. + # in_setter do + # # setter logic + # end + # + # @return [ Object ] The result of the yield. + def in_setter + Threaded.begin_execution("in_setter") + yield + ensure + Threaded.exit_execution("in_setter") + end + # Parse out the attributes and the options from the args passed to a # build_ or create_ methods. # @@ -345,15 +370,16 @@ def self.define_setter!(association) name = association.name association.inverse_class.tap do |klass| klass.re_define_method("#{name}=") do |object| - without_autobuild do - if value = get_relation(name, association, object) - if !value.respond_to?(:substitute) - value = __build__(name, value, association) + in_setter do + without_autobuild do + if value = get_relation(name, association, object) + if !value.respond_to?(:substitute) + value = __build__(name, value, association) + end + set_relation(name, value.substitute(object.substitutable)) + else + __build__(name, object.substitutable, association) end - - set_relation(name, value.substitute(object.substitutable)) - else - __build__(name, object.substitutable, association) end end end diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index f794aa3962..bba6d1b4b2 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -601,11 +601,31 @@ def as_attributes # @api private def update_attributes_hash if _target.empty? - _base.attributes.delete(_association.store_as) + handle_empty_target else _base.attributes.merge!(_association.store_as => _target.map(&:attributes)) end end + + # Check if we're currently in a setter + def in_setter? + _base.send(:in_setter?) + end + + # Handle the case when the target is empty. + # + # @api private + def handle_empty_target + # Only persist empty array if: + # We're explicitly assigning (setter was called) + if _base.attributes.key?(_association.store_as) || in_setter? + _base.attributes.merge!(_association.store_as => _target.map(&:attributes)) + else + # During initialization with no prior data, delete the key to maintain + # the original behavior of not persisting unassigned empty associations + _base.attributes.delete(_association.store_as) + end + end end end end diff --git a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb index fa5471aeb7..35e7f7c2d6 100644 --- a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +++ b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb @@ -4271,6 +4271,25 @@ class TrackingIdValidationHistory end end + context "when setting an embedded relation to an empty array" do + let(:document) do + Person.create! + end + + let(:person) do + Person.find(document.id) + end + + before do + person.update_attributes!(addresses: []) + end + + it "sets the embedded relation to an empty array" do + expect(person.addresses).to be_empty + expect(person.attributes.keys).to include("addresses") + end + end + context "when pushing with a before_add callback" do let(:artist) do @@ -4771,18 +4790,38 @@ class DNS::Record context "in an embeds_many relation" do - let(:band) { Band.create! } + context "using .create! first" do - before do - band.labels = [] - band.save! + let(:band) { Band.create! } + + before do + band.labels = [] + band.save! + end + + let(:reloaded_band) { Band.collection.find(_id: band._id).first } + + it "persists the empty list" do + expect(reloaded_band).to have_key(:labels) + expect(reloaded_band[:labels]).to eq [] + end end - let(:reloaded_band) { Band.collection.find(_id: band._id).first } + context "using .new first" do - it "persists the empty list" do - expect(reloaded_band).to have_key(:labels) - expect(reloaded_band[:labels]).to eq [] + let(:band) { Band.new } + + before do + band.labels = [] + band.save! + end + + let(:reloaded_band) { Band.collection.find(_id: band._id).first } + + it "persists the empty list" do + expect(reloaded_band).to have_key(:labels) + expect(reloaded_band[:labels]).to eq [] + end end end diff --git a/spec/mongoid/attributes_spec.rb b/spec/mongoid/attributes_spec.rb index 1adc953561..76a4995363 100644 --- a/spec/mongoid/attributes_spec.rb +++ b/spec/mongoid/attributes_spec.rb @@ -2368,7 +2368,7 @@ end it "updates the attributes" do - expect(doc.attributes).to_not have_key("pages") + expect(doc.pages).to be_empty end it "has the same attributes after reloading" do @@ -2475,10 +2475,12 @@ end it "updates the attributes" do - expect(doc.attributes).to_not have_key("pages") + expect(doc.pages).to be_empty end it "has the same attributes after reloading" do + puts doc.attributes + puts doc.reload.attributes expect(doc.attributes).to eq(doc.reload.attributes) end end @@ -2495,7 +2497,7 @@ end it "updates the attributes" do - expect(doc.attributes).to_not have_key("pages") + expect(doc.pages).to be_empty end it "has the same attributes after reloading" do