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 */
66namespace Magento \Sitemap \Model \ResourceModel \Catalog ;
77
88use Magento \Catalog \Model \Product \Image \UrlBuilder ;
99use Magento \CatalogUrlRewrite \Model \ProductUrlRewriteGenerator ;
1010use Magento \Framework \App \ObjectManager ;
11+ use Magento \Framework \DataObject ;
12+ use Magento \Framework \Exception \LocalizedException ;
13+ use Magento \Sitemap \Model \SitemapConfigReaderInterface ;
1114use 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,23 @@ 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 = [];
321349 while ($ row = $ query ->fetch ()) {
350+ $ productRows [] = $ row ;
351+ $ productIds [] = $ row [$ this ->getIdFieldName ()];
352+ }
353+
354+ // Pre-load all images in batch to avoid N+1 queries
355+ $ imageIncludePolicy = $ this ->sitemapConfigReader ->getProductImageIncludePolicy ($ store ->getId ());
356+ if (\Magento \Sitemap \Model \Source \Product \Image \IncludeImage::INCLUDE_NONE != $ imageIncludePolicy ) {
357+ $ this ->_preloadAllProductImages ($ productIds , $ store ->getId ());
358+ }
359+
360+ // Now create products with cached image data
361+ foreach ($ productRows as $ row ) {
322362 $ product = $ this ->_prepareProduct ($ row , $ store ->getId ());
323363 $ products [$ product ->getId ()] = $ product ;
324364 }
@@ -332,12 +372,12 @@ public function getCollection($storeId)
332372 * @param array $productRow
333373 * @param int $storeId
334374 *
335- * @return \Magento\Framework\ DataObject
375+ * @return DataObject
336376 * @throws \Magento\Framework\Exception\LocalizedException
337377 */
338378 protected function _prepareProduct (array $ productRow , $ storeId )
339379 {
340- $ product = new \ Magento \ Framework \ DataObject ();
380+ $ product = new DataObject ();
341381
342382 $ product ['id ' ] = $ productRow [$ this ->getIdFieldName ()];
343383 if (empty ($ productRow ['url ' ])) {
@@ -352,16 +392,14 @@ protected function _prepareProduct(array $productRow, $storeId)
352392 /**
353393 * Load product images
354394 *
355- * @param \Magento\Framework\ DataObject $product
395+ * @param DataObject $product
356396 * @param int $storeId
357397 * @return void
358398 */
359399 protected function _loadProductImages ($ product , $ storeId )
360400 {
361401 $ this ->_storeManager ->setCurrentStore ($ storeId );
362- /** @var $helper \Magento\Sitemap\Helper\Data */
363- $ helper = $ this ->_sitemapData ;
364- $ imageIncludePolicy = $ helper ->getProductImageIncludePolicy ($ storeId );
402+ $ imageIncludePolicy = $ this ->sitemapConfigReader ->getProductImageIncludePolicy ($ storeId );
365403
366404 // Get product images
367405 $ imagesCollection = [];
@@ -372,7 +410,7 @@ protected function _loadProductImages($product, $storeId)
372410 $ product ->getImage () != self ::NOT_SELECTED_IMAGE
373411 ) {
374412 $ imagesCollection = [
375- new \ Magento \ Framework \ DataObject (
413+ new DataObject (
376414 ['url ' => $ this ->getProductImageUrl ($ product ->getImage ())]
377415 ),
378416 ];
@@ -388,7 +426,7 @@ protected function _loadProductImages($product, $storeId)
388426 }
389427
390428 $ product ->setImages (
391- new \ Magento \ Framework \ DataObject (
429+ new DataObject (
392430 ['collection ' => $ imagesCollection , 'title ' => $ product ->getName (), 'thumbnail ' => $ thumbnail ]
393431 )
394432 );
@@ -404,22 +442,38 @@ protected function _loadProductImages($product, $storeId)
404442 */
405443 protected function _getAllProductImages ($ product , $ storeId )
406444 {
407- $ product ->setStoreId ($ storeId );
408- $ gallery = $ this ->mediaGalleryResourceModel ->loadProductGalleryByAttributeId (
409- $ product ,
410- $ this ->mediaGalleryReadHandler ->getAttribute ()->getId ()
411- );
412-
445+ $ productId = $ product ->getId ();
413446 $ imagesCollection = [];
414- if ($ gallery ) {
447+
448+ // Use cached images if available (from batch loading)
449+ if (isset ($ this ->_productImagesCache [$ productId ])) {
450+ $ gallery = $ this ->_productImagesCache [$ productId ];
415451 foreach ($ gallery as $ image ) {
416- $ imagesCollection [] = new \ Magento \ Framework \ DataObject (
452+ $ imagesCollection [] = new DataObject (
417453 [
418454 'url ' => $ this ->getProductImageUrl ($ image ['file ' ]),
419455 'caption ' => $ image ['label ' ] ? $ image ['label ' ] : $ image ['label_default ' ],
420456 ]
421457 );
422458 }
459+ } else {
460+ // Fallback to individual query (should rarely happen now)
461+ $ product ->setStoreId ($ storeId );
462+ $ gallery = $ this ->mediaGalleryResourceModel ->loadProductGalleryByAttributeId (
463+ $ product ,
464+ $ this ->mediaGalleryReadHandler ->getAttribute ()->getId ()
465+ );
466+
467+ if ($ gallery ) {
468+ foreach ($ gallery as $ image ) {
469+ $ imagesCollection [] = new DataObject (
470+ [
471+ 'url ' => $ this ->getProductImageUrl ($ image ['file ' ]),
472+ 'caption ' => $ image ['label ' ] ? $ image ['label ' ] : $ image ['label_default ' ],
473+ ]
474+ );
475+ }
476+ }
423477 }
424478
425479 return $ imagesCollection ;
@@ -449,6 +503,58 @@ public function prepareSelectStatement(\Magento\Framework\DB\Select $select)
449503 return $ select ;
450504 }
451505
506+ /**
507+ * Pre-load all product images in batched queries to avoid N+1 problem while respecting DB limits
508+ *
509+ * @param array $productIds
510+ * @param int $storeId
511+ * @return void
512+ * @throws LocalizedException
513+ */
514+ private function _preloadAllProductImages ($ productIds , $ storeId )
515+ {
516+ if (empty ($ productIds )) {
517+ return ;
518+ }
519+
520+ // Split into smaller batches to avoid hitting database IN() clause limits
521+ $ productBatches = array_chunk ($ productIds , self ::IMAGE_BATCH_SIZE );
522+
523+ $ linkField = $ this ->_productResource ->getLinkField ();
524+ $ connection = $ this ->getConnection ();
525+
526+ foreach ($ productBatches as $ batch ) {
527+ // Use the existing createBatchBaseSelect method for each batch
528+ $ select = $ this ->mediaGalleryResourceModel ->createBatchBaseSelect (
529+ $ storeId ,
530+ $ this ->mediaGalleryReadHandler ->getAttribute ()->getId ()
531+ );
532+
533+ $ select ->where ('entity. ' . $ linkField . ' IN (?) ' , $ batch );
534+
535+ // Add ordering to ensure consistent results
536+ $ select ->order (['entity. ' . $ linkField , 'position ' ]);
537+
538+ $ result = $ connection ->fetchAll ($ select );
539+
540+ // Group images by product ID
541+ foreach ($ result as $ row ) {
542+ $ productId = $ row [$ linkField ];
543+ if (!isset ($ this ->_productImagesCache [$ productId ])) {
544+ $ this ->_productImagesCache [$ productId ] = [];
545+ }
546+ $ this ->_productImagesCache [$ productId ][] = $ row ;
547+ }
548+ }
549+
550+ // Ensure all requested products have an entry (even if empty)
551+ foreach ($ productIds as $ productId ) {
552+ if (!isset ($ this ->_productImagesCache [$ productId ])) {
553+ $ this ->_productImagesCache [$ productId ] = [];
554+ }
555+ }
556+ }
557+
452558 /**
453559 * Get product image URL from image filename
454560 *
0 commit comments