mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-20 17:19:34 +00:00
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:
parent
e0141025db
commit
77ecd99646
2 changed files with 67 additions and 47 deletions
|
|
@ -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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
56
src/Services/InfoProviderSystem/ExistingPartFinder.php
Normal file
56
src/Services/InfoProviderSystem/ExistingPartFinder.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue