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,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