In a fully decoupled drupal 9 project I have a custom entity type and added a unique constraint for several fields just as described here. This works well and adding a second entity with the same field values is not possible. However, I'm using JSONAPI POST requests to create the entities. I noticed that when issuing multiple POST requests with the exact same field values right after another, the validator method (using the entityTypeManager->getStorage(...)->getQuery(...)->condition(...)->execute()
to check the DB) does not return other entities, as no duplicate entity exists yet. I.e. it happens so fast that multiple identical-valued entities are created at the exact same timestamp (The created
values of the entities are identical)!
Bypassing the constraint is dangerous and must be prevented.
What can I do to solve this problem?
Update
This is the function that is called inside the ConstraintValidator
public function validate($entity, Constraint $constraint)
{
...
if (!$this->isUnique($entity))
$this->context->addViolation($constraint->notUnique);
...
}
private function isUnique(CustomType $entity) {
$date = $entity->get('date')->value;
$type = $entity->bundle();
$employee = $entity->get('employee')->target_id;
$query = $this->entityTypeManager->getStorage('custom_type')->getQuery()
->condition('status', 1)
->condition('type', $type)
->condition('employee', $employee)
->condition('date', $date);
if (!is_null($entity->id()))
$query->condition('id', $entity->id(), '<>');
$workIds = $query->execute();
return empty($workIds);
}
I'm happy to find any flaws. So far this code works well in all other cases.
Update Drupal::lock()
I've implemented 2 event subscribers to add and release \Drupal::lock()
as mentioned in the comments. Using xdebug I can confirm that the code is run, however, the lock does not seem to have any effect. The documentation for lock()
is rather limited. Not sure what's wrong here.
<?php
namespace Drupal\custom_entities\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class JsonApiRequestDBLock implements EventSubscriberInterface {
/**
* Adds a lock for JSON:API requests.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to process.
*/
public function onRequest(RequestEvent $event) {
$request = $event->getRequest();
if ($request->getRequestFormat() !== 'api_json') {
return;
}
if ($request->attributes->get('_route') === 'jsonapi.custom_type--work.collection.post' &&
$request->attributes->get('_controller') === 'jsonapi.entity_resource:createIndividual'
) {
$lock = \Drupal::lock();
$lock->acquire('custom_create_lock');
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = ['onRequest'];
return $events;
}
}
and release the lock after the response
<?php
namespace Drupal\custom_entities\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class JsonApiResponseDBRelease implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = ['onResponse'];
return $events;
}
/**
* Release JSON:API responses.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The event to process.
*/
public function onResponse(ResponseEvent $event) {
$response = $event->getResponse();
if (strpos($response->headers->get('Content-Type'), 'application/vnd.api+json') === FALSE) {
return;
}
$request = $event->getRequest();
if ($request->attributes->get('_route') === 'jsonapi.custom_type--work.collection.post' &&
$request->attributes->get('_controller') === 'jsonapi.entity_resource:createIndividual'
) {
// Release the lock.
$lock = \Drupal::lock();
if (!$lock->lockMayBeAvailable('custom_create_lock'))
$lock->release('custom_create_lock');
}
}
}
This was added to the services.yml
# Event subscribers.
custom_entities.jsonapi_db_lock.subscriber:
class: Drupal\custom_entities\EventSubscriber\JsonApiRequestDBLock
tags:
- { name: event_subscriber }
custom_entities.jsonapi_response_db_release.subscriber:
class: Drupal\custom_entities\EventSubscriber\JsonApiResponseDBRelease
tags:
- { name: event_subscriber }
\Drupal::logger
statement inside theisUnique()
, logging the current time. It gets called multiple times at the exact same second$this->entityTypeManager->getStorage('custom_type')->getQuery()
line is missing a call toaccessCheck(FALSE)
, which is necessary for the query to ignore any access permission the logged-in user may have for the queried entities.