Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch updating resources with a resource template may throw a UniqueConstraintViolationException #1690

Closed
jajm opened this issue Feb 24, 2021 · 7 comments

Comments

@jajm
Copy link
Contributor

jajm commented Feb 24, 2021

Hi,

I found a weird bug in the batch update process. Here are the steps to reproduce:

  1. Install this module: https://github.com/jajm/omeka-s-module-UniqueConstraintViolationException. It is a minimal module made only to trigger the bug (the whole code is pasted below)
  2. Create a new item with a resource template
  3. Go to /admin/item, select the new item and start the batch update process using the "edit selected" option
  4. In the batch edit form, add a text value. Any property/value will work. Then submit the form.

You should see the following error message:

Doctrine\DBAL\Exception\UniqueConstraintViolationException
An exception occurred while executing 'INSERT INTO resource_template_property (alternate_label, alternate_comment, position, data_type, is_required, is_private, resource_template_id, property_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, 1, "[]", 0, 0, 1, 1]: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'UNIQ_4689E2F116131EA549213EC'

Details:

PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'UNIQ_4689E2F116131EA549213EC' in /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:117
Stack trace:
#0 /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php(117): PDOStatement->execute(NULL)
#1 /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php(153): Doctrine\DBAL\Driver\PDOStatement->execute(NULL)
#2 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php(287): Doctrine\DBAL\Statement->execute()
#3 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(1087): Doctrine\ORM\Persisters\Entity\BasicEntityPersister->executeInserts()
#4 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(400): Doctrine\ORM\UnitOfWork->executeInserts(Object(Doctrine\ORM\Mapping\ClassMetadata))
#5 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php(368): Doctrine\ORM\UnitOfWork->commit(NULL)
#6 /home/omekas/omeka-s/application/src/Api/Adapter/AbstractEntityAdapter.php(449): Doctrine\ORM\EntityManager->flush()
#7 /home/omekas/omeka-s/application/src/Api/Manager.php(236): Omeka\Api\Adapter\AbstractEntityAdapter->batchUpdate(Object(Omeka\Api\Request))
#8 /home/omekas/omeka-s/application/src/Api/Manager.php(146): Omeka\Api\Manager->execute(Object(Omeka\Api\Request))
#9 /home/omekas/omeka-s/application/src/Mvc/Controller/Plugin/Api.php(171): Omeka\Api\Manager->batchUpdate('items', Array, Array, Array)
#10 /home/omekas/omeka-s/application/src/Controller/Admin/ItemController.php(290): Omeka\Mvc\Controller\Plugin\Api->batchUpdate('items', Array, Array, Array)
#11 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Controller/AbstractActionController.php(77): Omeka\Controller\Admin\ItemController->batchEditAction()
#12 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(321): Laminas\Mvc\Controller\AbstractActionController->onDispatch(Object(Laminas\Mvc\MvcEvent))
#13 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent), Object(Closure))
#14 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(103): Laminas\EventManager\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\Mvc\MvcEvent))
#15 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/DispatchListener.php(139): Laminas\Mvc\Controller\AbstractController->dispatch(Object(Laminas\Http\PhpEnvironment\Request), Object(Laminas\Http\PhpEnvironment\Response))
#16 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(321): Laminas\Mvc\DispatchListener->onDispatch(Object(Laminas\Mvc\MvcEvent))
#17 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent), Object(Closure))
#18 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Application.php(331): Laminas\EventManager\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\Mvc\MvcEvent))
#19 /home/omekas/omeka-s/index.php(21): Laminas\Mvc\Application->run()
#20 {main}

