Skip to content

Commit 29687f3

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-4153' into PR_2025_08_25_muntianu
2 parents 2c0cc6e + 8414c89 commit 29687f3

File tree

2 files changed

+301
-20
lines changed

2 files changed

+301
-20
lines changed

app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php

Lines changed: 129 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2011 Adobe
4+
* All Rights Reserved.
55
*/
66
namespace Magento\Sitemap\Model\ResourceModel\Catalog;
77

88
use Magento\Catalog\Model\Product\Image\UrlBuilder;
99
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
1010
use Magento\Framework\App\ObjectManager;
11+
use Magento\Framework\DataObject;
12+
use Magento\Framework\Exception\LocalizedException;
13+
use Magento\Sitemap\Model\SitemapConfigReaderInterface;
1114
use Magento\Store\Model\Store;
1215

1316
/**
@@ -21,6 +24,11 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
2124
{
2225
public const NOT_SELECTED_IMAGE = 'no_selection';
2326

27+
/**
28+
* Batch size for loading product images to avoid database IN() clause limits
29+
*/
30+
private const IMAGE_BATCH_SIZE = 1000;
31+
2432
/**
2533
* Collection Zend Db select
2634
*
@@ -35,6 +43,13 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
3543
*/
3644
protected $_attributesCache = [];
3745

46+
/**
47+
* Cached product images to avoid N+1 queries
48+
*
49+
* @var array
50+
*/
51+
private $_productImagesCache = [];
52+
3853
/**
3954
* @var \Magento\Catalog\Model\Product\Gallery\ReadHandler
4055
* @since 100.1.0
@@ -46,6 +61,11 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
4661
*/
4762
protected $_sitemapData = null;
4863

