Search code examples
ruby-on-railsvalidationmongoidautosave

Why does Mongoid's "presence" validation enable autosave?


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]

Setup

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.

Change

After a while, Customer needs a new field with a default value:

class Customer
  # ...
  field :premium, type: Boolean, default: false
end

Problem

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.

Cause

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]}

Solution

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

Open Questions

But the question remains: Why does Mongoid's "presence" validation enable autosave? How could this be the desired default behavior? Please enlighten me.


Solution

  • 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.