doctrine / mongodb-odm

The Official PHP MongoDB ORM/ODM
https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/
MIT License
1.09k stars 502 forks source link

Issue in flush When duplication index error occurs. #2625

Closed aanair08 closed 5 months ago

aanair08 commented 5 months ago

I have a document called Customer, Which have a field named CustomerCode and added unique index. I have been trying to create Customer

try {
            $customer = new Customer();
            $customer->setCustomerCode($customerCode);
            $customer->setCustomerName($customerName);
            $dm->persist($customer);
            $dm->flush();

        } catch (\Exception $e) {
            if ((int) $e->getCode() === (int) self::MONGO_DUPLICATE_KEY_ERR_CODE) {
                // duplicate customer code insert into tracking
                $duplicateCustomer = new DuplicateCustomer();
                $duplicateCustomer->setCustomerCode($customerCode);
                $duplicateCustomer->setCustomerName($customerName);
                $dm->persist($duplicateCustomer);
                $dm->flush();
            }
        }

This above code was working fine before. After the version changed 2.7.0 data not inserted into Duplicate Customer and show error in log saying that duplicate error in CustomerCode in Cutomer. I can not figure out the issue in there, since the code was working fine.

alcaeus commented 5 months ago

The logic you were relying on was broken to begin with and fixed in 2.7.0. Before 2.7.0, the UnitOfWork unscheduled workloads (e.g. document insertions) before the write was actually completed. This led to situations where UnitOfWork thought that documents were successfully inserted when they weren't. So after that first flush that causes the duplicate key error, ODM thought that the customer was inserted successfully and on the subsequent call to flush that inserts the DuplicateCustomer document didn't handle $customer at all.

In 2.7.0, this changed and the pending workload in the UnitOfWork is only marked as completed when it was actually completed in the database. Since the $customer couldn't be saved to the database, it is still marked as a pending insertion and left for you to figure out. On the next flush, it will once again attempt to save the document to the database, which of course will fail again.

The reason for making this change is twofold: for one, it is required to support transactions. In MongoDB, transactions can be retried after certain errors, so we have to remember pending writes until each write has completed. The other issue is that before the change the UnitOfWork state was broken: the document was marked as fully persisted when it wasn't - this could lead to further issues down the line.

Since you are handling the error yourself, the correct course of action here is to detach the $customer document so that it will no longer be managed by the UnitOfWork:

try {
    $customer = new Customer();
    $customer->setCustomerCode($customerCode);
    $customer->setCustomerName($customerName);
    $dm->persist($customer);
    $dm->flush();
} catch (\Exception $e) {
    if ($e->getCode() === self::MONGO_DUPLICATE_KEY_ERR_CODE) {
        // detach original customer from document manager
        $dm->detach($customer);

        // duplicate customer code insert into tracking
        $duplicateCustomer = new DuplicateCustomer();
        $duplicateCustomer->setCustomerCode($customerCode);
        $duplicateCustomer->setCustomerName($customerName);
        $dm->persist($duplicateCustomer);
        $dm->flush();
    }
}

FWIW, situations like this one are why ORM closes the entity manager after any exception that occurs during a write. We have chosen not to go that route and instead leave the DocumentManager in the (broken) dirty state for the user to clean up. So, if you decide to handle errors during flushing, make sure to clean up before attempting to flush again.