Next Doctrine\DBAL\Driver\PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'UNIQ_4689E2F116131EA549213EC' in /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:119
Stack trace:
#0 /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php(153): Doctrine\DBAL\Driver\PDOStatement->execute(NULL)
#1 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php(287): Doctrine\DBAL\Statement->execute()
#2 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(1087): Doctrine\ORM\Persisters\Entity\BasicEntityPersister->executeInserts()
#3 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(400): Doctrine\ORM\UnitOfWork->executeInserts(Object(Doctrine\ORM\Mapping\ClassMetadata))
#4 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php(368): Doctrine\ORM\UnitOfWork->commit(NULL)
#5 /home/omekas/omeka-s/application/src/Api/Adapter/AbstractEntityAdapter.php(449): Doctrine\ORM\EntityManager->flush()
#6 /home/omekas/omeka-s/application/src/Api/Manager.php(236): Omeka\Api\Adapter\AbstractEntityAdapter->batchUpdate(Object(Omeka\Api\Request))
#7 /home/omekas/omeka-s/application/src/Api/Manager.php(146): Omeka\Api\Manager->execute(Object(Omeka\Api\Request))
#8 /home/omekas/omeka-s/application/src/Mvc/Controller/Plugin/Api.php(171): Omeka\Api\Manager->batchUpdate('items', Array, Array, Array)
#9 /home/omekas/omeka-s/application/src/Controller/Admin/ItemController.php(290): Omeka\Mvc\Controller\Plugin\Api->batchUpdate('items', Array, Array, Array)
#10 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Controller/AbstractActionController.php(77): Omeka\Controller\Admin\ItemController->batchEditAction()
#11 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(321): Laminas\Mvc\Controller\AbstractActionController->onDispatch(Object(Laminas\Mvc\MvcEvent))
#12 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent), Object(Closure))
#13 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(103): Laminas\EventManager\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\Mvc\MvcEvent))
#14 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/DispatchListener.php(139): Laminas\Mvc\Controller\AbstractController->dispatch(Object(Laminas\Http\PhpEnvironment\Request), Object(Laminas\Http\PhpEnvironment\Response))
#15 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(321): Laminas\Mvc\DispatchListener->onDispatch(Object(Laminas\Mvc\MvcEvent))
#16 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent), Object(Closure))
#17 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Application.php(331): Laminas\EventManager\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\Mvc\MvcEvent))
#18 /home/omekas/omeka-s/index.php(21): Laminas\Mvc\Application->run()
#19 {main}

Next Doctrine\DBAL\Exception\UniqueConstraintViolationException: An exception occurred while executing 'INSERT INTO resource_template_property (alternate_label, alternate_comment, position, data_type, is_required, is_private, resource_template_id, property_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, 1, "[]", 0, 0, 1, 1]:

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'UNIQ_4689E2F116131EA549213EC' in /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php:55
Stack trace:
#0 /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php(169): Doctrine\DBAL\Driver\AbstractMySQLDriver->convertException('An exception oc...', Object(Doctrine\DBAL\Driver\PDOException))
#1 /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php(145): Doctrine\DBAL\DBALException::wrapException(Object(Doctrine\DBAL\Driver\PDOMySql\Driver), Object(Doctrine\DBAL\Driver\PDOException), 'An exception oc...')
#2 /home/omekas/omeka-s/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php(162): Doctrine\DBAL\DBALException::driverExceptionDuringQuery(Object(Doctrine\DBAL\Driver\PDOMySql\Driver), Object(Doctrine\DBAL\Driver\PDOException), 'INSERT INTO res...', Array)
#3 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php(287): Doctrine\DBAL\Statement->execute()
#4 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(1087): Doctrine\ORM\Persisters\Entity\BasicEntityPersister->executeInserts()
#5 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(400): Doctrine\ORM\UnitOfWork->executeInserts(Object(Doctrine\ORM\Mapping\ClassMetadata))
#6 /home/omekas/omeka-s/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php(368): Doctrine\ORM\UnitOfWork->commit(NULL)
#7 /home/omekas/omeka-s/application/src/Api/Adapter/AbstractEntityAdapter.php(449): Doctrine\ORM\EntityManager->flush()
#8 /home/omekas/omeka-s/application/src/Api/Manager.php(236): Omeka\Api\Adapter\AbstractEntityAdapter->batchUpdate(Object(Omeka\Api\Request))
#9 /home/omekas/omeka-s/application/src/Api/Manager.php(146): Omeka\Api\Manager->execute(Object(Omeka\Api\Request))
#10 /home/omekas/omeka-s/application/src/Mvc/Controller/Plugin/Api.php(171): Omeka\Api\Manager->batchUpdate('items', Array, Array, Array)
#11 /home/omekas/omeka-s/application/src/Controller/Admin/ItemController.php(290): Omeka\Mvc\Controller\Plugin\Api->batchUpdate('items', Array, Array, Array)
#12 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Controller/AbstractActionController.php(77): Omeka\Controller\Admin\ItemController->batchEditAction()
#13 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(321): Laminas\Mvc\Controller\AbstractActionController->onDispatch(Object(Laminas\Mvc\MvcEvent))
#14 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent), Object(Closure))
#15 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(103): Laminas\EventManager\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\Mvc\MvcEvent))
#16 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/DispatchListener.php(139): Laminas\Mvc\Controller\AbstractController->dispatch(Object(Laminas\Http\PhpEnvironment\Request), Object(Laminas\Http\PhpEnvironment\Response))
#17 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(321): Laminas\Mvc\DispatchListener->onDispatch(Object(Laminas\Mvc\MvcEvent))
#18 /home/omekas/omeka-s/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\EventManager\EventManager->triggerListeners(Object(Laminas\Mvc\MvcEvent), Object(Closure))
#19 /home/omekas/omeka-s/vendor/laminas/laminas-mvc/src/Application.php(331): Laminas\EventManager\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\Mvc\MvcEvent))
#20 /home/omekas/omeka-s/index.php(21): Laminas\Mvc\Application->run()
#21 {main}

