Skip to content

Commit 4fa0934

Browse files
author
Igor Melnikov
committed
MAGETWO-66331: Implement solution to update duplicate rows at once and add environment variable to control batch size
- implementing solution, updating tests
1 parent 30ceb4c commit 4fa0934

File tree

5 files changed

+234
-58
lines changed

5 files changed

+234
-58
lines changed

app/etc/di.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,4 +1239,9 @@
12391239
</argument>
12401240
</arguments>
12411241
</type>
1242+
<type name="Magento\Framework\DB\FieldDataConverter">
1243+
<arguments>
1244+
<argument name="envBatchSize" xsi:type="init_parameter">Magento\Framework\DB\FieldDataConverter::BATCH_SIZE_VARIABLE_NAME</argument>
1245+
</arguments>
1246+
</type>
12421247
</config>

dev/tests/integration/testsuite/Magento/Framework/DB/DataConverter/DataConverterTest.php

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88
use Magento\Framework\DB\Adapter\Pdo\Mysql;
99
use Magento\Framework\DB\FieldDataConverter;
1010
use Magento\Framework\DB\Select;
11+
use Magento\Framework\DB\Select\QueryModifierInterface;
1112
use Magento\Framework\DB\Select\InQueryModifier;
1213
use Magento\Framework\Serialize\Serializer\Serialize;
1314
use Magento\TestFramework\Helper\Bootstrap;
15+
use Magento\Framework\DB\Query\Generator;
16+
use Magento\Framework\DB\Query\BatchIterator;
17+
use Magento\Framework\ObjectManagerInterface;
1418

1519
class DataConverterTest extends \PHPUnit_Framework_TestCase
1620
{
17-
/**
18-
* @var \Magento\Framework\ObjectManagerInterface
19-
*/
20-
private $objectManager;
21-
2221
/**
2322
* @var InQueryModifier|\PHPUnit_Framework_MockObject_MockObject
2423
*/
@@ -30,12 +29,12 @@ class DataConverterTest extends \PHPUnit_Framework_TestCase
3029
private $dataConverter;
3130

3231
/**
33-
* @var \Magento\Framework\DB\Query\BatchIterator|\PHPUnit_Framework_MockObject_MockObject
32+
* @var BatchIterator|\PHPUnit_Framework_MockObject_MockObject
3433
*/
3534
private $iteratorMock;
3635

3736
/**
38-
* @var \Magento\Framework\DB\Query\Generator|\PHPUnit_Framework_MockObject_MockObject
37+
* @var Generator|\PHPUnit_Framework_MockObject_MockObject
3938
*/
4039
private $queryGeneratorMock;
4140

@@ -54,6 +53,11 @@ class DataConverterTest extends \PHPUnit_Framework_TestCase
5453
*/
5554
private $fieldDataConverter;
5655

56+
/**
57+
* @var ObjectManagerInterface
58+
*/
59+
private $objectManager;
60+
5761
/**
5862
* Set up before test
5963
*/
@@ -62,23 +66,24 @@ protected function setUp()
6266
$this->objectManager = Bootstrap::getObjectManager();
6367

6468
/** @var InQueryModifier $queryModifier */
65-
$this->queryModifierMock = $this->getMockBuilder(Select\QueryModifierInterface::class)
69+
$this->queryModifierMock = $this->getMockBuilder(QueryModifierInterface::class)
6670
->disableOriginalConstructor()
6771
->setMethods(['modify'])
6872
->getMock();
6973

7074
$this->dataConverter = $this->objectManager->get(SerializedToJson::class);
7175

72-
$this->iteratorMock = $this->getMockBuilder(\Magento\Framework\DB\Query\BatchIterator::class)
76+
$this->iteratorMock = $this->getMockBuilder(BatchIterator::class)
7377
->disableOriginalConstructor()
7478
->setMethods(['current', 'valid', 'next'])
7579
->getMock();
7680

7781
$iterationComplete = false;
7882

7983
// mock valid() call so iterator passes only current() result in foreach invocation
80-
$this->iteratorMock->expects($this->any())->method('valid')->will(
81-
$this->returnCallback(
84+
$this->iteratorMock->expects($this->any())
85+
->method('valid')
86+
->willReturnCallback(
8287
function () use (&$iterationComplete) {
8388
if (!$iterationComplete) {
8489
$iterationComplete = true;
@@ -87,10 +92,9 @@ function () use (&$iterationComplete) {
8792
return false;
8893
}
8994
}
90-
)
91-
);
95+
);
9296

93-
$this->queryGeneratorMock = $this->getMockBuilder(\Magento\Framework\DB\Query\Generator::class)
97+
$this->queryGeneratorMock = $this->getMockBuilder(Generator::class)
9498
->disableOriginalConstructor()
9599
->setMethods(['generate'])
96100
->getMock();
@@ -111,7 +115,7 @@ function () use (&$iterationComplete) {
111115

112116
$this->adapterMock = $this->getMockBuilder(Mysql::class)
113117
->disableOriginalConstructor()
114-
->setMethods(['fetchAll', 'quoteInto', 'update'])
118+
->setMethods(['fetchPairs', 'quoteInto', 'update'])
115119
->getMock();
116120

117121
$this->adapterMock->expects($this->any())
@@ -129,6 +133,7 @@ function () use (&$iterationComplete) {
129133

130134
/**
131135
* Test that exception with valid text is thrown when data is corrupted
136+
*
132137
* @expectedException \Magento\Framework\DB\FieldDataConversionException
133138
* @expectedExceptionMessage Error converting field `value` in table `table` where `id`=2 using
134139
*/
@@ -140,18 +145,18 @@ public function testDataConvertErrorReporting()
140145
$serializedDataLength = strlen($serializedData);
141146
$brokenSerializedData = substr($serializedData, 0, $serializedDataLength - 6);
142147
$rows = [
143-
['id' => 1, 'value' => 'N;'],
144-
['id' => 2, 'value' => $brokenSerializedData],
148+
1 => 'N;',
149+
2 => $brokenSerializedData,
145150
];
146151

147152
$this->adapterMock->expects($this->any())
148-
->method('fetchAll')
153+
->method('fetchPairs')
149154
->with($this->selectByRangeMock)
150155
->will($this->returnValue($rows));
151156

152157
$this->adapterMock->expects($this->once())
153158
->method('update')
154-
->with('table', ['value' => 'null'], ['id = ?' => 1]);
159+
->with('table', ['value' => 'null'], ['id IN (?)' => [1]]);
155160

156161
$this->fieldDataConverter->convert($this->adapterMock, 'table', 'id', 'value', $this->queryModifierMock);
157162
}
@@ -161,23 +166,23 @@ public function testDataConvertErrorReporting()
161166
public function testAlreadyConvertedDataSkipped()
162167
{
163168
$rows = [
164-
['id' => 2, 'value' => '[]'],
165-
['id' => 3, 'value' => '{}'],
166-
['id' => 4, 'value' => 'null'],
167-
['id' => 5, 'value' => '""'],
168-
['id' => 6, 'value' => '0'],
169-
['id' => 7, 'value' => 'N;'],
170-
['id' => 8, 'value' => '{"valid": "json value"}'],
169+
2 => '[]',
170+
3 => '{}',
171+
4 => 'null',
172+
5 => '""',
173+
6 => '0',
174+
7 => 'N;',
175+
8 => '{"valid": "json value"}',
171176
];
172177

173178
$this->adapterMock->expects($this->any())
174-
->method('fetchAll')
179+
->method('fetchPairs')
175180
->with($this->selectByRangeMock)
176181
->will($this->returnValue($rows));
177182

178183
$this->adapterMock->expects($this->once())
179184
->method('update')
180-
->with('table', ['value' => 'null'], ['id = ?' => 7]);
185+
->with('table', ['value' => 'null'], ['id IN (?)' => [7]]);
181186

182187
$this->fieldDataConverter->convert($this->adapterMock, 'table', 'id', 'value', $this->queryModifierMock);
183188
}

lib/internal/Magento/Framework/DB/FieldDataConverter.php

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66
namespace Magento\Framework\DB;
77

8-
use Magento\Framework\App\ObjectManager;
98
use Magento\Framework\DB\Adapter\AdapterInterface;
109
use Magento\Framework\DB\DataConverter\DataConversionException;
1110
use Magento\Framework\DB\Query\Generator;
@@ -17,6 +16,26 @@
1716
*/
1817
class FieldDataConverter
1918
{
19+
/**
20+
* Batch size env variable name
21+
*/
22+
const BATCH_SIZE_VARIABLE_NAME = 'DATA_CONVERTER_BATCH_SIZE';
23+
24+
/**
25+
* Default batch size
26+
*/
27+
const DEFAULT_BATCH_SIZE = 50000;
28+
29+
/**
30+
* Min batch size
31+
*/
32+
const MIN_BATCH_SIZE = 25000;
33+
34+
/**
35+
* Max batch size
36+
*/
37+
const MAX_BATCH_SIZE = 500000;
38+
2039
/**
2140
* @var Generator
2241
*/
@@ -32,25 +51,33 @@ class FieldDataConverter
3251
*/
3352
private $selectFactory;
3453

54+
/**
55+
* @var string|null
56+
*/
57+
private $envBatchSize;
58+
3559
/**
3660
* Constructor
3761
*
3862
* @param Generator $queryGenerator
3963
* @param DataConverterInterface $dataConverter
40-
* @param SelectFactory $selectFactory
64+
* @param SelectFactory|null $selectFactory
65+
* @param string $envBatchSize
4166
*/
4267
public function __construct(
4368
Generator $queryGenerator,
4469
DataConverterInterface $dataConverter,
45-
SelectFactory $selectFactory = null
70+
SelectFactory $selectFactory,
71+
$envBatchSize
4672
) {
4773
$this->queryGenerator = $queryGenerator;
4874
$this->dataConverter = $dataConverter;
49-
$this->selectFactory = $selectFactory ?: ObjectManager::getInstance()->get(SelectFactory::class);
75+
$this->selectFactory = $selectFactory;
76+
$this->envBatchSize = $envBatchSize;
5077
}
5178

5279
/**
53-
* Convert table field data from one representation to another uses DataConverterInterface
80+
* Convert table field data from one representation to another
5481
*
5582
* @param AdapterInterface $connection
5683
* @param string $table
@@ -73,18 +100,20 @@ public function convert(
73100
if ($queryModifier) {
74101
$queryModifier->modify($select);
75102
}
76-
$iterator = $this->queryGenerator->generate($identifier, $select);
103+
$iterator = $this->queryGenerator->generate($identifier, $select, $this->getBatchSize());
77104
foreach ($iterator as $selectByRange) {
78-
$rows = $connection->fetchAll($selectByRange);
79-
foreach ($rows as $row) {
105+
$rows = $connection->fetchPairs($selectByRange);
106+
$uniqueFieldDataArray = array_unique($rows);
107+
foreach ($uniqueFieldDataArray as $uniqueFieldData) {
108+
$ids = array_keys($rows, $uniqueFieldData);
80109
try {
81-
$convertedValue = $this->dataConverter->convert($row[$field]);
82-
if ($row[$field] === $convertedValue) {
83-
// skip for data rows that have been already converted
110+
$convertedValue = $this->dataConverter->convert($uniqueFieldData);
111+
if ($uniqueFieldData === $convertedValue) {
112+
// Skip for data rows that have been already converted
84113
continue;
85114
}
86115
$bind = [$field => $convertedValue];
87-
$where = [$identifier . ' = ?' => (int) $row[$identifier]];
116+
$where = [$identifier . ' IN (?)' => $ids];
88117
$connection->update($table, $bind, $where);
89118
} catch (DataConversionException $e) {
90119
throw new \Magento\Framework\DB\FieldDataConversionException(
@@ -93,7 +122,7 @@ public function convert(
93122
$field,
94123
$table,
95124
$identifier,
96-
$row[$identifier],
125+
implode(', ', $ids),
97126
get_class($this->dataConverter),
98127
$e->getMessage()
99128
)
@@ -102,4 +131,24 @@ public function convert(
102131
}
103132
}
104133
}
134+
135+
/**
136+
* Get batch size from environment variable or default
137+
*
138+
* @return int
139+
*/
140+
private function getBatchSize()
141+
{
142+
if (null !== $this->envBatchSize) {
143+
$batchSize = (int) $this->envBatchSize;
144+
if ($batchSize < self::MIN_BATCH_SIZE || $batchSize > self::MAX_BATCH_SIZE) {
145+
throw new \InvalidArgumentException(
146+
'Invalid value for environment variable ' . self::BATCH_SIZE_VARIABLE_NAME . '. '
147+
. 'Should be integer and be > ' . self::MIN_BATCH_SIZE . ', < ' . self::MAX_BATCH_SIZE
148+
);
149+
}
150+
return $batchSize;
151+
}
152+
return self::DEFAULT_BATCH_SIZE;
153+
}
105154
}

0 commit comments

Comments
 (0)