Puncte:2

Constrângerea unică asupra mai multor câmpuri de entitate eșuează atunci când >1 solicitări jsonapi POST au loc imediat după alta

drapel cn

Într-un proiect drupal 9 complet decuplat, am un tip de entitate personalizat și am adăugat o constrângere unică pentru mai multe câmpuri, așa cum este descris Aici. Acest lucru funcționează bine și adăugarea unei a doua entitati cu aceleași valori de câmp nu este posibilă. Cu toate acestea, folosesc cereri JSONAPI POST pentru a crea entitățile. Am observat că atunci când emit mai multe solicitări POST cu exact aceleași valori de câmp imediat după alta, metoda validatorului (folosind entityTypeManager->getStorage(...)->getQuery(...)->condition(...)->execute() pentru a verifica DB) nu returnează alte entități, deoarece nu există încă o entitate duplicată. i.e. se întâmplă atât de repede încât mai multe entități cu valori identice sunt create exact la aceeași amprentă temporală (The creată valorile entităților sunt identice)!

Ocolirea constrângerii este periculoasă și trebuie prevenită.

Ce pot face pentru a rezolva această problemă?

Actualizați Aceasta este funcția numită în interiorul ConstraintValidator

validare funcție publică ($entity, Constraint $constraint)
{
  ...
  dacă (!$acest->este Unic($entitate))
    $this->context->addViolation($constraint->notUnique);
  ...
}
funcția privată esteUnică(CustomType $entity) {
  $date = $entity->get('data')->value;
  $tip = $entitate->bundle();
  $angajat = $entitate->get('angajat')->target_id;
  $query = $this->entityTypeManager->getStorage('custom_type')->getQuery()
    ->condition('status', 1)
    ->condition('tip', $tip)
    ->condition('angajat', $angajat)
    ->condition('data', $data);

  dacă (! este_null($entity->id()))
    $query->condition('id', $entity->id(), '<>');

  $workIds = $query->execute();
  return empty($workIds);
}

Mă bucur să găsesc orice defecte. Până acum, acest cod funcționează bine în toate celelalte cazuri.

Actualizați Drupal::lock()

Am implementat 2 abonați la eveniment pentru a adăuga și a elibera \Drupal::lock() asa cum se mentioneaza in comentarii. Folosind xdebug, pot confirma că codul este rulat, totuși, blocarea nu pare să aibă niciun efect. Documentatia pentru Lacăt() este destul de limitat. Nu sunt sigur ce este în neregulă aici.

<?php

spațiu de nume Drupal\custom_entities\EventSubscriber;

utilizați Symfony\Component\EventDispatcher\EventSubscriberInterface;
utilizați Symfony\Component\HttpKernel\Event\RequestEvent;
utilizați Symfony\Component\HttpKernel\KernelEvents;

clasa JsonApiRequestDBLock implementează EventSubscriberInterface {

  /**
   * Adaugă o blocare pentru solicitările JSON:API.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   * Evenimentul de procesat.
   */
  funcția publică onRequest(RequestEvent $event) {
    $cerere = $eveniment->getRequest();
    if ($request->getRequestFormat() !== 'api_json') {
      întoarcere;
    }

    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}
   */
  funcție publică statică getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['onRequest'];
    returnează $evenimente;
  }

}

și eliberați blocarea după răspuns

<?php

spațiu de nume Drupal\custom_entities\EventSubscriber;

utilizați Symfony\Component\EventDispatcher\EventSubscriberInterface;
utilizați Symfony\Component\HttpKernel\Event\ResponseEvent;
utilizați Symfony\Component\HttpKernel\KernelEvents;

clasa JsonApiResponseDBRelease implementează EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  funcție publică statică getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = ['onResponse'];
    returnează $evenimente;
  }


  /**
   * Eliberați răspunsuri JSON:API.
   *
   * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
   * Evenimentul de procesat.
   */
  funcția publică onResponse(ResponseEvent $event) {
    $răspuns = $eveniment->getResponse();
    if (strpos($response->headers->get('Content-Type'), 'application/vnd.api+json') === FALSE) {
      întoarcere;
    }
    $cerere = $eveniment->getRequest();
    if ($request->attributes->get('_route') === 'jsonapi.custom_type--work.collection.post' &&
      $request->attributes->get('_controller') === 'jsonapi.entity_resource:createIndividual'
    ) {
      // Eliberează blocarea.
      $lock = \Drupal::lock();
      dacă (!$lock->lockMayBeAvailable('custom_create_lock'))
        $lock->release('custom_create_lock');
    }
  }

}