64+
/**
65+
* @var SitemapConfigReaderInterface
66+
*/
67+
private $sitemapConfigReader;
68+
4969
/**
5070
* @var \Magento\Catalog\Model\ResourceModel\Product
5171
*/
@@ -107,6 +127,7 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
107127
* @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig
108128
* @param UrlBuilder $urlBuilder
109129
* @param ProductSelectBuilder $productSelectBuilder
130+
* @param SitemapConfigReaderInterface $sitemapConfigReader
110131
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
111132
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
112133
*/
@@ -125,7 +146,8 @@ public function __construct(
125146
?\Magento\Catalog\Helper\Image $catalogImageHelper = null,
126147
?\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null,
127148
?UrlBuilder $urlBuilder = null,
128-
?ProductSelectBuilder $productSelectBuilder = null
149+
?ProductSelectBuilder $productSelectBuilder = null,
150+
?SitemapConfigReaderInterface $sitemapConfigReader = null
129151
) {
130152
$this->_productResource = $productResource;
131153
$this->_storeManager = $storeManager;
@@ -138,6 +160,8 @@ public function __construct(
138160
$this->imageUrlBuilder = $urlBuilder ?? ObjectManager::getInstance()->get(UrlBuilder::class);
139161
$this->productSelectBuilder = $productSelectBuilder ??
140162
ObjectManager::getInstance()->get(ProductSelectBuilder::class);
163+
$this->sitemapConfigReader = $sitemapConfigReader ??
164+
ObjectManager::getInstance()->get(SitemapConfigReaderInterface::class);
141165

142166
parent::__construct($context, $connectionName);
143167
}
@@ -307,7 +331,7 @@ public function getCollection($storeId)
307331
$this->_addFilter($store->getId(), 'status', $this->_productStatus->getVisibleStatusIds(), 'in');
308332

309333
// Join product images required attributes
310-
$imageIncludePolicy = $this->_sitemapData->getProductImageIncludePolicy($store->getId());
334+
$imageIncludePolicy = $this->sitemapConfigReader->getProductImageIncludePolicy($store->getId());
311335
if (\Magento\Sitemap\Model\Source\Product\Image\IncludeImage::INCLUDE_NONE != $imageIncludePolicy) {
312336
$this->_joinAttribute($store->getId(), 'name', 'name');
313337
if (\Magento\Sitemap\Model\Source\Product\Image\IncludeImage::INCLUDE_ALL == $imageIncludePolicy) {
@@ -318,7 +342,25 @@ public function getCollection($storeId)
318342
}
319343

320344
$query = $connection->query($this->prepareSelectStatement($this->_select));
345+
346+
// First, collect all product data without loading images
347+
$productRows = [];
348+
$productIds = [];
349+
$linkField = $this->_productResource->getLinkField();
350+
321351
while ($row = $query->fetch()) {
352+
$productRows[] = $row;
353+
$productIds[] = $row[$linkField];
354+
}
355+
356+
// Pre-load all images in batch to avoid N+1 queries
357+
$imageIncludePolicy = $this->sitemapConfigReader->getProductImageIncludePolicy($store->getId());
358+
if (\Magento\Sitemap\Model\Source\Product\Image\IncludeImage::INCLUDE_NONE != $imageIncludePolicy) {
359+
$this->_preloadAllProductImages($productIds, $store->getId());
360+
}
361+
362+
// Now create products with cached image data
363+
foreach ($productRows as $row) {
322364
$product = $this->_prepareProduct($row, $store->getId());
323365
$products[$product->getId()] = $product;
324366
}
@@ -332,12 +374,12 @@ public function getCollection($storeId)
332374
* @param array $productRow
333375
* @param int $storeId
334376
*
335-
* @return \Magento\Framework\DataObject
377+
* @return DataObject
336378
* @throws \Magento\Framework\Exception\LocalizedException
337379
*/
338380
protected function _prepareProduct(array $productRow, $storeId)
339381
{
340-
$product = new \Magento\Framework\DataObject();
382+
$product = new DataObject();
341383

342384
$product['id'] = $productRow[$this->getIdFieldName()];
343385
if (empty($productRow['url'])) {
@@ -352,16 +394,14 @@ protected function _prepareProduct(array $productRow, $storeId)
352394
/**
353395
* Load product images
354396
*
355-
* @param \Magento\Framework\DataObject $product
397+
* @param DataObject $product
356398
* @param int $storeId
357399
* @return void
358400
*/
359401
protected function _loadProductImages($product, $storeId)
360402
{
361403
$this->_storeManager->setCurrentStore($storeId);
362-
/** @var $helper \Magento\Sitemap\Helper\Data */
363-
$helper = $this->_sitemapData;
364-
$imageIncludePolicy = $helper->getProductImageIncludePolicy($storeId);
404+
$imageIncludePolicy = $this->sitemapConfigReader->getProductImageIncludePolicy($storeId);
365405

366406
// Get product images
367407
$imagesCollection = [];
@@ -372,7 +412,7 @@ protected function _loadProductImages($product, $storeId)
372412
$product->getImage() != self::NOT_SELECTED_IMAGE
373413
) {
374414
$imagesCollection = [
375-
new \Magento\Framework\DataObject(
415+
new DataObject(
376416
['url' => $this->getProductImageUrl($product->getImage())]
377417
),
378418
];
@@ -388,7 +428,7 @@ protected function _loadProductImages($product, $storeId)
388428
}
389429

390430
$product->setImages(
391-
new \Magento\Framework\DataObject(
431+
new DataObject(
392432
['collection' => $imagesCollection, 'title' => $product->getName(), 'thumbnail' => $thumbnail]
393433
)
394434
);
@@ -404,22 +444,39 @@ protected function _loadProductImages($product, $storeId)
404444
*/
405445
protected function _getAllProductImages($product, $storeId)
406446
{
407-
$product->setStoreId($storeId);
408-
$gallery = $this->mediaGalleryResourceModel->loadProductGalleryByAttributeId(
409-
$product,
410-
$this->mediaGalleryReadHandler->getAttribute()->getId()
411-
);
412-
447+
$linkField = $this->_productResource->getLinkField();
448+
$productRowId = $product->getData($linkField);
413449
$imagesCollection = [];
414-
if ($gallery) {
450+
451+
// Use cached images if available (from batch loading)
452+
if (isset($this->_productImagesCache[$productRowId])) {
453+
$gallery = $this->_productImagesCache[$productRowId];
415454
foreach ($gallery as $image) {
416-
$imagesCollection[] = new \Magento\Framework\DataObject(
455+
$imagesCollection[] = new DataObject(
417456
[
418457
'url' => $this->getProductImageUrl($image['file']),
419458
'caption' => $image['label'] ? $image['label'] : $image['label_default'],
420459
]
421460
);
422461
}
462+
} else {
463+
// Fallback to individual query
464+
$product->setStoreId($storeId);
465+
$gallery = $this->mediaGalleryResourceModel->loadProductGalleryByAttributeId(
466+
$product,
467+
$this->mediaGalleryReadHandler->getAttribute()->getId()
468+
);
469+
470+
if ($gallery) {
471+
foreach ($gallery as $image) {
472+
$imagesCollection[] = new DataObject(
473+
[
474+
'url' => $this->getProductImageUrl($image['file']),
475+
'caption' => $image['label'] ? $image['label'] : $image['label_default'],
476+
]
477+
);
478+
}
479+
}
423480
}
424481

425482
return $imagesCollection;
@@ -449,6 +506,58 @@ public function prepareSelectStatement(\Magento\Framework\DB\Select $select)
449506
return $select;
450507
}
451508

509+
/**
510+
* Pre-load all product images in batched queries to avoid N+1 problem while respecting DB limits
511+
*
512+
* @param array $productIds
513+
* @param int $storeId
514+
* @return void
515+
* @throws LocalizedException
516+
*/
517+
private function _preloadAllProductImages($productIds, $storeId)
518+
{
519+
if (empty($productIds)) {
520+
return;
521+
}
522+
523+
// Split into smaller batches to avoid hitting database IN() clause limits
524+
$productBatches = array_chunk($productIds, self::IMAGE_BATCH_SIZE);
525+
526+
$linkField = $this->_productResource->getLinkField();
527+
$connection = $this->getConnection();
528+
529+
foreach ($productBatches as $batch) {
530+
// Use the existing createBatchBaseSelect method for each batch
531+
$select = $this->mediaGalleryResourceModel->createBatchBaseSelect(
532+
$storeId,
533+
$this->mediaGalleryReadHandler->getAttribute()->getId()
534+
);
535+
536+
$select->where('entity.' . $linkField . ' IN (?)', $batch);
537+
538+
// Add ordering to ensure consistent results
539+
$select->order(['entity.' . $linkField, 'position']);
540+
541+
$result = $connection->fetchAll($select);
542+
543+
// Group images by product ID
544+
foreach ($result as $row) {
545+
$productId = $row[$linkField];
546+
if (!isset($this->_productImagesCache[$productId])) {
547+
$this->_productImagesCache[$productId] = [];
548+
}
549+
$this->_productImagesCache[$productId][] = $row;
550+
}
551+
}
552+
553+
// Ensure all requested products have an entry (even if empty)
554+
foreach ($productIds as $productId) {
555+
if (!isset($this->_productImagesCache[$productId])) {
556+
$this->_productImagesCache[$productId] = [];
557+
}
558+
}
559+
}
560+
452561
/**
453562
* Get product image URL from image filename
454563
*

0 commit comments

Comments
 (0)