diff --git a/migrations/Version20260116104840.php b/migrations/Version20260116104840.php new file mode 100644 index 0000000..2be66c5 --- /dev/null +++ b/migrations/Version20260116104840.php @@ -0,0 +1,34 @@ +addSql('DROP INDEX unique_quartal'); + $this->addSql('CREATE UNIQUE INDEX unique_quartal ON contacts (phone_number, due_quartal) WHERE due_quartal IS NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP INDEX unique_quartal'); + $this->addSql('CREATE INDEX unique_quartal ON contacts (phone_number, due_quartal) WHERE (due_quartal IS NOT NULL)'); + } +} diff --git a/src/Command/CleanMobileCommand.php b/src/Command/CleanMobileCommand.php index e907ebd..bd992d8 100644 --- a/src/Command/CleanMobileCommand.php +++ b/src/Command/CleanMobileCommand.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Command; use App\Entity\Contacts; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Uid\Uuid; use Doctrine\ORM\EntityManagerInterface; use League\Csv\Reader; @@ -33,8 +35,9 @@ final class CleanMobileCommand extends Command private string $backendApiURL = 'https://umfragetool.ukbonn.de/api/api/participant/create-qm-befr-participant'; public function __construct( - private readonly EntityManagerInterface $em, - private readonly HttpClientInterface $http + private EntityManagerInterface $em, + private readonly HttpClientInterface $http, + private readonly ManagerRegistry $doctrine, ) { parent::__construct(); } @@ -219,7 +222,8 @@ final class CleanMobileCommand extends Command // Create a Contact entity for DB insertion $contact = new Contacts(); $contact->setPhoneNumber($row['HANDY_E164']); - $dueDate = (new \DateTime('today'))->setTime(12, 0, 0); + $today = new \DateTime('today'); + $dueDate = ($today)->setTime(12, 0, 0); $contact->setDueDate($dueDate); $contact->setContacted(false); $contact->setParsedFilename($inputPath); @@ -228,6 +232,8 @@ final class CleanMobileCommand extends Command $contact->setParsedFileLinenum($rowCount + 1); $contact->setParsedFileLine($sanitiseUtf8(implode(';', $row))); $contact->setMsgContentType($rowCount % 2 ? 1 : 2); + $quartal = $today->format('Y') . ceil($today->format('n') / 3); + $contact->setDueQuartal((int) $quartal); try { $result = $this->http->request('POST', $this->backendApiURL . "/" . $study_id . "/" . $study_id_chain, [ @@ -254,16 +260,29 @@ final class CleanMobileCommand extends Command // 6️⃣ Persist the valid contacts (batch insert) // ------------------------------------------------------------- if (\count($validContacts) > 0) { - $batchSize = 100; + $batch = []; + foreach ($validContacts as $i => $contact) { - $this->em->persist($contact); - if ((($i + 1) % $batchSize) === 0) { - $this->em->flush(); - $this->em->clear(); // free memory + $batch[] = $contact; + if (count($batch) === 100) { + $this->flushBatch($batch, $io); + $batch = []; } } - $this->em->flush(); - $this->em->clear(); + if ($batch) { + $this->flushBatch($batch, $io); + } + + +// foreach ($validContacts as $i => $contact) { +// $this->em->persist($contact); +// try { +// $this->em->flush(); +// } catch(UniqueConstraintViolationException $e) { +// $io->warning(['The number ', $contact->getPhoneNumber(), ' already contacted for quartal', $contact->getDueQuartal()]); +// } +// $this->em->clear(); +// } } // ------------------------------------------------------------- @@ -280,4 +299,64 @@ final class CleanMobileCommand extends Command return Command::SUCCESS; } + + private function flushBatch(array $batch, $io): void + { + $this->em = $this->resetEntityManager(); + foreach ($batch as $c) { + $this->em->persist($c); + } + + try { + $this->em->flush(); // versucht, den kompletten Batch zu speichern + } catch (UniqueConstraintViolationException $e) { + $io->warning('Batch conflict – falling back to element‑wise'); + + // Transaction rollback (falls aktiv) + $conn = $this->em->getConnection(); + if ($conn->isTransactionActive()) { + $conn->rollBack(); + } + + // Der EM ist jetzt *geschlossen* → resetten + if (!$this->em->isOpen()) { + $this->em = $this->resetEntityManager(); // hol dir einen frischen EM + } + + // Einzelweise weiter versuchen, damit die „guten“ Zeilen nicht verloren gehen + $this->flushElementsIndividually($batch, $io); + return; + } + + // Batch erfolgreich – Speicher freigeben + $this->em->clear(); + } + + private function flushElementsIndividually(array $contacts, $io): void + { + $io->warning('flushing individually'); + foreach ($contacts as $contact) { + $this->em->persist($contact); + $io->warning('flushing individually2'); + try { + $this->em->flush(); + $io->warning('flushing individually3'); + } catch (UniqueConstraintViolationException $e) { + $io->warning(['The number ', $contact->getPhoneNumber(), ' already contacted for quartal', $contact->getDueQuartal()]); + // das duplizierte Objekt aus dem Unit‑of‑Work entfernen + $this->em->detach($contact); + $io->warning('flushing individually4'); + if (!$this->em->isOpen()) { + $this->em = $this->resetEntityManager(); // hol dir einen frischen EM + } + } + // jedes Entity einzeln freigeben, sonst wächst der Speicher + $this->em->clear(); + } + } + + private function resetEntityManager(): EntityManagerInterface + { + return $this->doctrine->resetManager(); + } } diff --git a/src/Entity/Contacts.php b/src/Entity/Contacts.php index c809fb6..95af837 100644 --- a/src/Entity/Contacts.php +++ b/src/Entity/Contacts.php @@ -6,6 +6,11 @@ use App\Repository\ContactsRepository; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ContactsRepository::class)] +#[ORM\UniqueConstraint( + name: 'unique_quartal', + columns: ['phone_number', 'due_quartal'], + options: ['where' => "due_quartal IS NOT NULL"] +)] class Contacts { #[ORM\Id] @@ -19,6 +24,9 @@ class Contacts #[ORM\Column(nullable: true)] private ?\DateTime $due_date = null; + #[ORM\Column(nullable: true)] + private ?int $due_quartal = null; + #[ORM\Column] private ?bool $contacted = null; @@ -184,4 +192,14 @@ class Contacts { $this->study_id_short = $study_id_short; } + + public function getDueQuartal(): ?int + { + return $this->due_quartal; + } + + public function setDueQuartal(?int $due_quartal): void + { + $this->due_quartal = $due_quartal; + } }