Aceasta a fost adăugată la servicii.yml

  # abonați la eveniment.
  custom_entities.jsonapi_db_lock.subscriber:
    clasa: Drupal\custom_entities\EventSubscriber\JsonApiRequestDBLock
    Etichete:
      - { nume: event_subscriber }
  custom_entities.jsonapi_response_db_release.subscriber:
    clasa: Drupal\custom_entities\EventSubscriber\JsonApiResponseDBRelease
    Etichete:
      - { nume: event_subscriber }
Jaypan avatar
drapel de
Asta nu pare să se întâmple, deoarece ar fi aproape imposibil să salvezi două entități în același timp, una trebuie să se întâmple înaintea celeilalte, așa că a doua ar trebui să fie prinsă de constrângere. Sunteți sigur că codul dvs. de constrângere este corect?
drapel cn
Sunt de acord și sunt destul de confuz în acest moment. Am adăugat o instrucțiune `\Drupal::logger` în interiorul `isUnique()`, înregistrând ora curentă. Este apelat de mai multe ori exact în aceeași secundă
apaderno avatar
drapel us
Linia `$this->entityTypeManager->getStorage('custom_type')->getQuery()` lipsește un apel la `accessCheck(FALSE)`, care este necesar pentru ca interogarea să ignore orice permisiune de acces a utilizatorului conectat. poate avea pentru entitățile interogate.
4uk4 avatar
drapel cn
Nu cred că aceasta este problema aici. Este vorba despre cereri concurente. După cum a menționat @Jaypan, este foarte puțin probabil ca aceleași date de câmp să fie trimise de două ori într-o secundă sau două. Dar dacă acest lucru se poate întâmpla, aveți nevoie de un fel de mecanism de blocare. Probabil că nu este posibilă blocarea bazei de date pe tabelele implicate, așa că aveți nevoie de un mecanism de blocare independent, cum ar fi https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Lock%21LockBackendInterface.php/ grup/blocare
drapel cn
@4k4 Sunt de acord, este foarte puțin probabil ca aceleași date de câmp să fie trimise de două ori și nu mi s-a întâmplat niciodată înainte. Cu toate acestea, se poate întâmpla. M-am gândit și la blocarea DB și, datorită comentariului tău, voi verifica linkul mecanismelor de blocare
4uk4 avatar
drapel cn
Trebuie să aplicați blocarea întregii cereri, nu numai constrângerii. Fie prin suprascrierea controlerului, fie prin utilizarea abonaților de evenimente, KernelEvents::REQUEST pentru a obține blocarea, KernelEvents::RESPONSE pentru a elibera blocarea și KernelEvents::EXCEPTION pentru a gestiona excepțiile generate din cauza blocării.
drapel cn
@4k4 Am actualizat întrebarea care arată încercarea mea de a lucra cu încuietori. Nu pare să aibă efect. S-ar putea să-mi lipsească încă ceva
4uk4 avatar
drapel cn
Blocarea ta nu face nimic. Dacă nu reușiți să obțineți blocarea, trebuie să ridicați o excepție. Opțional, puteți pune o buclă de așteptare înainte de a face acest lucru.
Puncte:2
drapel us

Fără a vedea tot codul folosit pentru tip_personalizat entitate, inclusiv codul pentru gestionatorii săi și răspunzând la motivul pentru care codul nu găsește duplicate, există două „defecte” pe care mi le pot imagina posibile în codul afișat.

Primul „defect” este că interogarea de căutare a entităților existente este mai restrictivă decât ar trebui. Aceasta înseamnă că verifică câmpurile de entități care ar trebui să fie irelevante pentru ca entitățile să fie duplicate, de exemplu:

  • The stare câmp, presupunând că este stare câmp folosit de entitățile de bază Drupal
  • The Data câmp, presupunând că conține data/marca temporală a creării

