perf: batch Wikidata SPARQL queries per film instead of per actor

Use a VALUES clause to fetch awards for all actors of a film in a
single SPARQL request, reducing Wikidata API calls from ~20 per film
to 1 and avoiding idle timeout errors from rate limiting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere
2026-04-01 20:37:13 +02:00
parent 369893a77e
commit 116d7b409e
4 changed files with 119 additions and 46 deletions

View File

@@ -35,19 +35,21 @@ class AwardImporterTest extends TestCase
public function testSkipsActorWithAwardsAlreadyImported(): void
{
$actor = $this->createActorWithFlag(awardsImported: true);
$actor = $this->createActorWithFlag('Already Imported', awardsImported: true);
$this->wikidataGateway->expects($this->never())->method('getAwards');
$this->wikidataGateway->expects($this->never())->method('getAwardsForActors');
$this->importer->importForActor($actor);
$this->importer->importForActors([$actor]);
}
public function testImportsAwardsAndSetsFlag(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$actor = $this->createActorWithFlag('Test Actor', awardsImported: false);
$this->wikidataGateway->method('getAwards')->willReturn([
['name' => 'Academy Award for Best Actor', 'year' => 2020],
$this->wikidataGateway->method('getAwardsForActors')->willReturn([
'Test Actor' => [
['name' => 'Academy Award for Best Actor', 'year' => 2020],
],
]);
$existingType = new AwardType();
@@ -60,7 +62,7 @@ class AwardImporterTest extends TestCase
$persisted[] = $entity;
});
$this->importer->importForActor($actor);
$this->importer->importForActors([$actor]);
$this->assertTrue($actor->isAwardsImported());
$this->assertCount(1, $persisted);
@@ -73,10 +75,12 @@ class AwardImporterTest extends TestCase
public function testCreatesNewAwardTypeWhenNoPatternMatches(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$actor = $this->createActorWithFlag('Test Actor', awardsImported: false);
$this->wikidataGateway->method('getAwards')->willReturn([
['name' => 'Screen Actors Guild Award for Outstanding Performance', 'year' => 2019],
$this->wikidataGateway->method('getAwardsForActors')->willReturn([
'Test Actor' => [
['name' => 'Screen Actors Guild Award for Outstanding Performance', 'year' => 2019],
],
]);
$this->awardTypeRepository->method('findAll')->willReturn([]);
@@ -86,7 +90,7 @@ class AwardImporterTest extends TestCase
$persisted[] = $entity;
});
$this->importer->importForActor($actor);
$this->importer->importForActors([$actor]);
$this->assertTrue($actor->isAwardsImported());
// Should persist both a new AwardType and the Award
@@ -104,34 +108,65 @@ class AwardImporterTest extends TestCase
public function testDoesNotSetFlagOnWikidataError(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$actor = $this->createActorWithFlag('Test Actor', awardsImported: false);
$this->wikidataGateway->method('getAwards')
$this->wikidataGateway->method('getAwardsForActors')
->willThrowException(new \RuntimeException('Wikidata timeout'));
$this->importer->importForActor($actor);
$this->importer->importForActors([$actor]);
$this->assertFalse($actor->isAwardsImported());
}
public function testHandlesActorWithNoAwards(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$actor = $this->createActorWithFlag('Test Actor', awardsImported: false);
$this->wikidataGateway->method('getAwards')->willReturn([]);
$this->wikidataGateway->method('getAwardsForActors')->willReturn([]);
$this->awardTypeRepository->method('findAll')->willReturn([]);
$this->em->expects($this->never())->method('persist');
$this->importer->importForActor($actor);
$this->importer->importForActors([$actor]);
$this->assertTrue($actor->isAwardsImported());
}
private function createActorWithFlag(bool $awardsImported): Actor
public function testBatchImportsMultipleActors(): void
{
$actor1 = $this->createActorWithFlag('Actor One', awardsImported: false);
$actor2 = $this->createActorWithFlag('Actor Two', awardsImported: false);
$alreadyImported = $this->createActorWithFlag('Actor Three', awardsImported: true);
$this->wikidataGateway->expects($this->once())->method('getAwardsForActors')
->with($this->callback(fn (array $actors) => 2 === \count($actors)))
->willReturn([
'Actor One' => [['name' => 'Academy Award for Best Actor', 'year' => 2020]],
'Actor Two' => [['name' => 'Golden Globe for Best Actor', 'year' => 2021]],
]);
$existingType = new AwardType();
$existingType->setName('Oscar')->setPattern('Academy Award');
$this->awardTypeRepository->method('findAll')->willReturn([$existingType]);
$persisted = [];
$this->em->method('persist')->willReturnCallback(function ($entity) use (&$persisted) {
$persisted[] = $entity;
});
$this->importer->importForActors([$actor1, $actor2, $alreadyImported]);
$this->assertTrue($actor1->isAwardsImported());
$this->assertTrue($actor2->isAwardsImported());
// 2 Awards + 1 new AwardType (Golden Globe)
$this->assertCount(3, $persisted);
}
private function createActorWithFlag(string $name, bool $awardsImported): Actor
{
$actor = new Actor();
$actor->setName('Test Actor');
$actor->setName($name);
$actor->setAwardsImported($awardsImported);
return $actor;