1

I'm using Mongoid version 2.8.1. I've noticed that the destroy method does not work when the model has a has_and_belongs_to_many relationship.

For example, I have two models

class Article
    include Mongoid::Document
    has_and_belongs_to_many :subjects
    ...
end

and

class Subject
    include Mongoid::Document
    has_and_belongs_to_many :articles
    ...
end

Now I want to delete an article document. So I tried

a = Article.find('someid1234')

this returns a valid object, then I do

>> a.destroy
=> true
>> a.errors.any?
=> false
>> a.errors.count
=> 0

But when I do

a.reload

I still get the object back!

If I use

a.delete

instead, it would work, but delete doesn't run callbacks, and I want to run callbacks.

I have nailed this down to the has_and_belongs_to_many relationship. Because of this relationship, destroy invokes a callback method.

Article._destroy_callbacks
=> [#<ActiveSupport::Callbacks::Callback:0x007fc4a0e71258 @klass=Shortlist, @kind=:after, @chain=[...], @per_key={:if=>[], :unless=>[]}, @options={:if=>[], :unless=>[]}, @raw_filter=#<Proc:0x007fc4a0e714d8@/opt/boxen/rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/mongoid-2.4.12/lib/mongoid/relations/synchronization.rb:160>, @filter="_callback_after_31(self)", @compiled_options="true", @callback_id=32>]

The method in question in /opt/boxen/rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/mongoid-2.4.12/lib/mongoid/relations/synchronization.rb:160 is

def synced_destroy(metadata)
    tap do
        set_callback(
            :destroy,
            :after
        ) do |doc|
            doc.remove_inverse_keys(metadata)
        end 
    end 
end

After I call a.destroy, a's id is removed from its subjects' article_ids array fields. But article a is not destroyed. So it seems the callback executes correctly, but the object is not destroyed afterwards.

I debugged this problem by looking at mongoid's source code. The destroy method looks like

def destroy(options = {})
    self.flagged_for_destroy = true
    run_callbacks(:destroy) do
        remove(options)    # THIS IS NOT EXECUTED!
    end.tap do
        self.flagged_for_destroy = false
    end
end

def remove(options = {})
    Operations.remove(self, options).persist
end
alias :delete :remove

The comment is mine. This seems to be a bug with Mongoid, that destroy only executes the callbacks, and does not destroy the object itself.

However, when there's no callback methods for destroy (for example, on a model without the has_and_belongs_to_many relationship), the object is destroyed correctly. Strange

Has anyone has experienced the same problem and if there's any workaround.

Thanks.

Zack Xu
  • 11,505
  • 9
  • 70
  • 78

1 Answers1

0

It could be that :article must be referenced instead of Article. Keep in mind that doing Article.new doesn't automatically give a relation. The mongoid relation doc says you need something like the following

class Person
  include Mongoid::Document
  embeds_many :addresses
end

person.addresses = [ address ]

a work around is add manual deletes to the modals

after_destroy :delete_self!

def delete_self!
if persisted?
  self.delete
end
Eddie
  • 1,428
  • 14
  • 24