Skip to content

Commit 98b7b50

Browse files
NattfarinnSteveb-palongosz
authored
IBX-5388: Fixed performance issues of content updates after field changes
For more details see https://issues.ibexa.co/browse/IBX-5388 and #397 Key changes: * Fixed performance issues of content updates after field definition changes * Made DefaultDataFieldStorage extend FieldStorage * [Tests] Aligned test coverage with the changes --------- Co-Authored-By: Paweł Niedzielski <[email protected]> Co-Authored-By: Andrew Longosz <[email protected]>
1 parent b5fe9ad commit 98b7b50

File tree

26 files changed

+1164
-241
lines changed

26 files changed

+1164
-241
lines changed

eZ/Publish/API/Repository/Tests/ContentTypeServiceTest.php

-61
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use eZ\Publish\API\Repository\Values\ContentType\ContentTypeGroup;
1616
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
1717
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinitionCollection as APIFieldDefinitionCollection;
18-
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinitionCreateStruct;
1918
use eZ\Publish\API\Repository\Values\Translation\Message;
2019
use eZ\Publish\Core\FieldType\TextLine\Value as TextLineValue;
2120

@@ -2021,66 +2020,6 @@ public function testRemoveFieldDefinitionRemovesFieldFromContentRemoved($data)
20212020
);
20222021
}
20232022

2024-
/**
2025-
* @covers \eZ\Publish\API\Repository\ContentTypeService::removeFieldDefinition()
2026-
*/
2027-
public function testRemoveFieldDefinitionRemovesOrphanedRelations(): void
2028-
{
2029-
$repository = $this->getRepository();
2030-
2031-
$contentTypeService = $repository->getContentTypeService();
2032-
$contentService = $repository->getContentService();
2033-
2034-
// Create ContentType
2035-
$contentTypeDraft = $this->createContentTypeDraft([$this->getRelationFieldDefinition()]);
2036-
$contentTypeService->publishContentTypeDraft($contentTypeDraft);
2037-
$publishedType = $contentTypeService->loadContentType($contentTypeDraft->id);
2038-
2039-
// Create Content with Relation
2040-
$contentDraft = $this->createContentDraft();
2041-
$publishedVersion = $contentService->publishVersion($contentDraft->versionInfo);
2042-
2043-
$newDraft = $contentService->createContentDraft($publishedVersion->contentInfo);
2044-
$updateStruct = $contentService->newContentUpdateStruct();
2045-
$updateStruct->setField('relation', 14, 'eng-US');
2046-
$contentDraft = $contentService->updateContent($newDraft->versionInfo, $updateStruct);
2047-
$publishedContent = $contentService->publishVersion($contentDraft->versionInfo);
2048-
2049-
// Remove field definition from ContentType
2050-
$contentTypeDraft = $contentTypeService->createContentTypeDraft($publishedType);
2051-
$relationField = $contentTypeDraft->getFieldDefinition('relation');
2052-
$contentTypeService->removeFieldDefinition($contentTypeDraft, $relationField);
2053-
$contentTypeService->publishContentTypeDraft($contentTypeDraft);
2054-
2055-
// Load Content
2056-
$content = $contentService->loadContent($publishedContent->contentInfo->id);
2057-
2058-
$this->assertCount(0, $contentService->loadRelations($content->versionInfo));
2059-
}
2060-
2061-
private function getRelationFieldDefinition(): FieldDefinitionCreateStruct
2062-
{
2063-
$repository = $this->getRepository();
2064-
2065-
$contentTypeService = $repository->getContentTypeService();
2066-
2067-
$relationFieldCreate = $contentTypeService->newFieldDefinitionCreateStruct(
2068-
'relation',
2069-
'ezobjectrelation'
2070-
);
2071-
$relationFieldCreate->names = ['eng-US' => 'Relation'];
2072-
$relationFieldCreate->descriptions = ['eng-US' => 'Relation to any Content'];
2073-
$relationFieldCreate->fieldGroup = 'blog-content';
2074-
$relationFieldCreate->position = 3;
2075-
$relationFieldCreate->isTranslatable = false;
2076-
$relationFieldCreate->isRequired = false;
2077-
$relationFieldCreate->isInfoCollector = false;
2078-
$relationFieldCreate->validatorConfiguration = [];
2079-
$relationFieldCreate->isSearchable = false;
2080-
2081-
return $relationFieldCreate;
2082-
}
2083-
20842023
/**
20852024
* Test for the addFieldDefinition() method.
20862025
*

eZ/Publish/API/Repository/Values/Content/Field.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ class Field extends ValueObject
2424
/**
2525
* The field id.
2626
*
27-
* @todo may be not needed
27+
* Value of `null` indicates the field is virtual
28+
* and is not persisted (yet).
2829
*
29-
* @var int
30+
* @var int|null
3031
*/
3132
protected $id;
3233

