Remove one N+1 by fetching TechnicalDataSets

This commit is contained in:
Benoît Burnichon
2016-03-29 10:33:15 +02:00
parent 6bfe235bfa
commit c580b55c3e
14 changed files with 382 additions and 71 deletions

View File

@@ -80,6 +80,7 @@ use Alchemy\Phrasea\Form\Extension\HelpTypeExtension;
use Alchemy\Phrasea\Media\DatafilesResolver; use Alchemy\Phrasea\Media\DatafilesResolver;
use Alchemy\Phrasea\Media\MediaAccessorResolver; use Alchemy\Phrasea\Media\MediaAccessorResolver;
use Alchemy\Phrasea\Media\PermalinkMediaResolver; use Alchemy\Phrasea\Media\PermalinkMediaResolver;
use Alchemy\Phrasea\Media\TechnicalDataServiceProvider;
use Alchemy\Phrasea\Model\Entities\User; use Alchemy\Phrasea\Model\Entities\User;
use Doctrine\DBAL\Event\ConnectionEventArgs; use Doctrine\DBAL\Event\ConnectionEventArgs;
use MediaVorus\Media\MediaInterface; use MediaVorus\Media\MediaInterface;
@@ -190,6 +191,7 @@ class Application extends SilexApplication
$this->register(new NotificationDelivererServiceProvider()); $this->register(new NotificationDelivererServiceProvider());
$this->register(new RepositoriesServiceProvider()); $this->register(new RepositoriesServiceProvider());
$this->register(new ManipulatorServiceProvider()); $this->register(new ManipulatorServiceProvider());
$this->register(new TechnicalDataServiceProvider());
$this->register(new InstallerServiceProvider()); $this->register(new InstallerServiceProvider());
$this->register(new PhraseaVersionServiceProvider()); $this->register(new PhraseaVersionServiceProvider());

View File