The code:

namespace UniqueConstraintViolationException;

use Omeka\Module\AbstractModule;

class Module extends AbstractModule
{
    public function attachListeners($events)
    {
        $events->attach('Omeka\Api\Adapter\ItemAdapter', 'api.update.post', [$this, 'updateItemPost']);
    }

    public function updateItemPost($event)
    {
        $apiAdapters = $this->getServiceLocator()->get('Omeka\ApiAdapterManager');

        $resource = $event->getParam('response')->getContent();
        $apiAdapter = $apiAdapters->get($resource->getResourceName());
        $representation = $apiAdapter->getRepresentation($resource);

        //* When the following block is executed, an exception will be thrown later
        $template = $representation->resourceTemplate();
        if ($template) {
            $template->resourceTemplateProperties();
        }

        /*/ // but no exception will be thrown if only the following block is executed
        $template = $resource->getResourceTemplate();
        if ($template) {
            $template->getResourceTemplateProperties();
        }
        //*/
    }
}

It seems that using the representation causes problems, while using the entity directly does not (which is the weird part of the bug :))
The bug exists at least on 2.1.2, 3.0.1, and develop branch

@zerocrates
Copy link
Member

Well I'm somewhat at a loss for this one...

It obviously seems to be related to the "detach" system we have in place for batch edits to keep them from going wild in terms of memory usage and poor speed, but I don't quite get how it's happening here specifically...

In particular: removing a call that detaches "new" entities after the flush that throws this error eliminates the error. But I can't come up with a mechanism for how that later detach is affecting the earlier flush.

@zerocrates
Copy link
Member

OK so I think I have a better handle on why the error happens here, because the batch edit actually performs several "batch edit" API calls in a row, so it's just that our detach on the first one is affecting the flush of the next one. That makes sense.

The problem here seems to be: we're detaching everything "new," but the templates themselves are already loaded before we start the batch edit. When you access the template properties from within the batch edit, they get stored in the template object. When we go to detach the "new" stuff, that means we detach the template properties (which were not previously loaded) but not the templates. The result is that the templates are still in the Doctrine system, and are holding references to all their properties, which have been detached, so Doctrine sees them as "new" properties and tries to insert them, giving us this error.

The problem at least makes sense but I don't know that I have a solid answer on how to fix it. The detaching is necessary to have reasonable performance and memory usage for large updates (otherwise every item, value, etc. that is touched during an update accumulates in memory). I don't think we really have a way to "reach into" every template and reset its list of properties, at least not in any reasonable way. Doctrine is moving to deprecate its "detach" method and this kind of mess is essentially the reason why.

