Рейтинг:2

Уникальное ограничение для нескольких полей сущностей не работает, когда > 1 запрос POST jsonapi происходит сразу после другого

флаг cn

В полностью отделенном проекте drupal 9 у меня есть собственный тип объекта и добавлено уникальное ограничение для нескольких полей, как описано здесь. Это работает хорошо, и добавление второго объекта с теми же значениями поля невозможно. Однако я использую запросы JSONAPI POST для создания объектов. Я заметил, что при отправке нескольких запросов POST с одинаковыми значениями полей сразу после другого метод валидатора (используя entityTypeManager->getStorage(...)->getQuery(...)->условие(...)->выполнить() для проверки БД) не возвращает другие сущности, так как еще не существует повторяющейся сущности. т.е. это происходит так быстро, что несколько сущностей с одинаковыми значениями создаются с одной и той же временной меткой (The созданный значения сущностей идентичны)!

Обход ограничения опасен и должен быть предотвращен.

Что я могу сделать, чтобы решить эту проблему?

Обновлять Это функция, которая вызывается внутри ConstraintValidator

проверка публичной функции ($entity, ограничение $constraint)
{
  ...
  если (!$this->isUnique($entity))
    $this->context->addViolation($constraint->notUnique);
  ...
}
частная функция isUnique (CustomType $entity) {
  $date = $entity->get('date')->value;
  $type = $entity->bundle();
  $employee = $entity->get('employee')->target_id;
  $query = $this->entityTypeManager->getStorage('custom_type')->getQuery()
    ->условие('статус', 1)
    ->условие('тип', $тип)
    ->условие('сотрудник', $сотрудник)
    ->условие('дата', $дата);

  если (!is_null($entity->id()))
    $query->условие('id', $entity->id(), '<>');

  $workIds = $запрос->выполнить();
  вернуть пустой ($workIds);
}

Я рад найти любые недостатки. Пока этот код хорошо работает во всех остальных случаях.

Обновить Drupal::lock()

Я реализовал 2 подписчика событий для добавления и выпуска \Друпал::блокировка() как упоминалось в комментариях. Используя xdebug, я могу подтвердить, что код запущен, однако блокировка, похоже, не имеет никакого эффекта. Документация для замок() довольно ограничен. Не уверен, что здесь не так.

<?php

пространство имен Drupal\custom_entities\EventSubscriber;

используйте Symfony\Component\EventDispatcher\EventSubscriberInterface;
используйте Symfony\Component\HttpKernel\Event\RequestEvent;
используйте Symfony\Component\HttpKernel\KernelEvents;

класс JsonApiRequestDBLock реализует EventSubscriberInterface {

  /**
   * Добавляет блокировку для запросов JSON:API.
   *
   * @param\Symfony\Component\HttpKernel\Event\RequestEvent $event
   * Событие для обработки.
   */
  публичная функция onRequest(RequestEvent $event) {
    $запрос = $event->getRequest();
    если ($request->getRequestFormat() !== 'api_json') {
      возвращаться;
    }

    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}
   */
  общедоступная статическая функция getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['onRequest'];
    вернуть $события;
  }

}

и снять блокировку после ответа

<?php

пространство имен Drupal\custom_entities\EventSubscriber;

используйте Symfony\Component\EventDispatcher\EventSubscriberInterface;
используйте Symfony\Component\HttpKernel\Event\ResponseEvent;
используйте Symfony\Component\HttpKernel\KernelEvents;

класс JsonApiResponseDBRelease реализует EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  общедоступная статическая функция getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = ['onResponse'];
    вернуть $события;
  }


  /**
   * Выпуск ответов JSON:API.
   *
   * @param\Symfony\Component\HttpKernel\Event\ResponseEvent $event
   * Событие для обработки.
   */
  публичная функция onResponse (ResponseEvent $ событие) {
    $ответ = $event->getResponse();
    if (strpos($response->headers->get('Content-Type'), 'application/vnd.api+json') === FALSE) {
      возвращаться;
    }
    $запрос = $event->getRequest();
    if ($request->attributes->get('_route') === 'jsonapi.custom_type--work.collection.post' &&
      $request->attributes->get('_controller') === 'jsonapi.entity_resource:createIndividual'
    ) {
      // Снять блокировку.
      $lock = \Drupal::lock();
      если (!$lock->lockMayBeAvailable('custom_create_lock'))
        $lock->release('custom_create_lock');
    }
  }

}

