33
33
use Doctrine \ORM \Mapping \AssociationMapping ;
34
34
use Doctrine \ORM \Mapping \ClassMetadata ;
35
35
use Doctrine \ORM \Mapping \MappingException ;
36
+ use Doctrine \ORM \Mapping \ToManyInverseSideMapping ;
36
37
use Doctrine \ORM \Persisters \Collection \CollectionPersister ;
37
38
use Doctrine \ORM \Persisters \Collection \ManyToManyPersister ;
38
39
use Doctrine \ORM \Persisters \Collection \OneToManyPersister ;
50
51
use Throwable ;
51
52
use UnexpectedValueException ;
52
53
54
+ use function array_chunk ;
53
55
use function array_combine ;
54
56
use function array_diff_key ;
55
57
use function array_filter ;
@@ -292,6 +294,9 @@ class UnitOfWork implements PropertyChangedListener
292
294
*/
293
295
private array $ eagerLoadingEntities = [];
294
296
297
+ /** @var array<string, array<string, mixed>> */
298
+ private array $ eagerLoadingCollections = [];
299
+
295
300
protected bool $ hasCache = false ;
296
301
297
302
/**
@@ -2218,6 +2223,7 @@ public function clear(): void
2218
2223
$ this ->pendingCollectionElementRemovals =
2219
2224
$ this ->visitedCollections =
2220
2225
$ this ->eagerLoadingEntities =
2226
+ $ this ->eagerLoadingCollections =
2221
2227
$ this ->orphanRemovals = [];
2222
2228
2223
2229
if ($ this ->evm ->hasListeners (Events::onClear)) {
@@ -2352,6 +2358,10 @@ public function createEntity(string $className, array $data, array &$hints = [])
2352
2358
continue ;
2353
2359
}
2354
2360
2361
+ if (! isset ($ hints ['fetchMode ' ][$ class ->name ][$ field ])) {
2362
+ $ hints ['fetchMode ' ][$ class ->name ][$ field ] = $ assoc ->fetch ;
2363
+ }
2364
+
2355
2365
$ targetClass = $ this ->em ->getClassMetadata ($ assoc ->targetEntity );
2356
2366
2357
2367
switch (true ) {
@@ -2416,10 +2426,6 @@ public function createEntity(string $className, array $data, array &$hints = [])
2416
2426
break ;
2417
2427
}
2418
2428
2419
- if (! isset ($ hints ['fetchMode ' ][$ class ->name ][$ field ])) {
2420
- $ hints ['fetchMode ' ][$ class ->name ][$ field ] = $ assoc ->fetch ;
2421
- }
2422
-
2423
2429
// Foreign key is set
2424
2430
// Check identity map first
2425
2431
// FIXME: Can break easily with composite keys if join column values are in
@@ -2514,9 +2520,13 @@ public function createEntity(string $className, array $data, array &$hints = [])
2514
2520
$ reflField = $ class ->reflFields [$ field ];
2515
2521
$ reflField ->setValue ($ entity , $ pColl );
2516
2522
2517
- if ($ assoc ->fetch === ClassMetadata::FETCH_EAGER ) {
2518
- $ this ->loadCollection ($ pColl );
2519
- $ pColl ->takeSnapshot ();
2523
+ if ($ hints ['fetchMode ' ][$ class ->name ][$ field ] === ClassMetadata::FETCH_EAGER ) {
2524
+ if ($ assoc ->isOneToMany ()) {
2525
+ $ this ->scheduleCollectionForBatchLoading ($ pColl , $ class );
2526
+ } elseif ($ assoc ->isManyToMany ()) {
2527
+ $ this ->loadCollection ($ pColl );
2528
+ $ pColl ->takeSnapshot ();
2529
+ }
2520
2530
}
2521
2531
2522
2532
$ this ->originalEntityData [$ oid ][$ field ] = $ pColl ;
@@ -2532,7 +2542,7 @@ public function createEntity(string $className, array $data, array &$hints = [])
2532
2542
2533
2543
public function triggerEagerLoads (): void
2534
2544
{
2535
- if (! $ this ->eagerLoadingEntities ) {
2545
+ if (! $ this ->eagerLoadingEntities && ! $ this -> eagerLoadingCollections ) {
2536
2546
return ;
2537
2547
}
2538
2548
@@ -2545,11 +2555,69 @@ public function triggerEagerLoads(): void
2545
2555
continue ;
2546
2556
}
2547
2557
2548
- $ class = $ this ->em ->getClassMetadata ($ entityName );
2558
+ $ class = $ this ->em ->getClassMetadata ($ entityName );
2559
+ $ batches = array_chunk ($ ids , $ this ->em ->getConfiguration ()->getEagerFetchBatchSize ());
2549
2560
2550
- $ this ->getEntityPersister ($ entityName )->loadAll (
2551
- array_combine ($ class ->identifier , [array_values ($ ids )]),
2552
- );
2561
+ foreach ($ batches as $ batchedIds ) {
2562
+ $ this ->getEntityPersister ($ entityName )->loadAll (
2563
+ array_combine ($ class ->identifier , [$ batchedIds ]),
2564
+ );
2565
+ }
2566
+ }
2567
+
2568
+ $ eagerLoadingCollections = $ this ->eagerLoadingCollections ; // avoid recursion
2569
+ $ this ->eagerLoadingCollections = [];
2570
+
2571
+ foreach ($ eagerLoadingCollections as $ group ) {
2572
+ $ this ->eagerLoadCollections ($ group ['items ' ], $ group ['mapping ' ]);
2573
+ }
2574
+ }
2575
+
2576
+ /**
2577
+ * Load all data into the given collections, according to the specified mapping
2578
+ *
2579
+ * @param PersistentCollection[] $collections
2580
+ */
2581
+ private function eagerLoadCollections (array $ collections , ToManyInverseSideMapping $ mapping ): void
2582
+ {
2583
+ $ targetEntity = $ mapping ->targetEntity ;
2584
+ $ class = $ this ->em ->getClassMetadata ($ mapping ->sourceEntity );
2585
+ $ mappedBy = $ mapping ->mappedBy ;
2586
+
2587
+ $ batches = array_chunk ($ collections , $ this ->em ->getConfiguration ()->getEagerFetchBatchSize (), true );
2588
+
2589
+ foreach ($ batches as $ collectionBatch ) {
2590
+ $ entities = [];
2591
+
2592
+ foreach ($ collectionBatch as $ collection ) {
2593
+ $ entities [] = $ collection ->getOwner ();
2594
+ }
2595
+
2596
+ $ found = $ this ->getEntityPersister ($ targetEntity )->loadAll ([$ mappedBy => $ entities ]);
2597
+
2598
+ $ targetClass = $ this ->em ->getClassMetadata ($ targetEntity );
2599
+ $ targetProperty = $ targetClass ->getReflectionProperty ($ mappedBy );
2600
+ assert ($ targetProperty !== null );
2601
+
2602
+ foreach ($ found as $ targetValue ) {
2603
+ $ sourceEntity = $ targetProperty ->getValue ($ targetValue );
2604
+
2605
+ $ id = $ this ->identifierFlattener ->flattenIdentifier ($ class , $ class ->getIdentifierValues ($ sourceEntity ));
2606
+ $ idHash = implode (' ' , $ id );
2607
+
2608
+ if ($ mapping ->indexBy !== null ) {
2609
+ $ indexByProperty = $ targetClass ->getReflectionProperty ($ mapping ->indexBy );
2610
+ assert ($ indexByProperty !== null );
2611
+ $ collectionBatch [$ idHash ]->hydrateSet ($ indexByProperty ->getValue ($ targetValue ), $ targetValue );
2612
+ } else {
2613
+ $ collectionBatch [$ idHash ]->add ($ targetValue );
2614
+ }
2615
+ }
2616
+ }
2617
+
2618
+ foreach ($ collections as $ association ) {
2619
+ $ association ->setInitialized (true );
2620
+ $ association ->takeSnapshot ();
2553
2621
}
2554
2622
}
2555
2623
@@ -2576,6 +2644,33 @@ public function loadCollection(PersistentCollection $collection): void
2576
2644
$ collection ->setInitialized (true );
2577
2645
}
2578
2646
2647
+ /**
2648
+ * Schedule this collection for batch loading at the end of the UnitOfWork
2649
+ */
2650
+ private function scheduleCollectionForBatchLoading (PersistentCollection $ collection , ClassMetadata $ sourceClass ): void
2651
+ {
2652
+ $ mapping = $ collection ->getMapping ();
2653
+ $ name = $ mapping ['sourceEntity ' ] . '# ' . $ mapping ['fieldName ' ];
2654
+
2655
+ if (! isset ($ this ->eagerLoadingCollections [$ name ])) {
2656
+ $ this ->eagerLoadingCollections [$ name ] = [
2657
+ 'items ' => [],
2658
+ 'mapping ' => $ mapping ,
2659
+ ];
2660
+ }
2661
+
2662
+ $ owner = $ collection ->getOwner ();
2663
+ assert ($ owner !== null );
2664
+
2665
+ $ id = $ this ->identifierFlattener ->flattenIdentifier (
2666
+ $ sourceClass ,
2667
+ $ sourceClass ->getIdentifierValues ($ owner ),
2668
+ );
2669
+ $ idHash = implode (' ' , $ id );
2670
+
2671
+ $ this ->eagerLoadingCollections [$ name ]['items ' ][$ idHash ] = $ collection ;
2672
+ }
2673
+
2579
2674
/**
2580
2675
* Gets the identity map of the UnitOfWork.
2581
2676
*
0 commit comments