@@ -58,7 +59,7 @@ class Field extends ValueObject
5859
*/
5960
protected $fieldTypeIdentifier;
6061

61-
public function getId(): int
62+
public function getId(): ?int
6263
{
6364
return $this->id;
6465
}
@@ -85,4 +86,12 @@ public function getFieldTypeIdentifier(): string
8586
{
8687
return $this->fieldTypeIdentifier;
8788
}
89+
90+
/**
91+
* @phpstan-assert-if-true !null $this->getId()
92+
*/
93+
public function isVirtual(): bool
94+
{
95+
return null === $this->id;
96+
}
8897
}

eZ/Publish/Core/Persistence/Legacy/Content/FieldHandler.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,13 @@ protected function getEmptyField(FieldDefinition $fieldDefinition, $languageCode
148148
*
149149
* @param \eZ\Publish\SPI\Persistence\Content $content
150150
*/
151-
public function createExistingFieldsInNewVersion(Content $content)
151+
public function createExistingFieldsInNewVersion(Content $content): void
152152
{
153153
foreach ($content->fields as $field) {
154+
if ($field->id === null) {
155+
// Virtual field with default value, skip creating field as it has no id
156+
continue;
157+
}
154158
$this->createExistingFieldInNewVersion($field, $content);
155159
}
156160
}

eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php

+164-22
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,45 @@
1616
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
1717
use eZ\Publish\SPI\Persistence\Content\Relation;
1818
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as RelationCreateStruct;
19+
use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler;
1920
use eZ\Publish\SPI\Persistence\Content\VersionInfo;
21+
use Ibexa\Contracts\Core\Event\Mapper\ResolveMissingFieldEvent;
22+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2023

2124
/**
2225
* Mapper for Content Handler.
2326
*
2427
* Performs mapping of Content objects.
28+
*
29+
* @phpstan-type TVersionedLanguageFieldDefinitionsMap array<
30+
* int, array<
31+
* int, array<
32+
* string, array<
33+
* int, \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition,
34+
* >
35+
* >
36+
* >
37+
* >
38+
* @phpstan-type TVersionedFieldMap array<
39+
* int, array<
40+
* int, array<
41+
* int, \eZ\Publish\SPI\Persistence\Content\Field,
42+
* >
43+
* >
44+
* >
45+
* @phpstan-type TVersionedNameMap array<
46+
* int, array<
47+
* int, array<
48+
* string, array<int, string>
49+
* >
50+
* >
51+
* >
52+
* @phpstan-type TContentInfoMap array<int, \eZ\Publish\SPI\Persistence\Content\ContentInfo>
53+
* @phpstan-type TVersionInfoMap array<
54+
* int, array<
55+
* int, \eZ\Publish\SPI\Persistence\Content\VersionInfo,
56+
* >
57+
* >
2558
*/
2659
class Mapper
2760
{
@@ -40,15 +73,25 @@ class Mapper
4073
protected $languageHandler;
4174

4275
/**
43-
* Creates a new mapper.
44-
*
45-
* @param \eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry $converterRegistry
46-
* @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
76+
* @var \eZ\Publish\SPI\Persistence\Content\Type\Handler
4777
*/
48-
public function __construct(Registry $converterRegistry, LanguageHandler $languageHandler)
49-
{
78+
private $contentTypeHandler;
79+
80+
/**
81+
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
82+
*/
83+
private $eventDispatcher;
84+
85+
public function __construct(
86+
Registry $converterRegistry,
87+
LanguageHandler $languageHandler,
88+
ContentTypeHandler $contentTypeHandler,
89+
EventDispatcherInterface $eventDispatcher
90+
) {
5091
$this->converterRegistry = $converterRegistry;
5192
$this->languageHandler = $languageHandler;
93+
$this->contentTypeHandler = $contentTypeHandler;
94+
$this->eventDispatcher = $eventDispatcher;
5295
}
5396

5497
/**
@@ -174,66 +217,166 @@ public function convertToStorageValue(Field $field)
174217
*
175218
* "$tableName_$columnName"
176219
*
177-
* @param array $rows
178-
* @param array $nameRows
220+
* @param array<array<string, scalar>> $rows
221+
* @param array<array<string, scalar>> $nameRows
222+
* @param string $prefix
179223
*
180224
* @return \eZ\Publish\SPI\Persistence\Content[]
225+
*
226+
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
181227
*/
182-
public function extractContentFromRows(array $rows, array $nameRows, $prefix = 'ezcontentobject_')
183-
{
228+
public function extractContentFromRows(
229+
array $rows,
230+
array $nameRows,
231+
string $prefix = 'ezcontentobject_'
232+
): array {
184233
$versionedNameData = [];
234+
185235
foreach ($nameRows as $row) {
186-
$contentId = (int)$row['ezcontentobject_name_contentobject_id'];
187-
$versionNo = (int)$row['ezcontentobject_name_content_version'];
188-
$versionedNameData[$contentId][$versionNo][$row['ezcontentobject_name_content_translation']] = $row['ezcontentobject_name_name'];
236+
$contentId = (int)$row["{$prefix}name_contentobject_id"];
237+
$versionNo = (int)$row["{$prefix}name_content_version"];
238+
$languageCode = $row["{$prefix}name_content_translation"];
239+
$versionedNameData[$contentId][$versionNo][$languageCode] = $row["{$prefix}name_name"];
189240
}
190241

191242
$contentInfos = [];
192243
$versionInfos = [];
193244
$fields = [];
194245

246+
$fieldDefinitions = $this->loadCachedVersionFieldDefinitionsPerLanguage(
247+
$rows,
248+
$prefix
249+
);
250+
195251
foreach ($rows as $row) {
196252
$contentId = (int)$row["{$prefix}id"];
253+
$versionId = (int)$row["{$prefix}version_id"];
254+
197255
if (!isset($contentInfos[$contentId])) {
198256
$contentInfos[$contentId] = $this->extractContentInfoFromRow($row, $prefix);
199257
}
258+
200259
if (!isset($versionInfos[$contentId])) {
201260
$versionInfos[$contentId] = [];
202261
}
203262

204-
$versionId = (int)$row['ezcontentobject_version_id'];
205263
if (!isset($versionInfos[$contentId][$versionId])) {
206264
$versionInfos[$contentId][$versionId] = $this->extractVersionInfoFromRow($row);
207265
}
208266

209-
$fieldId = (int)$row['ezcontentobject_attribute_id'];
210-
if (!isset($fields[$contentId][$versionId][$fieldId])) {
267+
$fieldId = (int)$row["{$prefix}attribute_id"];
268+
$fieldDefinitionId = (int)$row["{$prefix}attribute_contentclassattribute_id"];
269+
$languageCode = $row["{$prefix}attribute_language_code"];
270+
271+
if (!isset($fields[$contentId][$versionId][$fieldId])
272+
&& isset($fieldDefinitions[$contentId][$versionId][$languageCode][$fieldDefinitionId])
273+
) {
211274
$fields[$contentId][$versionId][$fieldId] = $this->extractFieldFromRow($row);
275+
unset($fieldDefinitions[$contentId][$versionId][$languageCode][$fieldDefinitionId]);
212276
}
213277
}
214278

279+
return $this->buildContentObjects(
280+
$contentInfos,
281+
$versionInfos,
282+
$fields,
283+
$fieldDefinitions,
284+
$versionedNameData
285+
);
286+
}
287+
288+
/**
289+
* @phpstan-param TContentInfoMap $contentInfos
290+
* @phpstan-param TVersionInfoMap $versionInfos
291+
* @phpstan-param TVersionedFieldMap $fields
292+
* @phpstan-param TVersionedLanguageFieldDefinitionsMap $missingFieldDefinitions
293+
* @phpstan-param TVersionedNameMap $versionedNames
294+
*
295+
* @return \eZ\Publish\SPI\Persistence\Content[]
296+
*/
297+
private function buildContentObjects(
298+
array $contentInfos,
299+
array $versionInfos,
300+
array $fields,
301+
array $missingFieldDefinitions,
302+
array $versionedNames
303+
): array {
215304
$results = [];
305+
216306
foreach ($contentInfos as $contentId => $contentInfo) {
217307
foreach ($versionInfos[$contentId] as $versionId => $versionInfo) {
218308
// Fallback to just main language name if versioned name data is missing
219-
if (isset($versionedNameData[$contentId][$versionInfo->versionNo])) {
220-
$names = $versionedNameData[$contentId][$versionInfo->versionNo];
221-
} else {
222-
$names = [$contentInfo->mainLanguageCode => $contentInfo->name];
223-
}
309+
$names = $versionedNames[$contentId][$versionInfo->versionNo]
310+
?? [$contentInfo->mainLanguageCode => $contentInfo->name];
224311

225312
$content = new Content();
226313
$content->versionInfo = $versionInfo;
227314
$content->versionInfo->names = $names;
228315
$content->versionInfo->contentInfo = $contentInfo;
229316
$content->fields = array_values($fields[$contentId][$versionId]);
317+
318+
$missingVersionFieldDefinitions = $missingFieldDefinitions[$contentId][$versionId];
319+
foreach ($missingVersionFieldDefinitions as $languageCode => $versionFieldDefinitions) {
320+
foreach ($versionFieldDefinitions as $fieldDefinition) {
321+
$event = $this->eventDispatcher->dispatch(
322+
new ResolveMissingFieldEvent(
323+
$content,
324+
$fieldDefinition,
325+
$languageCode
326+
)
327+
);
328+
329+
$field = $event->getField();
330+
if ($field !== null) {
331+
$content->fields[] = $field;
332+
}
333+
}
334+
}
335+
230336
$results[] = $content;
231337
}
232338
}
233339

234340
return $results;
235341
}
236342

343+
/**
344+
* @phpstan-return TVersionedLanguageFieldDefinitionsMap
345+
*
346+
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
347+
*/
348+
private function loadCachedVersionFieldDefinitionsPerLanguage(
349+
array $rows,
350+
string $prefix
351+
): array {
352+
$fieldDefinitions = [];
353+
$contentTypes = [];
354+
$allLanguages = $this->loadAllLanguagesWithIdKey();
355+
356+
foreach ($rows as $row) {
357+
$contentId = (int)$row["{$prefix}id"];
358+
$versionId = (int)$row["{$prefix}version_id"];
359+
$contentTypeId = (int)$row["{$prefix}contentclass_id"];
360+
$languageMask = (int)$row["{$prefix}version_language_mask"];
361+
362+
if (isset($fieldDefinitions[$contentId][$versionId])) {
363+
continue;
364+
}
365+
366+
$languageCodes = $this->extractLanguageCodesFromMask($languageMask, $allLanguages);
367+
$contentTypes[$contentTypeId] = $contentTypes[$contentTypeId] ?? $this->contentTypeHandler->load($contentTypeId);
368+
$contentType = $contentTypes[$contentTypeId];
369+
foreach ($contentType->fieldDefinitions as $fieldDefinition) {
370+
foreach ($languageCodes as $languageCode) {
371+
$id = $fieldDefinition->id;
372+
$fieldDefinitions[$contentId][$versionId][$languageCode][$id] = $fieldDefinition;
373+
}
374+
}
375+
}
376+
377+
return $fieldDefinitions;
378+
}
379+
237380
/**
238381
* Extracts a ContentInfo object from $row.
239382
*
@@ -251,7 +394,6 @@ public function extractContentInfoFromRow(array $row, $prefix = '', $treePrefix
251394
$contentInfo->contentTypeId = (int)$row["{$prefix}contentclass_id"];
252395
$contentInfo->sectionId = (int)$row["{$prefix}section_id"];
253396
$contentInfo->currentVersionNo = (int)$row["{$prefix}current_version"];
254-
$contentInfo->isPublished = ($row["{$prefix}status"] == ContentInfo::STATUS_PUBLISHED);
255397
$contentInfo->ownerId = (int)$row["{$prefix}owner_id"];
256398
$contentInfo->publicationDate = (int)$row["{$prefix}published"];
257399
$contentInfo->modificationDate = (int)$row["{$prefix}modified"];

0 commit comments

Comments
 (0)