Singura entitate de bază Drupal care utilizează o validare a entității pentru a evita crearea de entități duplicate este alias_cale entitate, implementată de către PathAlias clasă. Acea entitate are un stare câmp, acceptă revizuiri, dar nu are un câmp de stocat atunci când a fost creat și nici nu are o entitate proprietară (cum au nodurile).
UniquePathAliasConstraintValidator este validatorul său de constrângeri de entitate; codul pentru UniquePathAliasConstraintValidator::validate() este urmatoarea.

  $cale = $entity->getPath();
  $alias = $entity->getAlias();
  $langcode = $entity->language()->getId();
  $storage = $this->entityTypeManager->getStorage('path_alias');
  $query = $storage->getQuery()
    ->accessCheck(FALSE)
    ->condition('alias', $alias, '=')
    ->condition('langcode', $langcode, '=');
  dacă (!$entity->isNew()) {
    $query->condition('id', $entity->id(), '<>');
  }
  dacă ($cale) {
    $query->condition('cale', $cale, '<>');
  }
  if ($rezultat = $interogare->interval(0, 1)->execute()) {
    $existing_alias_id = reset($rezultat);
    $existing_alias = $storage->load($existing_alias_id);
    if ($alias_existent->getAlias() !== $alias) {
      $this->context->buildViolation($constraint->differentCapitalizationMessage, ['%alias' => $alias, '%stored_alias' => $existing_alias->getAlias()])
        ->addViolation();
    }
    else {
      $this->context->buildViolation($constraint->message, ['%alias' => $alias])
        ->addViolation();
    }
  }
}

Folosind acel cod ca exemplu și folosind doar codul care verifică strict dacă există duplicate, în cazul dvs. aș folosi următorul cod. (Arăt doar codul pt este unic().)

funcția privată esteUnică(CustomType $entity) {
  $date = $entity->get('data')->value;
  $tip = $entitate->bundle();
  $angajat = $entitate->get('angajat')->target_id;
  $interogare = $this->entityTypeManager->getStorage('custom_type')
    ->getQuery()
    ->accessCheck(FALSE)
    ->condition('tip', $tip)
    ->condition('angajat', $angajat)
    ->condition('data', $data);

  dacă (!$entity->isNew()) {
    $query->condition('id', $entity->id(), '<>');
  }

  $rezultat = $interogare->interval(0, 1)->execute();
  returnează gol ($rezultat);
}

Am adăugat apelul la accessCheck(FALSE) deoarece, fără să văd codul folosit de handler-ul de acces al entității și fără să știu dacă entitatea are o entitate proprietară, nu pot exclude implementarea este unic() nu găsește duplicate, deoarece utilizatorul conectat în prezent nu are acces la duplicate (sau la orice entitate de acest tip). Fără să suni accessCheck(FALSE), interogarea va returna entitățile la care are acces utilizatorul conectat în prezent.
(Apelul lipsă către accessCheck(FALSE) este celălalt „defect” posibil pe care îl pot vedea în codul afișat.)

drapel cn
Mulțumiri! Am încercat acest cod, am jucat și am testat de multe ori. Cu toate acestea, problema reală rămâne, cu 2 solicitări simultane, validatorul meu nu împiedică entitățile duplicate. Sapă mai adânc și țin această problemă la zi.
apaderno avatar
drapel us
Este câmpul *data* folosit de codul dvs. ca și câmpul *creat* folosit pentru noduri? Dacă acesta este cazul, nu aș căuta entități existente cu aceeași valoare pentru *data*.
drapel cn
nu sunt sigur ce anume vrei să spui, dar data este foarte importantă pentru verificarea unicității
apaderno avatar
drapel us
Adică, dacă acel câmp conține când a fost creată entitatea, nu l-aș include în interogare pentru a găsi duplicate. În mod clar, dacă entitatea este pentru evenimente, de exemplu, și *data* este data programată pentru eveniment, atunci aș include-o.

Postează un răspuns

Majoritatea oamenilor nu înțeleg că a pune multe întrebări deblochează învățarea și îmbunătățește legătura interpersonală. În studiile lui Alison, de exemplu, deși oamenii își puteau aminti cu exactitate câte întrebări au fost puse în conversațiile lor, ei nu au intuit legătura dintre întrebări și apreciere. În patru studii, în care participanții au fost implicați în conversații ei înșiși sau au citit transcrieri ale conversațiilor altora, oamenii au avut tendința să nu realizeze că întrebarea ar influența – sau ar fi influențat – nivelul de prietenie dintre conversatori.