I had a hard time debugging this one and I was about to ask for help. But I've managed to identify the cause and would like to share my findings in case someone else is running into the same problem. [And maybe someone can explain why it works the way it does]
Let's say I have two Mongoid-Documents, Customer
and Order
with a 1:n relation. In addition, Customer
has an after_save
callback to synchronize document changes with an external API:
class Customer
include Mongoid::Document
has_many :orders
after_save do
puts "synchronizing customer" # <- not my actual code
end
end
class Order
include Mongoid::Document
belongs_to :customer
validates_presence_of :customer
end
Everything works as expected. Creating and updating customers results in the after_save
callback being triggered and creating orders does not.
After a while, Customer
needs a new field with a default value:
class Customer
# ...
field :premium, type: Boolean, default: false
end
But all of a sudden, things get weird. After this change, creating (or updating) orders result in the customer being saved as well! (I noticed this because of my logs – the synchronization was running for no apparent reason)
c = Customer.last
c.orders.create
synchronizing customer # <- what the?
#=> #<Order _id: 575a995aab265d730b8bddba ...>
Strangely, this only happens for existing customers and it only occurs once.
A long and tedious debugging session revealed that the Order
's belongs_to
relation has an autosave
flag:
Order.relations['customer'].autosave?
#=> true
It was enabled by the presence validation and in fact, Mongoid's documentation notes it casually:
Note that autosave functionality will automatically be added to a relation when using
accepts_nested_attributes_for
or validating presence of the relation.
But autosave
only saves a document if it was changed, so where did the change come from? Apparently, my new premium
field with the default value introduced a subtle change:
c = Customer.first # a customer from before the change without "premium" attribute
c.changed?
#=> true
c.changes
#=> {"premium"=>[nil, false]}
After all, the fix was quite trivial. I just had to explicitly disable autosave on my belongs_to
relation:
class Order
include Mongoid::Document
belongs_to :customer, autosave: false
validates_presence_of :customer
end
But the question remains: Why does Mongoid's "presence" validation enable autosave? How could this be the desired default behavior? Please enlighten me.
It appears that this auto-enabling of autosave has been added to Mongoid 3.0 on purpose so that its behavior is consistent with ActiveRecord. Have a look at these two issues:
The second issue links to an ActiveRecord doc which indeed seems to behave the same way, let's particularly quote the following statement:
Note that
autosave: false
is not same as not declaring:autosave
. When the:autosave
option is not present then new association records are saved but the updated association records are not saved.
Here is a link to the source code of the Mongoid feature itself.
Also, from all these sources it turns out that you solution was perfect and you indeed should specifically state :autosave => false
to disable this feature.