diff --git a/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php
new file mode 100644
index 0000000000000..464d145c43024
--- /dev/null
+++ b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php
@@ -0,0 +1,167 @@
+resourceConnection = $resourceConnection;
+ $this->entityTypeCodes = $entityTypeCodes;
+ $this->changelog = $changelog;
+ $this->fromVersionId = $fromVersionId;
+ $this->toVersionId = $toVersionId;
+ $this->batchSize = $batchSize;
+ }
+
+ /**
+ * @return \Generator|\Traversable
+ * @throws \Exception
+ */
+ public function getIterator(): \Generator
+ {
+ while ($this->fromVersionId < $this->toVersionId) {
+ $ids = $this->walk();
+
+ if (empty($ids)) {
+ break;
+ }
+ $this->fromVersionId += $this->batchSize;
+ yield $ids;
+ }
+ }
+
+ /**
+ * Calculate EAV attributes size
+ *
+ * @param ChangelogInterface $changelog
+ * @return int
+ * @throws LocalizedException
+ */
+ private function calculateEavAttributeSize(): int
+ {
+ $connection = $this->resourceConnection->getConnection();
+
+ if (!isset($this->entityTypeCodes[$this->changelog->getViewId()])) {
+ throw new LocalizedException(__('Entity type for view was not defined'));
+ }
+
+ $select = $connection->select();
+ $select->from(
+ $this->resourceConnection->getTableName('eav_attribute'),
+ new Expression('COUNT(*)')
+ )->joinInner(
+ ['type' => $connection->getTableName('eav_entity_type')],
+ 'type.entity_type_id=eav_attribute.entity_type_id'
+ )->where('type.entity_type_code = ?', $this->entityTypeCodes[$this->changelog->getViewId()]);
+
+ return (int) $connection->fetchOne($select);
+ }
+
+ /**
+ * Prepare group max concat
+ *
+ * @param int $numberOfAttributes
+ * @return void
+ * @throws \Exception
+ */
+ private function setGroupConcatMax(int $numberOfAttributes): void
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $connection->query(sprintf(
+ 'SET SESSION %s=%s',
+ self::GROUP_CONCAT_MAX_VARIABLE,
+ $numberOfAttributes * (self::DEFAULT_ID_SIZE + 1)
+ ));
+ }
+
+ /**
+ * @inheritdoc
+ * @throws \Exception
+ */
+ private function walk()
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $numberOfAttributes = $this->calculateEavAttributeSize();
+ $this->setGroupConcatMax($numberOfAttributes);
+ $select = $connection->select()->distinct(true)
+ ->where(
+ 'version_id > ?',
+ (int) $this->fromVersionId
+ )
+ ->where(
+ 'version_id <= ?',
+ $this->toVersionId
+ )
+ ->group([$this->changelog->getColumnName(), 'store_id'])
+ ->limit($this->batchSize);
+
+ $columns = [
+ $this->changelog->getColumnName(),
+ 'attribute_ids' => new Expression('GROUP_CONCAT(attribute_id)'),
+ 'store_id'
+ ];
+ $select->from($this->changelog->getName(), $columns);
+ return $connection->fetchAll($select);
+ }
+}
diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml
new file mode 100644
index 0000000000000..64d857a0132a0
--- /dev/null
+++ b/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml b/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml
new file mode 100644
index 0000000000000..c7ccd8512fbf6
--- /dev/null
+++ b/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/registration.php b/dev/tests/integration/_files/Magento/TestModuleMview/registration.php
new file mode 100644
index 0000000000000..5c5453c1bd413
--- /dev/null
+++ b/dev/tests/integration/_files/Magento/TestModuleMview/registration.php
@@ -0,0 +1,12 @@
+getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleMview') === null) {
+ ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleMview', __DIR__);
+}
diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/EnhancedMviewTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/EnhancedMviewTest.php
new file mode 100644
index 0000000000000..6dfc2ba123464
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Eav/Model/EnhancedMviewTest.php
@@ -0,0 +1,102 @@
+objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+ $this->resourceConnection = $this->objectManager->get(ResourceConnection::class);
+ parent::setUp();
+ }
+
+ /**
+ * @dataProvider attributesDataProvider
+ * @magentoDataFixture Magento/Eav/_files/enable_mview_for_test_view.php
+ * @magentoDataFixture Magento/Catalog/_files/product_simple.php
+ * @param array $expectedAttributes
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function testCreateProduct(array $expectedAttributes)
+ {
+ $changelogData = $this->getChangelogData();
+ $attributesMapping = $this->getAttributeCodes();
+ $attributes = [];
+ foreach ($changelogData as $row) {
+ $this->assertArrayHasKey('store_id', $row);
+ $this->assertArrayHasKey('attribute_id', $row);
+
+ if ($row['store_id'] == 0 && $row['attribute_id'] !== null) {
+ $attributes[$attributesMapping[$row['attribute_id']]] = $attributesMapping[$row['attribute_id']];
+ }
+ }
+ sort($expectedAttributes);
+ sort($attributes);
+ $this->assertEquals($attributes, $expectedAttributes);
+
+ $this->assertTrue(true);
+ }
+
+ /**
+ * @return array
+ */
+ private function getAttributeCodes(): array
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $select = $connection->select()
+ ->from($connection->getTableName('eav_attribute'), ['attribute_id', 'attribute_code']);
+ return $connection->fetchPairs($select);
+ }
+
+ /**
+ * @return array|\string[][]
+ */
+ public function attributesDataProvider(): array
+ {
+ return [
+ [
+ 'default' => [
+ 'name',
+ 'meta_title',
+ 'meta_description',
+ 'is_returnable',
+ 'options_container'
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function getChangelogData()
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $select = $connection->select()
+ ->from($connection->getTableName('test_view_cl'));
+ return $connection->fetchAll($select);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/enable_mview_for_test_view.php b/dev/tests/integration/testsuite/Magento/Eav/_files/enable_mview_for_test_view.php
new file mode 100644
index 0000000000000..bb22de22d0b54
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Eav/_files/enable_mview_for_test_view.php
@@ -0,0 +1,14 @@
+create(\Magento\Framework\Mview\View::class);
+$view->load('test_view');
+$view->subscribe();
diff --git a/lib/internal/Magento/Framework/Mview/Config/Converter.php b/lib/internal/Magento/Framework/Mview/Config/Converter.php
index 5c33ac150d00a..988cffa8b7ce2 100644
--- a/lib/internal/Magento/Framework/Mview/Config/Converter.php
+++ b/lib/internal/Magento/Framework/Mview/Config/Converter.php
@@ -5,10 +5,34 @@
*/
namespace Magento\Framework\Mview\Config;
+use Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor;
+use Magento\Framework\Mview\View\ChangeLogBatchWalker;
use Magento\Framework\Mview\View\SubscriptionInterface;
class Converter implements \Magento\Framework\Config\ConverterInterface
{
+ /**
+ * @var string
+ */
+ private $defaultProcessor;
+
+ /**
+ * @var string
+ */
+ private $defaultIterator;
+
+ /**
+ * @param string $defaultProcessor
+ * @param string $defaultIterator
+ */
+ public function __construct(
+ string $defaultProcessor = DefaultProcessor::class,
+ string $defaultIterator = ChangeLogBatchWalker::class
+ ) {
+ $this->defaultProcessor = $defaultProcessor;
+ $this->defaultIterator = $defaultIterator;
+ }
+
/**
* Convert dom node tree to array
*
@@ -28,6 +52,7 @@ public function convert($source)
$data['view_id'] = $viewId;
$data['action_class'] = $this->getAttributeValue($viewNode, 'class');
$data['group'] = $this->getAttributeValue($viewNode, 'group');
+ $data['walker'] = $this->getAttributeValue($viewNode, 'walker') ?: $this->defaultIterator;
$data['subscriptions'] = [];
/** @var $childNode \DOMNode */
@@ -76,6 +101,7 @@ protected function convertChild(\DOMNode $childNode, $data)
$name = $this->getAttributeValue($subscription, 'name');
$column = $this->getAttributeValue($subscription, 'entity_column');
$subscriptionModel = $this->getAttributeValue($subscription, 'subscription_model');
+
if (!empty($subscriptionModel)
&& !in_array(
SubscriptionInterface::class,
@@ -89,11 +115,44 @@ class_implements(ltrim($subscriptionModel, '\\'))
$data['subscriptions'][$name] = [
'name' => $name,
'column' => $column,
- 'subscription_model' => $subscriptionModel
+ 'subscription_model' => $subscriptionModel,
+ 'additional_columns' => $this->getAdditionalColumns($subscription),
+ 'processor' => $this->getAttributeValue($subscription, 'processor')
+ ?: $this->defaultProcessor
];
}
break;
}
return $data;
}
+
+ /**
+ * Retrieve additional columns of subscription table
+ *
+ * @param \DOMNode $subscription
+ * @return array
+ */
+ private function getAdditionalColumns(\DOMNode $subscription): array
+ {
+ $additionalColumns = [];
+ foreach ($subscription->childNodes as $childNode) {
+ if ($childNode->nodeType != XML_ELEMENT_NODE || $childNode->nodeName != 'additionalColumns') {
+ continue;
+ }
+
+ foreach ($childNode->childNodes as $columnNode) {
+ if ($columnNode->nodeName !== 'column') {
+ continue;
+ }
+
+ $additionalColumns[$this->getAttributeValue($columnNode, 'name')] = [
+ 'name' => $this->getAttributeValue($columnNode, 'name'),
+ 'cl_name' => $this->getAttributeValue($columnNode, 'cl_name'),
+ 'constant' => $this->getAttributeValue($columnNode, 'constant'),
+ ];
+ }
+ }
+
+ return $additionalColumns;
+ }
}
diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php
index 0a4e8a3840749..4af3f65be6883 100644
--- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php
+++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php
@@ -11,6 +11,7 @@
use Magento\Framework\DB\Adapter\Pdo\Mysql;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\DB\Select;
+use Magento\Framework\Mview\Config;
use Magento\Framework\Mview\View\Changelog;
use Magento\Framework\Mview\View\ChangelogInterface;
use PHPUnit\Framework\MockObject\MockObject;
@@ -46,7 +47,21 @@ protected function setUp(): void
$this->resourceMock = $this->createMock(ResourceConnection::class);
$this->mockGetConnection($this->connectionMock);
- $this->model = new Changelog($this->resourceMock);
+ $this->model = new Changelog($this->resourceMock, $this->getMviewConfigMock());
+ }
+
+ /**
+ * @return Config|MockObject
+ */
+ private function getMviewConfigMock()
+ {
+ $mviewConfigMock = $this->createMock(Config::class);
+ $mviewConfigMock->expects($this->any())
+ ->method('getView')
+ ->willReturn([
+ 'subscriptions' => []
+ ]);
+ return $mviewConfigMock;
}
public function testInstanceOf()
@@ -54,7 +69,7 @@ public function testInstanceOf()
$resourceMock =
$this->createMock(ResourceConnection::class);
$resourceMock->expects($this->once())->method('getConnection')->willReturn(true);
- $model = new Changelog($resourceMock);
+ $model = new Changelog($resourceMock, $this->getMviewConfigMock());
$this->assertInstanceOf(ChangelogInterface::class, $model);
}
@@ -65,7 +80,7 @@ public function testCheckConnectionException()
$resourceMock =
$this->createMock(ResourceConnection::class);
$resourceMock->expects($this->once())->method('getConnection')->willReturn(null);
- $model = new Changelog($resourceMock);
+ $model = new Changelog($resourceMock, $this->getMviewConfigMock());
$model->setViewId('ViewIdTest');
$this->assertNull($model);
}
diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php
index b91c0b525390f..2178009eda436 100644
--- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php
+++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php
@@ -7,10 +7,13 @@
namespace Magento\Framework\Mview\Test\Unit\View;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\Pdo\Mysql;
use Magento\Framework\DB\Ddl\Trigger;
use Magento\Framework\DB\Ddl\TriggerFactory;
+use Magento\Framework\Mview\Config;
+use Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor;
use Magento\Framework\Mview\View\ChangelogInterface;
use Magento\Framework\Mview\View\CollectionInterface;
use Magento\Framework\Mview\View\StateInterface;
@@ -48,6 +51,7 @@ class SubscriptionTest extends TestCase
protected function setUp(): void
{
+ $this->tableName = 'test_table';
$this->connectionMock = $this->createMock(Mysql::class);
$this->resourceMock = $this->createMock(ResourceConnection::class);
@@ -55,10 +59,17 @@ protected function setUp(): void
->method('quoteIdentifier')
->willReturnArgument(0);
+ $this->connectionMock->expects($this->any())
+ ->method('describeTable')
+ ->willReturn([]);
+
$this->resourceMock->expects($this->atLeastOnce())
->method('getConnection')
->willReturn($this->connectionMock);
-
+ ObjectManager::getInstance()->expects($this->any())
+ ->method('get')
+ ->with(DefaultProcessor::class)
+ ->willReturn(2);
$this->triggerFactoryMock = $this->createMock(TriggerFactory::class);
$this->viewCollectionMock = $this->getMockForAbstractClass(
CollectionInterface::class,
@@ -78,18 +89,32 @@ protected function setUp(): void
true,
[]
);
-
+ $this->viewMock->expects($this->any())
+ ->method('getId')
+ ->willReturn(1);
$this->resourceMock->expects($this->any())
->method('getTableName')
->willReturnArgument(0);
-
+ $mviewConfigMock = $this->createMock(Config::class);
+ $mviewConfigMock->expects($this->any())
+ ->method('getView')
+ ->willReturn([
+ 'subscriptions' => [
+ $this->tableName => [
+ 'processor' => DefaultProcessor::class
+ ]
+ ]
+ ]);
+ $this->mviewConfig = $mviewConfigMock;
$this->model = new Subscription(
$this->resourceMock,
$this->triggerFactoryMock,
$this->viewCollectionMock,
$this->viewMock,
$this->tableName,
- 'columnName'
+ 'columnName',
+ [],
+ $mviewConfigMock
);
}
@@ -122,7 +147,7 @@ public function testCreate()
$triggerMock->expects($this->exactly(3))
->method('setName')
->with($triggerName)->willReturnSelf();
- $triggerMock->expects($this->exactly(3))
+ $triggerMock->expects($this->any())
->method('getName')
->willReturn('triggerName');
$triggerMock->expects($this->exactly(3))
@@ -167,7 +192,7 @@ public function testCreate()
true,
[]
);
- $changelogMock->expects($this->exactly(3))
+ $changelogMock->expects($this->any())
->method('getName')
->willReturn('test_view_cl');
$changelogMock->expects($this->exactly(3))
@@ -191,7 +216,7 @@ public function testCreate()
true,
[]
);
- $otherChangelogMock->expects($this->exactly(3))
+ $otherChangelogMock->expects($this->any())
->method('getName')
->willReturn('other_test_view_cl');
$otherChangelogMock->expects($this->exactly(3))
@@ -217,7 +242,7 @@ public function testCreate()
->method('getChangelog')
->willReturn($otherChangelogMock);
- $this->viewMock->expects($this->exactly(3))
+ $this->viewMock->expects($this->any())
->method('getId')
->willReturn('this_id');
$this->viewMock->expects($this->never())
@@ -235,7 +260,6 @@ public function testCreate()
$this->connectionMock->expects($this->exactly(3))
->method('createTrigger')
->with($triggerMock);
-
$this->model->create();
}
@@ -244,7 +268,7 @@ public function testRemove()
$triggerMock = $this->createMock(Trigger::class);
$triggerMock->expects($this->exactly(3))
->method('setName')->willReturnSelf();
- $triggerMock->expects($this->exactly(3))
+ $triggerMock->expects($this->any())
->method('getName')
->willReturn('triggerName');
$triggerMock->expects($this->exactly(3))
@@ -271,7 +295,7 @@ public function testRemove()
true,
[]
);
- $otherChangelogMock->expects($this->exactly(3))
+ $otherChangelogMock->expects($this->any())
->method('getName')
->willReturn('other_test_view_cl');
$otherChangelogMock->expects($this->exactly(3))
@@ -297,7 +321,7 @@ public function testRemove()
->method('getChangelog')
->willReturn($otherChangelogMock);
- $this->viewMock->expects($this->exactly(3))
+ $this->viewMock->expects($this->any())
->method('getId')
->willReturn('this_id');
$this->viewMock->expects($this->never())
diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php
index 7d69ff43f158b..6fff95a0cb089 100644
--- a/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php
+++ b/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php
@@ -7,6 +7,7 @@
namespace Magento\Framework\Mview\Test\Unit;
+use Laminas\Log\Filter\Mock;
use Magento\Framework\Mview\ActionFactory;
use Magento\Framework\Mview\ActionInterface;
use Magento\Framework\Mview\ConfigInterface;
@@ -53,6 +54,11 @@ class ViewTest extends TestCase
*/
protected $subscriptionFactoryMock;
+ /**
+ * @var MockObject|View\ChangeLogBatchWalkerInterface
+ */
+ private $iteratorMock;
+
/**
* @inheritdoc
*/
@@ -67,6 +73,7 @@ protected function setUp(): void
true,
['getView']
);
+ $this->iteratorMock = $this->createMock(View\ChangeLogBatchWalker::class);
$this->actionFactoryMock = $this->createPartialMock(ActionFactory::class, ['get']);
$this->stateMock = $this->createPartialMock(
State::class,
@@ -97,7 +104,10 @@ protected function setUp(): void
$this->actionFactoryMock,
$this->stateMock,
$this->changelogMock,
- $this->subscriptionFactoryMock
+ $this->subscriptionFactoryMock,
+ [],
+ [],
+ $this->iteratorMock
);
}
@@ -334,7 +344,7 @@ public function testUpdate()
$currentVersionId
);
$this->changelogMock->expects(
- $this->once()
+ $this->any()
)->method(
'getList'
)->with(
@@ -345,6 +355,7 @@ public function testUpdate()
);
$actionMock = $this->getMockForAbstractClass(ActionInterface::class);
+ $this->iteratorMock->expects($this->once())->method('walk')->willReturn($listId);
$actionMock->expects($this->once())->method('execute')->with($listId)->willReturnSelf();
$this->actionFactoryMock->expects(
$this->once()
@@ -390,7 +401,7 @@ public function testUpdateEx(): void
->expects($this->once())
->method('getVersion')
->willReturn($currentVersionId);
-
+ $this->iteratorMock->expects($this->any())->method('walk')->willReturn($this->generateChangeLog(150, 1, 150));
$this->changelogMock->method('getList')
->willReturnMap(
[
@@ -401,7 +412,7 @@ public function testUpdateEx(): void
);
$actionMock = $this->getMockForAbstractClass(ActionInterface::class);
- $actionMock->expects($this->once())
+ $actionMock->expects($this->any())
->method('execute')
->with($this->generateChangeLog(150, 1, 150))
->willReturnSelf();
@@ -457,7 +468,7 @@ public function testUpdateWithException()
$this->stateMock->expects($this->atLeastOnce())
->method('getMode')
->willReturn(StateInterface::MODE_ENABLED);
- $this->stateMock->expects($this->exactly(2))
+ $this->stateMock->expects($this->any())
->method('getStatus')
->willReturn(StateInterface::STATUS_IDLE);
$this->stateMock->expects($this->exactly(2))
@@ -472,16 +483,9 @@ public function testUpdateWithException()
)->willReturn(
$currentVersionId
);
- $this->changelogMock->expects(
- $this->once()
- )->method(
- 'getList'
- )->with(
- $lastVersionId,
- $currentVersionId
- )->willReturn(
- $listId
- );
+ $this->iteratorMock->expects($this->any())
+ ->method('walk')
+ ->willReturn([2,3]);
$actionMock = $this->createPartialMock(ActionInterface::class, ['execute']);
$actionMock->expects($this->once())->method('execute')->with($listId)->willReturnCallback(
@@ -767,8 +771,11 @@ public function testGetUpdated()
protected function loadView()
{
$viewId = 'view_test';
+ $this->changelogMock->expects($this->any())
+ ->method('getViewId')
+ ->willReturn($viewId);
$this->configMock->expects(
- $this->once()
+ $this->any()
)->method(
'getView'
)->with(
@@ -788,7 +795,7 @@ protected function getViewData()
'view_id' => 'view_test',
'action_class' => 'Some\Class\Name',
'group' => 'some_group',
- 'subscriptions' => ['some_entity' => ['name' => 'some_entity', 'column' => 'entity_id']]
+ 'subscriptions' => ['some_entity' => ['name' => 'some_entity', 'column' => 'entity_id']],
];
}
}
diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php b/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php
index a19f90546bac3..af488ecf5589e 100644
--- a/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php
+++ b/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php
@@ -20,14 +20,19 @@
'some_entity' => [
'name' => 'some_entity',
'column' => 'entity_id',
- 'subscription_model' => null
+ 'subscription_model' => null,
+ 'additional_columns' => [],
+ 'processor' => \Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor::class
],
'some_product_relation' => [
'name' => 'some_product_relation',
'column' => 'product_id',
- 'subscription_model' => null
+ 'subscription_model' => null,
+ 'additional_columns' => [],
+ 'processor' => \Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor::class
],
],
+ 'walker' => \Magento\Framework\Mview\View\ChangeLogBatchWalker::class
],
]
];
diff --git a/lib/internal/Magento/Framework/Mview/View.php b/lib/internal/Magento/Framework/Mview/View.php
index dade475a20482..e3e8726332783 100644
--- a/lib/internal/Magento/Framework/Mview/View.php
+++ b/lib/internal/Magento/Framework/Mview/View.php
@@ -9,7 +9,11 @@
namespace Magento\Framework\Mview;
use InvalidArgumentException;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DataObject;
+use Magento\Framework\Mview\View\ChangeLogBatchWalkerFactory;
+use Magento\Framework\Mview\View\ChangeLogBatchWalkerInterface;
+use Magento\Framework\Mview\View\ChangelogInterface;
use Magento\Framework\Mview\View\ChangelogTableNotExistsException;
use Magento\Framework\Mview\View\SubscriptionFactory;
use Exception;
@@ -27,11 +31,6 @@ class View extends DataObject implements ViewInterface
*/
const DEFAULT_BATCH_SIZE = 1000;
- /**
- * Max versions to load from database at a time
- */
- private static $maxVersionQueryBatch = 100000;
-
/**
* @var string
*/
@@ -67,6 +66,11 @@ class View extends DataObject implements ViewInterface
*/
private $changelogBatchSize;
+ /**
+ * @var ChangeLogBatchWalkerFactory
+ */
+ private $changeLogBatchWalkerFactory;
+
/**
* @param ConfigInterface $config
* @param ActionFactory $actionFactory
@@ -75,6 +79,7 @@ class View extends DataObject implements ViewInterface
* @param SubscriptionFactory $subscriptionFactory
* @param array $data
* @param array $changelogBatchSize
+ * @param ChangeLogBatchWalkerFactory $changeLogBatchWalkerFactory
*/
public function __construct(
ConfigInterface $config,
@@ -83,7 +88,8 @@ public function __construct(
View\ChangelogInterface $changelog,
SubscriptionFactory $subscriptionFactory,
array $data = [],
- array $changelogBatchSize = []
+ array $changelogBatchSize = [],
+ ChangeLogBatchWalkerFactory $changeLogBatchWalkerFactory = null
) {
$this->config = $config;
$this->actionFactory = $actionFactory;
@@ -92,6 +98,8 @@ public function __construct(
$this->subscriptionFactory = $subscriptionFactory;
$this->changelogBatchSize = $changelogBatchSize;
parent::__construct($data);
+ $this->changeLogBatchWalkerFactory = $changeLogBatchWalkerFactory ?:
+ ObjectManager::getInstance()->get(ChangeLogBatchWalkerFactory::class);
}
/**
@@ -296,41 +304,36 @@ private function executeAction(ActionInterface $action, int $lastVersionId, int
: self::DEFAULT_BATCH_SIZE;
$vsFrom = $lastVersionId;
- while ($vsFrom < $currentVersionId) {
- $ids = $this->getBatchOfIds($vsFrom, $currentVersionId);
- // We run the actual indexer in batches.
- // Chunked AFTER loading to avoid duplicates in separate chunks.
- $chunks = array_chunk($ids, $batchSize);
- foreach ($chunks as $ids) {
- $action->execute($ids);
- }
+ $iterator = $this->getWalker($this->getChangelog(), $vsFrom, $currentVersionId, $batchSize);
+ foreach ($iterator as $batchOfIds) {
+ $action->execute($batchOfIds);
}
}
/**
- * Get batch of entity ids
+ * Create and validate walker class for changelog
*
- * @param int $lastVersionId
- * @param int $currentVersionId
- * @return array
- */
- private function getBatchOfIds(int &$lastVersionId, int $currentVersionId): array
+ * @param ChangelogInterface $changelog
+ * @param int $vsFrom
+ * @param int $vsTo
+ * @param int $batchSize
+ * @return ChangeLogBatchWalkerInterface|mixed
+ */
+ private function getWalker(
+ ChangelogInterface $changelog,
+ int $vsFrom,
+ int $vsTo,
+ int $batchSize
+ ): ChangeLogBatchWalkerInterface
{
- $ids = [];
- $versionBatchSize = self::$maxVersionQueryBatch;
- $idsBatchSize = self::$maxVersionQueryBatch;
- for ($vsFrom = $lastVersionId; $vsFrom < $currentVersionId; $vsFrom += $versionBatchSize) {
- // Don't go past the current version for atomicity.
- $versionTo = min($currentVersionId, $vsFrom + $versionBatchSize);
- /** To avoid duplicate ids need to flip and merge the array */
- $ids += array_flip($this->getChangelog()->getList($vsFrom, $versionTo));
- $lastVersionId = $versionTo;
- if (count($ids) >= $idsBatchSize) {
- break;
- }
- }
-
- return array_keys($ids);
+ $config = $this->config->getView($this->changelog->getViewId());
+ $walkerClass = $config['walker'];
+ return $this->changeLogBatchWalkerFactory->create($walkerClass, [
+ 'changelog' => $changelog,
+ 'fromVersionId' => $vsFrom,
+ 'toVersionId' => $vsTo,
+ 'batchSize' => $batchSize
+ ]);
}
/**
diff --git a/lib/internal/Magento/Framework/Mview/View/AdditionalColumnProcessorInterface.php b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnProcessorInterface.php
new file mode 100644
index 0000000000000..49af83556d023
--- /dev/null
+++ b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnProcessorInterface.php
@@ -0,0 +1,37 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTriggerColumns(string $eventPrefix, array $additionalColumns): array
+ {
+ $resource = $this->resourceConnection->getConnection();
+ $triggersColumns = [
+ 'column_names' => [],
+ 'column_values' => []
+ ];
+
+ foreach ($additionalColumns as $additionalColumn) {
+ $triggersColumns['column_names'][$additionalColumn['name']] = $resource->quoteIdentifier(
+ $additionalColumn['cl_name']
+ );
+
+ $triggersColumns['column_values'][$additionalColumn['name']] = isset($additionalColumn['constant']) ?
+ $resource->quote($additionalColumn['constant']) :
+ $eventPrefix . $resource->quoteIdentifier($additionalColumn['name']);
+ }
+
+ return $triggersColumns;
+ }
+
+ /**
+ * Retrieve pre-statement for a trigger in Mview
+ *
+ * @return string
+ */
+ public function getPreStatements(): string
+ {
+ return '';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function processColumnForCLTable(Table $table, string $columnName): void
+ {
+ $table->addColumn(
+ $columnName,
+ \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
+ null,
+ ['unsigned' => true, 'nullable' => true, 'default' => null],
+ $columnName
+ );
+ }
+}
diff --git a/lib/internal/Magento/Framework/Mview/View/AdditionalColumnsProcessor/ProcessorFactory.php b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnsProcessor/ProcessorFactory.php
new file mode 100644
index 0000000000000..5907cefbffd50
--- /dev/null
+++ b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnsProcessor/ProcessorFactory.php
@@ -0,0 +1,38 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Instantiate additional columns processor
+ *
+ * @param string $processorClassName
+ * @return AdditionalColumnProcessorInterface
+ */
+ public function create(string $processorClassName): AdditionalColumnProcessorInterface
+ {
+ return $this->objectManager->create($processorClassName);
+ }
+}
diff --git a/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalker.php b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalker.php
new file mode 100644
index 0000000000000..2f7be835b2a13
--- /dev/null
+++ b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalker.php
@@ -0,0 +1,104 @@
+resourceConnection = $resourceConnection;
+ $this->changelog = $changelog;
+ $this->fromVersionId = $fromVersionId;
+ $this->toVersionId = $toVersionId;
+ $this->batchSize = $batchSize;
+ }
+
+ /**
+ * @return \Generator|\Traversable
+ * @throws \Exception
+ */
+ public function getIterator(): \Generator
+ {
+ while ($this->fromVersionId < $this->toVersionId) {
+ $ids = $this->walk();
+
+ if (empty($ids)) {
+ break;
+ }
+ $this->fromVersionId += $this->batchSize;
+ yield $ids;
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ private function walk()
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $changelogTableName = $this->resourceConnection->getTableName($this->changelog->getName());
+
+ $select = $connection->select()->distinct(true)
+ ->where(
+ 'version_id > ?',
+ $this->fromVersionId
+ )
+ ->where(
+ 'version_id <= ?',
+ $this->toVersionId
+ )
+ ->group([$this->changelog->getColumnName()])
+ ->limit($this->batchSize);
+
+ $select->from($changelogTableName, [$this->changelog->getColumnName()]);
+ return $connection->fetchCol($select);
+ }
+}
diff --git a/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerFactory.php b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerFactory.php
new file mode 100644
index 0000000000000..8670f78368178
--- /dev/null
+++ b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerFactory.php
@@ -0,0 +1,38 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Instantiate BatchWalker interface
+ *
+ * @param string $batchWalkerClassName
+ * @param array $data
+ * @return ChangeLogBatchWalkerInterface
+ */
+ public function create(string $batchWalkerClassName, array $data): ChangeLogBatchWalkerInterface
+ {
+ return $this->objectManager->create($batchWalkerClassName, $data);
+ }
+}
diff --git a/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerInterface.php b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerInterface.php
new file mode 100644
index 0000000000000..55d4abfcb4704
--- /dev/null
+++ b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerInterface.php
@@ -0,0 +1,20 @@
+connection = $resource->getConnection();
$this->resource = $resource;
$this->checkConnection();
+ $this->mviewConfig = $mviewConfig;
+ $this->additionalColumnsProcessorFactory = $additionalColumnsProcessorFactory ??
+ ObjectManager::getInstance()->get(ProcessorFactory::class);
}
/**
@@ -83,7 +110,7 @@ public function create()
$table = $this->connection->newTable(
$changelogTableName
)->addColumn(
- 'version_id',
+ self::VERSION_ID_COLUMN_NAME,
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
@@ -95,10 +122,46 @@ public function create()
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Entity ID'
);
+
+ foreach ($this->initAdditionalColumnData() as $columnData) {
+ /** @var AdditionalColumnProcessorInterface $processor */
+ $processorClass = $columnData['processor'];
+ $processor = $this->additionalColumnsProcessorFactory->create($processorClass);
+ $processor->processColumnForCLTable($table, $columnData['cl_name']);
+ }
+
$this->connection->createTable($table);
}
}
+ /**
+ * Retrieve additional column data
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function initAdditionalColumnData(): array
+ {
+ $config = $this->mviewConfig->getView($this->getViewId());
+ $additionalColumns = [];
+
+ if (!$config) {
+ return $additionalColumns;
+ }
+
+ foreach ($config['subscriptions'] as $subscription) {
+ if (isset($subscription['additional_columns'])) {
+ foreach ($subscription['additional_columns'] as $additionalColumn) {
+ //We are gatherig unique change log column names in order to create them later
+ $additionalColumns[$additionalColumn['cl_name']] = $additionalColumn;
+ $additionalColumns[$additionalColumn['cl_name']]['processor'] = $subscription['processor'];
+ }
+ }
+ }
+
+ return $additionalColumns;
+ }
+
/**
* Drop changelog table
*
@@ -139,7 +202,7 @@ public function clear($versionId)
*
* @param int $fromVersionId
* @param int $toVersionId
- * @return int[]
+ * @return array
* @throws ChangelogTableNotExistsException
*/
public function getList($fromVersionId, $toVersionId)
diff --git a/lib/internal/Magento/Framework/Mview/View/ChangelogInterface.php b/lib/internal/Magento/Framework/Mview/View/ChangelogInterface.php
index b00c1ca3a2e33..79f998cbe02b6 100644
--- a/lib/internal/Magento/Framework/Mview/View/ChangelogInterface.php
+++ b/lib/internal/Magento/Framework/Mview/View/ChangelogInterface.php
@@ -11,6 +11,11 @@
*/
interface ChangelogInterface
{
+ const ATTRIBUTE_SCOPE_SUPPORT = 'attribute_scope';
+ const STORE_SCOPE_SUPPORT = 'store_scope';
+ const ATTRIBUTE_COLUMN = 'attribute_id';
+ const STORE_COLUMN = 'store_id';
+
/**
* Create changelog table
*
diff --git a/lib/internal/Magento/Framework/Mview/View/Subscription.php b/lib/internal/Magento/Framework/Mview/View/Subscription.php
index ddfa39f0a089f..eacc376972a71 100644
--- a/lib/internal/Magento/Framework/Mview/View/Subscription.php
+++ b/lib/internal/Magento/Framework/Mview/View/Subscription.php
@@ -6,14 +6,17 @@
namespace Magento\Framework\Mview\View;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Ddl\Trigger;
+use Magento\Framework\Mview\Config;
+use Magento\Framework\Mview\View\AdditionalColumnsProcessor\ProcessorFactory;
use Magento\Framework\Mview\View\StateInterface;
/**
* Class Subscription
*
- * @package Magento\Framework\Mview\View
+ * Create triggers for subscription tables for Mview
*/
class Subscription implements SubscriptionInterface
{
@@ -69,6 +72,16 @@ class Subscription implements SubscriptionInterface
*/
protected $resource;
+ /**
+ * @var Config
+ */
+ private $mviewConfig;
+
+ /**
+ * @var ProcessorFactory|null
+ */
+ private $additionalColumnsProcessorFactory;
+
/**
* @param ResourceConnection $resource
* @param \Magento\Framework\DB\Ddl\TriggerFactory $triggerFactory
@@ -77,6 +90,10 @@ class Subscription implements SubscriptionInterface
* @param string $tableName
* @param string $columnName
* @param array $ignoredUpdateColumns
+ * @param Config|null $mviewConfig
+ * @param ProcessorFactory|null $additionalColumnsProcessorFactory
+ *
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
ResourceConnection $resource,
@@ -85,7 +102,9 @@ public function __construct(
\Magento\Framework\Mview\ViewInterface $view,
$tableName,
$columnName,
- $ignoredUpdateColumns = []
+ $ignoredUpdateColumns = [],
+ Config $mviewConfig = null,
+ ProcessorFactory $additionalColumnsProcessorFactory = null
) {
$this->connection = $resource->getConnection();
$this->triggerFactory = $triggerFactory;
@@ -95,6 +114,9 @@ public function __construct(
$this->columnName = $columnName;
$this->resource = $resource;
$this->ignoredUpdateColumns = $ignoredUpdateColumns;
+ $this->mviewConfig = $mviewConfig ?? ObjectManager::getInstance()->get(Config::class);
+ $this->additionalColumnsProcessorFactory = $additionalColumnsProcessorFactory ??
+ ObjectManager::getInstance()->get(ProcessorFactory::class);
}
/**
@@ -189,6 +211,41 @@ protected function getLinkedViews()
return $this->linkedViews;
}
+ /**
+ * Prepare columns for trigger statement. Should be protected in order to serve new approach
+ *
+ * @param ChangelogInterface $changelog
+ * @param string $event
+ * @return array
+ * @throws \Exception
+ */
+ protected function prepareColumns(ChangelogInterface $changelog, string $event): array
+ {
+ $prefix = $event === Trigger::EVENT_DELETE ? 'OLD.' : 'NEW.';
+ $subscriptionData = $this->mviewConfig->getView(
+ $changelog->getViewId()
+ )['subscriptions'][$this->getTableName()];
+
+ $columns = [
+ 'column_names' => [
+ 'entity_id' => $this->connection->quoteIdentifier($changelog->getColumnName())
+ ],
+ 'column_values' => [
+ 'entity_id' => $this->getEntityColumn($prefix)
+ ]
+ ];
+
+ if (!empty($subscriptionData['additional_columns'])) {
+ $processor = $this->getProcessor();
+ $columns = array_replace_recursive(
+ $columns,
+ $processor->getTriggerColumns($prefix, $subscriptionData['additional_columns'])
+ );
+ }
+
+ return $columns;
+ }
+
/**
* Build trigger statement for INSERT, UPDATE, DELETE events
*
@@ -198,13 +255,10 @@ protected function getLinkedViews()
*/
protected function buildStatement($event, $changelog)
{
+ $trigger = "%sINSERT IGNORE INTO %s (%s) VALUES (%s);";
switch ($event) {
- case Trigger::EVENT_INSERT:
- $trigger = "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);";
- break;
case Trigger::EVENT_UPDATE:
$tableName = $this->resource->getTableName($this->getTableName());
- $trigger = "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);";
if ($this->connection->isTableExists($tableName) &&
$describe = $this->connection->describeTable($tableName)
) {
@@ -226,20 +280,40 @@ protected function buildStatement($event, $changelog)
}
}
break;
- case Trigger::EVENT_DELETE:
- $trigger = "INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);";
- break;
- default:
- return '';
}
+ $columns = $this->prepareColumns($changelog, $event);
return sprintf(
$trigger,
+ $this->getProcessor()->getPreStatements(),
$this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())),
- $this->connection->quoteIdentifier($changelog->getColumnName()),
- $this->connection->quoteIdentifier($this->getColumnName())
+ implode(", ", $columns['column_names']),
+ implode(", ", $columns['column_values'])
);
}
+ /**
+ * Instantiate and retrieve additional columns processor
+ *
+ * @return AdditionalColumnProcessorInterface
+ */
+ private function getProcessor(): AdditionalColumnProcessorInterface
+ {
+ $subscriptionData = $this->mviewConfig->getView($this->getView()->getId())['subscriptions'];
+ $processorClass = $subscriptionData[$this->getTableName()]['processor'];
+ return $this->additionalColumnsProcessorFactory->create($processorClass);
+ }
+
+ /**
+ * Retrieves entity column for subscription tables
+ *
+ * @param string $prefix
+ * @return string
+ */
+ public function getEntityColumn(string $prefix): string
+ {
+ return $prefix . $this->connection->quoteIdentifier($this->getColumnName());
+ }
+
/**
* Build an "after" event for the given table and event
*
diff --git a/lib/internal/Magento/Framework/Mview/etc/mview.xsd b/lib/internal/Magento/Framework/Mview/etc/mview.xsd
index dfff4964f6587..04754fa499249 100644
--- a/lib/internal/Magento/Framework/Mview/etc/mview.xsd
+++ b/lib/internal/Magento/Framework/Mview/etc/mview.xsd
@@ -46,6 +46,7 @@
+
@@ -76,9 +77,25 @@
Table declaration.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+