1
1
<?php
2
2
/**
3
- * Copyright © Magento, Inc. All rights reserved.
4
- * See COPYING.txt for license details .
3
+ * Copyright 2011 Adobe
4
+ * All Rights Reserved .
5
5
*/
6
6
namespace Magento \Sitemap \Model \ResourceModel \Catalog ;
7
7
8
8
use Magento \Catalog \Model \Product \Image \UrlBuilder ;
9
9
use Magento \CatalogUrlRewrite \Model \ProductUrlRewriteGenerator ;
10
10
use Magento \Framework \App \ObjectManager ;
11
+ use Magento \Framework \DataObject ;
12
+ use Magento \Framework \Exception \LocalizedException ;
13
+ use Magento \Sitemap \Model \SitemapConfigReaderInterface ;
11
14
use Magento \Store \Model \Store ;
12
15
13
16
/**
@@ -21,6 +24,11 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
21
24
{
22
25
public const NOT_SELECTED_IMAGE = 'no_selection ' ;
23
26
27
+ /**
28
+ * Batch size for loading product images to avoid database IN() clause limits
29
+ */
30
+ private const IMAGE_BATCH_SIZE = 1000 ;
31
+
24
32
/**
25
33
* Collection Zend Db select
26
34
*
@@ -35,6 +43,13 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
35
43
*/
36
44
protected $ _attributesCache = [];
37
45
46
+ /**
47
+ * Cached product images to avoid N+1 queries
48
+ *
49
+ * @var array
50
+ */
51
+ private $ _productImagesCache = [];
52
+
38
53
/**
39
54
* @var \Magento\Catalog\Model\Product\Gallery\ReadHandler
40
55
* @since 100.1.0
@@ -46,6 +61,11 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
46
61
*/
47
62
protected $ _sitemapData = null ;
48
63
64
+ /**
65
+ * @var SitemapConfigReaderInterface
66
+ */
67
+ private $ sitemapConfigReader ;
68
+
49
69
/**
50
70
* @var \Magento\Catalog\Model\ResourceModel\Product
51
71
*/
@@ -107,6 +127,7 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
107
127
* @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig
108
128
* @param UrlBuilder $urlBuilder
109
129
* @param ProductSelectBuilder $productSelectBuilder
130
+ * @param SitemapConfigReaderInterface $sitemapConfigReader
110
131
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
111
132
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
112
133
*/
@@ -125,7 +146,8 @@ public function __construct(
125
146
?\Magento \Catalog \Helper \Image $ catalogImageHelper = null ,
126
147
?\Magento \Framework \App \Config \ScopeConfigInterface $ scopeConfig = null ,
127
148
?UrlBuilder $ urlBuilder = null ,
128
- ?ProductSelectBuilder $ productSelectBuilder = null
149
+ ?ProductSelectBuilder $ productSelectBuilder = null ,
150
+ ?SitemapConfigReaderInterface $ sitemapConfigReader = null
129
151
) {
130
152
$ this ->_productResource = $ productResource ;
131
153
$ this ->_storeManager = $ storeManager ;
@@ -138,6 +160,8 @@ public function __construct(
138
160
$ this ->imageUrlBuilder = $ urlBuilder ?? ObjectManager::getInstance ()->get (UrlBuilder::class);
139
161
$ this ->productSelectBuilder = $ productSelectBuilder ??
140
162
ObjectManager::getInstance ()->get (ProductSelectBuilder::class);
163
+ $ this ->sitemapConfigReader = $ sitemapConfigReader ??
164
+ ObjectManager::getInstance ()->get (SitemapConfigReaderInterface::class);
141
165
142
166
parent ::__construct ($ context , $ connectionName );
143
167
}
@@ -307,7 +331,7 @@ public function getCollection($storeId)
307
331
$ this ->_addFilter ($ store ->getId (), 'status ' , $ this ->_productStatus ->getVisibleStatusIds (), 'in ' );
308
332
309
333
// Join product images required attributes
310
- $ imageIncludePolicy = $ this ->_sitemapData ->getProductImageIncludePolicy ($ store ->getId ());
334
+ $ imageIncludePolicy = $ this ->sitemapConfigReader ->getProductImageIncludePolicy ($ store ->getId ());
311
335
if (\Magento \Sitemap \Model \Source \Product \Image \IncludeImage::INCLUDE_NONE != $ imageIncludePolicy ) {
312
336
$ this ->_joinAttribute ($ store ->getId (), 'name ' , 'name ' );
313
337
if (\Magento \Sitemap \Model \Source \Product \Image \IncludeImage::INCLUDE_ALL == $ imageIncludePolicy ) {
@@ -318,7 +342,25 @@ public function getCollection($storeId)
318
342
}
319
343
320
344
$ 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
+
321
351
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 ) {
322
364
$ product = $ this ->_prepareProduct ($ row , $ store ->getId ());
323
365
$ products [$ product ->getId ()] = $ product ;
324
366
}
@@ -332,12 +374,12 @@ public function getCollection($storeId)
332
374
* @param array $productRow
333
375
* @param int $storeId
334
376
*
335
- * @return \Magento\Framework\ DataObject
377
+ * @return DataObject
336
378
* @throws \Magento\Framework\Exception\LocalizedException
337
379
*/
338
380
protected function _prepareProduct (array $ productRow , $ storeId )
339
381
{
340
- $ product = new \ Magento \ Framework \ DataObject ();
382
+ $ product = new DataObject ();
341
383
342
384
$ product ['id ' ] = $ productRow [$ this ->getIdFieldName ()];
343
385
if (empty ($ productRow ['url ' ])) {
@@ -352,16 +394,14 @@ protected function _prepareProduct(array $productRow, $storeId)
352
394
/**
353
395
* Load product images
354
396
*
355
- * @param \Magento\Framework\ DataObject $product
397
+ * @param DataObject $product
356
398
* @param int $storeId
357
399
* @return void
358
400
*/
359
401
protected function _loadProductImages ($ product , $ storeId )
360
402
{
361
403
$ 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 );
365
405
366
406
// Get product images
367
407
$ imagesCollection = [];
@@ -372,7 +412,7 @@ protected function _loadProductImages($product, $storeId)
372
412
$ product ->getImage () != self ::NOT_SELECTED_IMAGE
373
413
) {
374
414
$ imagesCollection = [
375
- new \ Magento \ Framework \ DataObject (
415
+ new DataObject (
376
416
['url ' => $ this ->getProductImageUrl ($ product ->getImage ())]
377
417
),
378
418
];
@@ -388,7 +428,7 @@ protected function _loadProductImages($product, $storeId)
388
428
}
389
429
390
430
$ product ->setImages (
391
- new \ Magento \ Framework \ DataObject (
431
+ new DataObject (
392
432
['collection ' => $ imagesCollection , 'title ' => $ product ->getName (), 'thumbnail ' => $ thumbnail ]
393
433
)
394
434
);
@@ -404,22 +444,39 @@ protected function _loadProductImages($product, $storeId)
404
444
*/
405
445
protected function _getAllProductImages ($ product , $ storeId )
406
446
{
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 );
413
449
$ 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 ];
415
454
foreach ($ gallery as $ image ) {
416
- $ imagesCollection [] = new \ Magento \ Framework \ DataObject (
455
+ $ imagesCollection [] = new DataObject (
417
456
[
418
457
'url ' => $ this ->getProductImageUrl ($ image ['file ' ]),
419
458
'caption ' => $ image ['label ' ] ? $ image ['label ' ] : $ image ['label_default ' ],
420
459
]
421
460
);
422
461
}
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
+ }
423
480
}
424
481
425
482
return $ imagesCollection ;
@@ -449,6 +506,58 @@ public function prepareSelectStatement(\Magento\Framework\DB\Select $select)
449
506
return $ select ;
450
507
}
451
508
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
+
452
561
/**
453
562
* Get product image URL from image filename
454
563
*
0 commit comments