Это было добавлено в services.yml

  # Подписчики событий.
  custom_entities.jsonapi_db_lock.subscriber:
    класс: Drupal\custom_entities\EventSubscriber\JsonApiRequestDBLock
    теги:
      - {имя: event_subscriber}
  custom_entities.jsonapi_response_db_release.subscriber:
    класс: Drupal\custom_entities\EventSubscriber\JsonApiResponseDBRelease
    теги:
      - {имя: event_subscriber}
Jaypan avatar
флаг de
Похоже, что этого не должно происходить, так как почти невозможно сохранить две сущности одновременно, одна должна произойти раньше другой, поэтому вторая должна попасть в ограничение. Вы уверены, что ваш код ограничения правильный?
флаг cn
Я согласен, и я довольно запутался в данный момент. Я добавил оператор `\Drupal::logger` внутри `isUnique()`, регистрирующий текущее время. Он вызывается несколько раз в одну и ту же секунду
apaderno avatar
флаг us
В строке `$this->entityTypeManager->getStorage('custom_type')->getQuery()` отсутствует вызов `accessCheck(FALSE)`, который необходим для того, чтобы запрос игнорировал любое разрешение на доступ вошедшего в систему пользователя. может иметь для запрошенных объектов.
4uk4 avatar
флаг cn
Я не думаю, что это проблема здесь. Речь идет о параллельных запросах. Как отметил @Jaypan, очень маловероятно, что одни и те же полевые данные отправляются дважды в течение секунды или двух. Но если это может случиться, вам нужен какой-то запирающий механизм. Вероятно, невозможно установить блокировку базы данных для задействованных таблиц, поэтому вам нужен независимый механизм блокировки, такой как https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Lock%21LockBackendInterface.php/ группа/блокировка
флаг cn
@ 4k4 Я согласен, очень маловероятно, что одни и те же данные поля отправляются дважды, и со мной этого никогда не случалось. Однако это может случиться. Я тоже думал о блокировке БД, и благодаря вашему комментарию я проверю ссылку на механизмы блокировки
4uk4 avatar
флаг cn
Вам нужно применить блокировку ко всему запросу, а не только к ограничению. Либо путем переопределения контроллера, либо с помощью подписчиков событий: KernelEvents::REQUEST для получения блокировки, KernelEvents::RESPONSE для снятия блокировки и KernelEvents::EXCEPTION для обработки исключений, вызванных блокировкой.
флаг cn
@ 4k4 Я обновил вопрос, показывающий мою попытку работать с блокировками. Эффекта вроде нет. я все еще могу что-то пропустить
4uk4 avatar
флаг cn
Ваша блокировка ничего не делает. Если вам не удалось получить блокировку, вы должны создать исключение. При желании вы можете поставить цикл ожидания, прежде чем сделать это.
Рейтинг:2
флаг us

Не видя весь код, используемый для custom_type сущность, включая код для ее обработчиков и отвечая на вопрос, почему код не находит дубликаты, я могу предположить, что в показанном коде есть два «недостатка».

Первый «недостаток» заключается в том, что запрос на поиск существующих сущностей более ограничен, чем должен. Это означает, что он проверяет поля сущностей, которые должны быть нерелевантными, чтобы сущности были дубликатами, например:

  • положение дел поле, предполагая, что это положение дел поле, используемое основными объектами Drupal
  • датировать поле, при условии, что оно содержит дату/временную метку создания