@@ -1068,7 +1068,11 @@ class V1Controller extends Controller
/** @var SearchEngineResult $search_result */ /** @var SearchEngineResult $search_result */
$references = new RecordReferenceCollection($search_result->getResults()); $references = new RecordReferenceCollection($search_result->getResults());
foreach ($references->toRecords($this->getApplicationBox()) as $record) { $technicalData = $this->app['service.technical_data']->fetchRecordsTechnicalData($references);
foreach ($references->toRecords($this->getApplicationBox()) as $index => $record) {
$record->setTechnicalDataSet($technicalData[$index]);
if ($record->isStory()) { if ($record->isStory()) {
$ret['results']['stories'][] = $this->listStory($request, $record); $ret['results']['stories'][] = $this->listStory($request, $record);
} else { } else {

View File

@@ -11,22 +11,20 @@ namespace Alchemy\Phrasea\Media;
use Assert\Assertion; use Assert\Assertion;
final class ArrayTechnicalDataSet implements \IteratorAggregate, TechnicalDataSet class ArrayTechnicalDataSet implements \IteratorAggregate, TechnicalDataSet
{ {
/** @var TechnicalData[] */ /** @var TechnicalData[] */
private $data; private $data = [];
/** /**
* @param TechnicalData[] $data * @param TechnicalData[] $data
*/ */
public function __construct($data = []) public function __construct($data = [])
{ {
Assertion::allIsInstanceOf($data, TechnicalData::class); Assertion::isTraversable($data);
$this->data = [];
foreach ($data as $technicalData) { foreach ($data as $technicalData) {
$this->data[$technicalData->getName()] = $technicalData; $this[] = $technicalData;
} }
} }
@@ -41,7 +39,7 @@ final class ArrayTechnicalDataSet implements \IteratorAggregate, TechnicalDataSe
$offset = $offset->getName(); $offset = $offset->getName();
} }
return isset($this->data[$offset]) || array_key_exists($offset, $this->data); return isset($this->data[$offset]);
} }
public function offsetGet($offset) public function offsetGet($offset)
@@ -50,7 +48,7 @@ final class ArrayTechnicalDataSet implements \IteratorAggregate, TechnicalDataSe
} }
/** /**
* @param string $offset * @param null|string $offset
* @param TechnicalData $value * @param TechnicalData $value
*/ */
public function offsetSet($offset, $value) public function offsetSet($offset, $value)
@@ -58,6 +56,7 @@ final class ArrayTechnicalDataSet implements \IteratorAggregate, TechnicalDataSe
Assertion::isInstanceOf($value, TechnicalData::class); Assertion::isInstanceOf($value, TechnicalData::class);
$name = $value->getName(); $name = $value->getName();
if (null !== $offset) { if (null !== $offset) {
Assertion::eq($name, $offset); Assertion::eq($name, $offset);
} }
@@ -82,6 +81,7 @@ final class ArrayTechnicalDataSet implements \IteratorAggregate, TechnicalDataSe
public function getValues() public function getValues()
{ {
$values = []; $values = [];
foreach ($this->data as $key => $value) { foreach ($this->data as $key => $value) {
$values[$key] = $value->getValue(); $values[$key] = $value->getValue();
} }

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media\Factory;
use Alchemy\Phrasea\Databox\DataboxConnectionProvider;
use Alchemy\Phrasea\Media\RecordTechnicalDataSetRepositoryFactory;
use Alchemy\Phrasea\Media\Repository\DbalRecordTechnicalDataSetRepository;
class DbalRepositoryFactory implements RecordTechnicalDataSetRepositoryFactory
{
/**
* @var DataboxConnectionProvider
*/
private $connectionProvider;
public function __construct(DataboxConnectionProvider $connectionProvider)
{
$this->connectionProvider = $connectionProvider;
}
public function createRepositoryForDatabox($databoxId)
{
return new DbalRecordTechnicalDataSetRepository(
$this->connectionProvider->getConnection($databoxId),
new TechnicalDataFactory()
);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media\Factory;
use Alchemy\Phrasea\Media\FloatTechnicalData;
use Alchemy\Phrasea\Media\IntegerTechnicalData;
use Alchemy\Phrasea\Media\StringTechnicalData;
use Alchemy\Phrasea\Media\TechnicalData;
class TechnicalDataFactory
{
/**
* @param string $name
* @param string $value
* @return TechnicalData
*/
public function createFromNameAndValue($name, $value)
{
if (ctype_digit($value)) {
return new IntegerTechnicalData($name, $value);
} elseif (preg_match('/[0-9]?\.[0-9]+/', $value)) {
return new FloatTechnicalData($name, $value);
}
return new StringTechnicalData($name, $value);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media;
class RecordTechnicalDataSet extends ArrayTechnicalDataSet
{
/**
* @var int
*/
private $recordId;
/**
* @param int $recordId
* @param TechnicalData[] $technicalData
*/
public function __construct($recordId, $technicalData = [])
{
$this->recordId = (int)$recordId;
parent::__construct($technicalData);
}
/**
* @return int
*/
public function getRecordId()
{
return $this->recordId;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media;
interface RecordTechnicalDataSetRepository
{
/**
* @param int[] $recordIds
* @return RecordTechnicalDataSet[]
*/
public function findByRecordIds(array $recordIds);
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media;
interface RecordTechnicalDataSetRepositoryFactory
{
/**
* @param int $databoxId
* @return RecordTechnicalDataSetRepository
*/
public function createRepositoryForDatabox($databoxId);
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media;
class RecordTechnicalDataSetRepositoryProvider
{
/**
* @var RecordTechnicalDataSetRepository[]
*/
private $repositories = [];
/**
* @var RecordTechnicalDataSetRepositoryFactory
*/
private $factory;
public function __construct(RecordTechnicalDataSetRepositoryFactory $factory)
{
$this->factory = $factory;
}
/**
* @param int $databoxId
* @return RecordTechnicalDataSetRepository
*/
public function getRepositoryFor($databoxId)
{
if (!isset($this->repositories[$databoxId])) {
$this->repositories[$databoxId] = $this->factory->createRepositoryForDatabox($databoxId);
}
return $this->repositories[$databoxId];
}
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media\Repository;
use Alchemy\Phrasea\Media\Factory\TechnicalDataFactory;
use Alchemy\Phrasea\Media\RecordTechnicalDataSet;
use Alchemy\Phrasea\Media\RecordTechnicalDataSetRepository;
use Doctrine\DBAL\Connection;
class DbalRecordTechnicalDataSetRepository implements RecordTechnicalDataSetRepository
{
/**
* @var Connection
*/
private $connection;
/**
* @var TechnicalDataFactory
*/
private $dataFactory;
public function __construct(Connection $connection, TechnicalDataFactory $dataFactory)
{
$this->connection = $connection;
$this->dataFactory = $dataFactory;
}
/**
* @param int[] $recordIds
* @return RecordTechnicalDataSet[]
*/
public function findByRecordIds(array $recordIds)
{
if (empty($recordIds)) {
return [];
}
$data = $this->connection->fetchAll(
'SELECT record_id, name, value FROM technical_datas WHERE record_id IN (:recordIds)',
['recordIds' => $recordIds],
['recordIds' => Connection::PARAM_INT_ARRAY]
);
return $this->mapSetsFromDatabaseResult($recordIds, $data);
}
/**
* @param array $recordIds
* @param array $data
* @return RecordTechnicalDataSet[]
*/
private function mapSetsFromDatabaseResult(array $recordIds, array $data)
{
$groups = [];
foreach ($recordIds as $recordId) {
$groups[$recordId] = new RecordTechnicalDataSet($recordId);
}
foreach ($data as $item) {
$group =& $groups[$item['record_id']];
$group[] = $this->dataFactory->createFromNameAndValue($item['name'], $item['value']);
}
return array_values($groups);
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media;
use Alchemy\Phrasea\Record\RecordReference;
use Alchemy\Phrasea\Record\RecordReferenceCollection;
class TechnicalDataService
{
/**
* @var RecordTechnicalDataSetRepositoryProvider
*/
private $provider;
public function __construct(RecordTechnicalDataSetRepositoryProvider $provider)
{
$this->provider = $provider;
}
/**
* @param RecordReference[] $references
* @return RecordTechnicalDataSet[]
*/
public function fetchRecordsTechnicalData($references)
{
if (!$references instanceof RecordReferenceCollection) {
$references = new RecordReferenceCollection($references);
}
$sets = [];
foreach ($references->groupPerDataboxId() as $databoxId => $indexes) {
foreach ($this->provider->getRepositoryFor($databoxId)->findByRecordIds(array_keys($indexes)) as $set) {
$index = $indexes[$set->getRecordId()];
$sets[$index] = $set;
}
}
ksort($sets);
return $sets;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Media;
use Alchemy\Phrasea\Databox\DataboxConnectionProvider;
use Alchemy\Phrasea\Media\Factory\DbalRepositoryFactory;
use Silex\Application;
use Silex\ServiceProviderInterface;
class TechnicalDataServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['service.technical_data'] = $app->share(function (Application $app) {
$connectionProvider = new DataboxConnectionProvider($app['phraseanet.appbox']);
$repositoryFactory = new DbalRepositoryFactory($connectionProvider);
return new TechnicalDataService(new RecordTechnicalDataSetRepositoryProvider($repositoryFactory));
});
}
public function boot(Application $app)
{
// no-op
}
}

View File

@@ -1,5 +1,5 @@
<?php <?php
/** /*
* This file is part of Phraseanet * This file is part of Phraseanet
* *
* (c) 2005-2016 Alchemy * (c) 2005-2016 Alchemy
@@ -28,7 +28,7 @@ class RecordReferenceCollection implements \IteratorAggregate
foreach ($records as $index => $record) { foreach ($records as $index => $record) {
if (isset($record['id'])) { if (isset($record['id'])) {
$references[$index] = RecordReference::createFromRecordReference($record['id']); $references[$index] = RecordReference::createFromRecordReference($record['id']);
} elseif (isset($record['databox_id']) && isset($record['record_id'])) { } elseif (isset($record['databox_id'], $record['record_id'])) {
$references[$index] = RecordReference::createFromDataboxIdAndRecordId($record['databox_id'], $record['record_id']); $references[$index] = RecordReference::createFromDataboxIdAndRecordId($record['databox_id'], $record['record_id']);
} }
} }
@@ -117,6 +117,6 @@ class RecordReferenceCollection implements \IteratorAggregate
ksort($records); ksort($records);
return array_values($records); return $records;
} }
} }

View File

@@ -92,7 +92,7 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
private $created; private $created;
/** @var string */ /** @var string */
private $original_name; private $original_name;
/** @var TechnicalDataSet */ /** @var TechnicalDataSet|null */
private $technical_data; private $technical_data;
/** @var string */ /** @var string */
private $uuid; private $uuid;
@@ -727,13 +727,10 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
*/ */
public function get_technical_infos($data = '') public function get_technical_infos($data = '')
{ {
if (!$this->technical_data && !$this->mapTechnicalDataFromCache()) { if (null === $this->technical_data) {
$this->technical_data = []; $sets = $this->app['service.technical_data']->fetchRecordsTechnicalData([$this]);
$rs = $this->fetchTechnicalDataFromDb();
$this->mapTechnicalDataFromDb($rs); $this->setTechnicalDataSet(reset($sets));
$this->set_data_to_cache($this->technical_data, self::CACHE_TECHNICAL_DATA);
} }
if ($data) { if ($data) {
@@ -747,6 +744,15 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
return $this->technical_data; return $this->technical_data;
} }
/**
* @param TechnicalDataSet $dataSet
* @internal
*/
public function setTechnicalDataSet(TechnicalDataSet $dataSet)
{
$this->technical_data = $dataSet;
}
/** /**
* @return caption_record * @return caption_record
*/ */
@@ -1862,58 +1868,6 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
$this->mime = $row['mime']; $this->mime = $row['mime'];
} }
/**
* @return bool
*/
private function mapTechnicalDataFromCache()
{
try {
$technical_data = $this->get_data_from_cache(self::CACHE_TECHNICAL_DATA);
} catch (Exception $e) {
$technical_data = false;
}
if (false === $technical_data) {
return false;
}
$this->technical_data = $technical_data;
return true;
}
/**
* @return false|array
*/
private function fetchTechnicalDataFromDb()
{
$sql = 'SELECT name, value FROM technical_datas WHERE record_id = :record_id';
return $this->getDataboxConnection()
->fetchAll($sql, ['record_id' => $this->getRecordId()]);
}
/**
* @param array $rows
*/
private function mapTechnicalDataFromDb(array $rows)
{
$this->technical_data = new ArrayTechnicalDataSet();
foreach ($rows as $row) {
switch (true) {
case ctype_digit($row['value']):
$this->technical_data[] = new IntegerTechnicalData($row['name'], $row['value']);
break;
case preg_match('/[0-9]?\.[0-9]+/', $row['value']):
$this->technical_data[] = new FloatTechnicalData($row['name'], $row['value']);
break;
default:
$this->technical_data[] = new StringTechnicalData($row['name'], $row['value']);
}
}
}
/** /**
* @return Connection * @return Connection
*/ */