Extracted database queries for part matching into its own service and optimized the query reducing the required queries by factor 2

This commit is contained in:
Jan Böhmer 2024-12-31 17:33:00 +01:00
parent e0141025db
commit 77ecd99646
2 changed files with 67 additions and 47 deletions

View file

@ -26,6 +26,7 @@ namespace App\Controller;
use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Form\InfoProviderSystem\PartSearchType; use App\Form\InfoProviderSystem\PartSearchType;
use App\Services\InfoProviderSystem\ExistingPartFinder;
use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\ProviderRegistry;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -45,7 +46,8 @@ class InfoProviderController extends AbstractController
public function __construct(private readonly ProviderRegistry $providerRegistry, public function __construct(private readonly ProviderRegistry $providerRegistry,
private readonly PartInfoRetriever $infoRetriever, private readonly PartInfoRetriever $infoRetriever,
private readonly EntityManagerInterface $em) private readonly ExistingPartFinder $existingPartFinder
)
{ {
} }
@ -61,49 +63,6 @@ class InfoProviderController extends AbstractController
]); ]);
} }
/**
* Looks for parts in the local database that match the results from the info provider, so the user doesn't
* accidentally add a duplicate
*
* @param array $partsList Array of Arrays. Outer Array contains one entry per search result from the info Provider
* Inner array contains one entry "dto" for the dto from the info provider
* and one entry "localPart" where the local Part will be put if it exists
* Form: [["dto" => dto from provider, "localPart" => null]["dto" => dto from provider, "localPart" => null]]
* This function might modify the original array, not sure
* @return array Same format as the input array, but for parts that exist locally null will be replaced by a Part
*/
private function matchResultsToKnownParts(array $partsList): array
{
//we need a manufacturer object to look for a manufacturer
$manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder('manufacturer');
$manufacturerQb->where($manufacturerQb->expr()->like('LOWER(manufacturer.name)', 'LOWER(:manufacturer_name)'));
//check if both manufacturer and Manufacturer part namber matches. If so, it must be the same part
//use LOWER to make the search independent of case
$mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
$mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)'));
$mpnQb->andWhere($mpnQb->expr()->eq('part.manufacturer', ':manufacturer'));
foreach ($partsList as $index => $part) {
$manufacturerQb->setParameter('manufacturer_name', $part['dto']->manufacturer);
$manufacturers = $manufacturerQb->getQuery()->getResult();
if(!$manufacturers) {
continue;
}
$mpnQb->setParameter('manufacturer', $manufacturers);
$mpnQb->setParameter('mpn', $part['dto']->mpn);
$localParts = $mpnQb->getQuery()->getResult();
if(!$localParts) {
continue;
}
//We only use the first matching part. If a user already has duplicate parts they will get a random one
$partsList[$index]['localPart'] = $localParts[0];
}
return $partsList;
}
#[Route('/search', name: 'info_providers_search')] #[Route('/search', name: 'info_providers_search')]
#[Route('/update/{target}', name: 'info_providers_update_part_search')] #[Route('/update/{target}', name: 'info_providers_update_part_search')]
public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger): Response public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger): Response
@ -125,20 +84,25 @@ class InfoProviderController extends AbstractController
$keyword = $form->get('keyword')->getData(); $keyword = $form->get('keyword')->getData();
$providers = $form->get('providers')->getData(); $providers = $form->get('providers')->getData();
$dtos = [];
try { try {
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers); $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
} catch (ClientException $e) { } catch (ClientException $e) {
$this->addFlash('error', t('info_providers.search.error.client_exception')); $this->addFlash('error', t('info_providers.search.error.client_exception'));
$this->addFlash('error',$e->getMessage()); $this->addFlash('error',$e->getMessage());
//Log the exception //Log the exception
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]); $exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
} }
// modify the array to an array of arrays that has a field for a matching local Part // modify the array to an array of arrays that has a field for a matching local Part
// the advantage to use that format even when we don't look for local parts is that we // the advantage to use that format even when we don't look for local parts is that we
// always work with the same interface // always work with the same interface
$results = array_map(function ($result) {return ['dto' => $result, 'localPart' => null];}, $results); $results = array_map(function ($result) {return ['dto' => $result, 'localPart' => null];}, $dtos);
if(!$update_target) { if(!$update_target) {
$results = $this->matchResultsToKnownParts($results); foreach ($results as $index => $result) {
$results[$index]['localPart'] = $this->existingPartFinder->findFirstExisting($result['dto']);
}
} }
} }

View file

@ -0,0 +1,56 @@
<?php
namespace App\Services\InfoProviderSystem;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Doctrine\ORM\EntityManagerInterface;
/**
* This service assists in finding existing local parts for a SearchResultDTO, so that the user
* does not accidentally add a duplicate.
*/
final class ExistingPartFinder
{
public function __construct(private readonly EntityManagerInterface $em)
{
}
/**
* Return the first existing local part, that matches the search result.
* If no part is found, return null.
* @param SearchResultDTO $dto
* @return Part|null
*/
public function findFirstExisting(SearchResultDTO $dto): ?Part
{
$results = $this->findAllExisting($dto);
return count($results) > 0 ? $results[0] : null;
}
/**
* Returns all existing local parts that match the search result.
* If no part is found, return an empty array.
* @param SearchResultDTO $dto
* @return Part[]
*/
public function findAllExisting(SearchResultDTO $dto): array
{
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
$qb->select('part')
->leftJoin('part.manufacturer', 'manufacturer')
//The manufacturer name must match
->where("ILIKE(manufacturer.name, :manufacturerName) = TRUE")
//And the manufacturer product number must match
->andWhere(
"ILIKE(part.manufacturer_product_number, :mpn) = TRUE"
);
$qb->setParameter('manufacturerName', $dto->manufacturer);
$qb->setParameter('mpn', $dto->mpn);
return $qb->getQuery()->getResult();
}
}