Единственная основная сущность Drupal, которая использует проверку сущности, чтобы избежать создания дублирующихся сущностей, — это path_alias организация, реализуемая Псевдоним пути учебный класс. Эта сущность имеет положение дел поле, оно поддерживает ревизии, но у него нет ни поля для хранения, когда оно было создано, ни сущности-владельца (как у узлов).
Валидатор UniquePathAliasConstraintValidator является его средством проверки ограничения сущности; код для UniquePathAliasConstraintValidator::validate() является следующим.

  $path = $entity->getPath();
  $alias = $entity->getAlias();
  $langcode = $entity->language()->getId();
  $storage = $this->entityTypeManager->getStorage('path_alias');
  $запрос = $хранилище->getQuery()
    ->Проверка доступа(ЛОЖЬ)
    ->условие('псевдоним', $псевдоним, '=')
    ->условие('langcode', $langcode, '=');
  если (!$entity->isNew()) {
    $query->условие('id', $entity->id(), '<>');
  }
  если ($путь) {
    $query->условие('путь', $путь, '<>');
  }
  если ($результат = $запрос->диапазон(0, 1)->выполнить()) {
    $existing_alias_id = сброс($результат);
    $existing_alias = $storage->load($existing_alias_id);
    если ($existing_alias->getAlias() !== $alias) {
      $this->context->buildViolation($constraint-> DifferentCapitalizationMessage, ['%alias' => $alias, '%stored_alias' => $existing_alias->getAlias()])
        ->добавитьнарушение();
    }
    еще {
      $this->context->buildViolation($constraint->message, ['%alias' => $alias])
        ->добавитьнарушение();
    }
  }
}

Используя этот код в качестве примера и используя только код, который строго проверяет наличие дубликатов, в вашем случае я бы использовал следующий код. (Я показываю только код для уникальный().)

частная функция isUnique (CustomType $entity) {
  $date = $entity->get('date')->value;
  $type = $entity->bundle();
  $employee = $entity->get('employee')->target_id;
  $query = $this->entityTypeManager->getStorage('custom_type')
    -> получить запрос ()
    ->Проверка доступа(ЛОЖЬ)
    ->условие('тип', $тип)
    ->условие('сотрудник', $сотрудник)
    ->условие('дата', $дата);

  если (!$entity->isNew()) {
    $query->условие('id', $entity->id(), '<>');
  }

  $result = $query->range(0, 1)->execute();
  вернуть пустой ($ результат);
}

Я добавил звонок в проверка доступа(ЛОЖЬ) потому что, не видя кода, используемого обработчиком доступа к сущности, и не зная, есть ли у сущности сущность-владелец, я не могу исключить реализацию уникальный() не находит дубликатов, потому что текущий пользователь, вошедший в систему, не имеет доступа к дубликатам (или любому объекту этого типа). Без звонка проверка доступа(ЛОЖЬ), запрос вернет сущности, к которым имеет доступ текущий пользователь, вошедший в систему.
(пропущенный вызов проверка доступа(ЛОЖЬ) это другой возможный «недостаток», который я вижу в показанном коде.)

флаг cn
Спасибо! Я пробовал этот код, играл и тестировал много раз. Однако актуальная проблема остается: с двумя одновременными запросами мой валидатор не предотвращает дублирование объектов. Я копаю глубже и держу этот вопрос в курсе.
apaderno avatar
флаг us
Используется ли поле *date* в вашем коде подобно полю *created*, используемому для узлов? Если это так, я бы не стал искать существующие сущности с тем же значением *date*.
флаг cn
не совсем уверен, что именно вы имеете в виду, но дата очень важна для проверки уникальности
apaderno avatar
флаг us
Я имею в виду, что если это поле содержит информацию о том, когда объект был создан, я бы не стал включать его в запрос на поиск дубликатов. Понятно, что если сущность предназначена, например, для событий, а *дата* является запланированной датой события, то я бы включил ее.

Ответить или комментировать

Большинство людей не понимают, что склонность к познанию нового открывает путь к обучению и улучшает межличностные связи. В исследованиях Элисон, например, хотя люди могли точно вспомнить, сколько вопросов было задано в их разговорах, они не чувствовали интуитивно связи между вопросами и симпатиями. В четырех исследованиях, в которых участники сами участвовали в разговорах или читали стенограммы чужих разговоров, люди, как правило, не осознавали, что задаваемый вопрос повлияет — или повлиял — на уровень дружбы между собеседниками.