One option is: just skip detaching resource template properties specifically, to avoid this specific bug only. That will probably work, actually, but it's a little "dirtier" of a fix than I'd really prefer. I may end up having to go with this one anyway, though: it's almost certainly the simplest solution by far.

We could also just skip the detaching process altogether in "synchronous" contexts like this (a non-background batch edit): it's really there for long-running background processes. I believe the templates are loaded here due to something else using/loading them earlier during the request, a complication that doesn't occur for a background job.

@jajm
Copy link
Contributor Author

jajm commented Feb 25, 2021

Thank you for looking at it.

Apparently it is possible to cascade a merge operation (https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/working-with-associations.html#transitive-persistence-cascade-operations). So instead of detaching entities we could clear the entity manager after the flush, then reattach "old" entities using cascading merge, so that all associated entities are re-attached as well. I never used that though, so I may be completely wrong.

@zerocrates
Copy link
Member

zerocrates commented Feb 25, 2021

Clear-then-merge probably won't help here: the issue is having the Template in the set of "old" things and not its properties. Regardless of the method used, the same kind of problem is going to occur on subsequent flushes. Cascade merge could account for some of that, but it's likely to bring in too much and blunt the point of the clear/detach in the first place. Maybe it could be done in a targeted enough way that it would work, though.

Another issue, I believe I'm remembering correctly that Doctrine is moving away from merge as well.

zerocrates added a commit that referenced this issue Feb 25, 2021
The detach process used in batch edit/create is pretty brittle,
especially in "synchronous" or in-process contexts where many more
entities are loaded before the batch edit begins, so the results are
less predictable. Typical "bad" results are Doctrine errors about
duplicates or encountering "new" un-persisted entities through a
relationship.

The detach also serves very little purpose in this
context, as it's only there to improve performance _between_ multiple
batches, and the in-process edits only do a single batch.

This commit adds a new request option for batch edits and creates,
"detachEntity", defaulted to true, and has the controllers for the
in-process batch updates set it to false.

(#1690)
@zerocrates
Copy link
Member

The strategy I'm going to go with for now is just eliminating the detach process from these kinds of batch edits, which should resolve this specific issue.

jajm pushed a commit to biblibre/omeka-s that referenced this issue Feb 26, 2021
The detach process used in batch edit/create is pretty brittle,
especially in "synchronous" or in-process contexts where many more
entities are loaded before the batch edit begins, so the results are
less predictable. Typical "bad" results are Doctrine errors about
duplicates or encountering "new" un-persisted entities through a
relationship.

The detach also serves very little purpose in this
context, as it's only there to improve performance _between_ multiple
batches, and the in-process edits only do a single batch.

This commit adds a new request option for batch edits and creates,
"detachEntity", defaulted to true, and has the controllers for the
in-process batch updates set it to false.

(omeka#1690)

(cherry picked from commit 578dd20)
@jajm
Copy link
Contributor Author

jajm commented Feb 26, 2021

Thanks. It works perfectly, and it applies nicely to 2.1, which is even more perfect :)

@zerocrates
Copy link
Member

I think we'll call this one fixed, then.

zerocrates added a commit that referenced this issue Mar 26, 2021
The detach process used in batch edit/create is pretty brittle,
especially in "synchronous" or in-process contexts where many more
entities are loaded before the batch edit begins, so the results are
less predictable. Typical "bad" results are Doctrine errors about
duplicates or encountering "new" un-persisted entities through a
relationship.

The detach also serves very little purpose in this
context, as it's only there to improve performance _between_ multiple
batches, and the in-process edits only do a single batch.

This commit adds a new request option for batch edits and creates,
"detachEntity", defaulted to true, and has the controllers for the
in-process batch updates set it to false.

(#1690)

(cherry picked from commit 578dd20)
loyolakain pushed a commit to scdantilles/omeka-s that referenced this issue Feb 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants