51
51
use Throwable ;
52
52
use UnexpectedValueException ;
53
53
54
+ use function array_chunk ;
54
55
use function array_combine ;
55
56
use function array_diff_key ;
56
57
use function array_filter ;
@@ -314,6 +315,9 @@ class UnitOfWork implements PropertyChangedListener
314
315
*/
315
316
private $ eagerLoadingEntities = [];
316
317
318
+ /** @var array<string, array<string, mixed>> */
319
+ private $ eagerLoadingCollections = [];
320
+
317
321
/** @var bool */
318
322
protected $ hasCache = false ;
319
323
@@ -2749,6 +2753,7 @@ public function clear($entityName = null)
2749
2753
$ this ->pendingCollectionElementRemovals =
2750
2754
$ this ->visitedCollections =
2751
2755
$ this ->eagerLoadingEntities =
2756
+ $ this ->eagerLoadingCollections =
2752
2757
$ this ->orphanRemovals = [];
2753
2758
} else {
2754
2759
Deprecation::triggerIfCalledFromOutside (
@@ -2938,6 +2943,10 @@ public function createEntity($className, array $data, &$hints = [])
2938
2943
continue ;
2939
2944
}
2940
2945
2946
+ if (! isset ($ hints ['fetchMode ' ][$ class ->name ][$ field ])) {
2947
+ $ hints ['fetchMode ' ][$ class ->name ][$ field ] = $ assoc ['fetch ' ];
2948
+ }
2949
+
2941
2950
$ targetClass = $ this ->em ->getClassMetadata ($ assoc ['targetEntity ' ]);
2942
2951
2943
2952
switch (true ) {
@@ -3001,10 +3010,6 @@ public function createEntity($className, array $data, &$hints = [])
3001
3010
break ;
3002
3011
}
3003
3012
3004
- if (! isset ($ hints ['fetchMode ' ][$ class ->name ][$ field ])) {
3005
- $ hints ['fetchMode ' ][$ class ->name ][$ field ] = $ assoc ['fetch ' ];
3006
- }
3007
-
3008
3013
// Foreign key is set
3009
3014
// Check identity map first
3010
3015
// FIXME: Can break easily with composite keys if join column values are in
@@ -3098,9 +3103,13 @@ public function createEntity($className, array $data, &$hints = [])
3098
3103
$ reflField = $ class ->reflFields [$ field ];
3099
3104
$ reflField ->setValue ($ entity , $ pColl );
3100
3105
3101
- if ($ assoc ['fetch ' ] === ClassMetadata::FETCH_EAGER ) {
3102
- $ this ->loadCollection ($ pColl );
3103
- $ pColl ->takeSnapshot ();
3106
+ if ($ hints ['fetchMode ' ][$ class ->name ][$ field ] === ClassMetadata::FETCH_EAGER ) {
3107
+ if ($ assoc ['type ' ] === ClassMetadata::ONE_TO_MANY ) {
3108
+ $ this ->scheduleCollectionForBatchLoading ($ pColl , $ class );
3109
+ } elseif ($ assoc ['type ' ] === ClassMetadata::MANY_TO_MANY ) {
3110
+ $ this ->loadCollection ($ pColl );
3111
+ $ pColl ->takeSnapshot ();
3112
+ }
3104
3113
}
3105
3114
3106
3115
$ this ->originalEntityData [$ oid ][$ field ] = $ pColl ;
@@ -3117,7 +3126,7 @@ public function createEntity($className, array $data, &$hints = [])
3117
3126
/** @return void */
3118
3127
public function triggerEagerLoads ()
3119
3128
{
3120
- if (! $ this ->eagerLoadingEntities ) {
3129
+ if (! $ this ->eagerLoadingEntities && ! $ this -> eagerLoadingCollections ) {
3121
3130
return ;
3122
3131
}
3123
3132
@@ -3130,11 +3139,69 @@ public function triggerEagerLoads()
3130
3139
continue ;
3131
3140
}
3132
3141
3133
- $ class = $ this ->em ->getClassMetadata ($ entityName );
3142
+ $ class = $ this ->em ->getClassMetadata ($ entityName );
3143
+ $ batches = array_chunk ($ ids , $ this ->em ->getConfiguration ()->getEagerFetchBatchSize ());
3134
3144
3135
- $ this ->getEntityPersister ($ entityName )->loadAll (
3136
- array_combine ($ class ->identifier , [array_values ($ ids )])
3137
- );
3145
+ foreach ($ batches as $ batchedIds ) {
3146
+ $ this ->getEntityPersister ($ entityName )->loadAll (
3147
+ array_combine ($ class ->identifier , [$ batchedIds ])
3148
+ );
3149
+ }
3150
+ }
3151
+
3152
+ $ eagerLoadingCollections = $ this ->eagerLoadingCollections ; // avoid recursion
3153
+ $ this ->eagerLoadingCollections = [];
3154
+
3155
+ foreach ($ eagerLoadingCollections as $ group ) {
3156
+ $ this ->eagerLoadCollections ($ group ['items ' ], $ group ['mapping ' ]);
3157
+ }
3158
+ }
3159
+
3160
+ /**
3161
+ * Load all data into the given collections, according to the specified mapping
3162
+ *
3163
+ * @param PersistentCollection[] $collections
3164
+ * @param array<string, mixed> $mapping
3165
+ * @psalm-param array{targetEntity: class-string, sourceEntity: class-string, mappedBy: string, indexBy: string|null} $mapping
3166
+ */
3167
+ private function eagerLoadCollections (array $ collections , array $ mapping ): void
3168
+ {
3169
+ $ targetEntity = $ mapping ['targetEntity ' ];
3170
+ $ class = $ this ->em ->getClassMetadata ($ mapping ['sourceEntity ' ]);
3171
+ $ mappedBy = $ mapping ['mappedBy ' ];
3172
+
3173
+ $ batches = array_chunk ($ collections , $ this ->em ->getConfiguration ()->getEagerFetchBatchSize (), true );
3174
+
3175
+ foreach ($ batches as $ collectionBatch ) {
3176
+ $ entities = [];
3177
+
3178
+ foreach ($ collectionBatch as $ collection ) {
3179
+ $ entities [] = $ collection ->getOwner ();
3180
+ }
3181
+
3182
+ $ found = $ this ->getEntityPersister ($ targetEntity )->loadAll ([$ mappedBy => $ entities ]);
3183
+
3184
+ $ targetClass = $ this ->em ->getClassMetadata ($ targetEntity );
3185
+ $ targetProperty = $ targetClass ->getReflectionProperty ($ mappedBy );
3186
+
3187
+ foreach ($ found as $ targetValue ) {
3188
+ $ sourceEntity = $ targetProperty ->getValue ($ targetValue );
3189
+
3190
+ $ id = $ this ->identifierFlattener ->flattenIdentifier ($ class , $ class ->getIdentifierValues ($ sourceEntity ));
3191
+ $ idHash = implode (' ' , $ id );
3192
+
3193
+ if (isset ($ mapping ['indexBy ' ])) {
3194
+ $ indexByProperty = $ targetClass ->getReflectionProperty ($ mapping ['indexBy ' ]);
3195
+ $ collectionBatch [$ idHash ]->hydrateSet ($ indexByProperty ->getValue ($ targetValue ), $ targetValue );
3196
+ } else {
3197
+ $ collectionBatch [$ idHash ]->add ($ targetValue );
3198
+ }
3199
+ }
3200
+ }
3201
+
3202
+ foreach ($ collections as $ association ) {
3203
+ $ association ->setInitialized (true );
3204
+ $ association ->takeSnapshot ();
3138
3205
}
3139
3206
}
3140
3207
@@ -3165,6 +3232,33 @@ public function loadCollection(PersistentCollection $collection)
3165
3232
$ collection ->setInitialized (true );
3166
3233
}
3167
3234
3235
+ /**
3236
+ * Schedule this collection for batch loading at the end of the UnitOfWork
3237
+ */
3238
+ private function scheduleCollectionForBatchLoading (PersistentCollection $ collection , ClassMetadata $ sourceClass ): void
3239
+ {
3240
+ $ mapping = $ collection ->getMapping ();
3241
+ $ name = $ mapping ['sourceEntity ' ] . '# ' . $ mapping ['fieldName ' ];
3242
+
3243
+ if (! isset ($ this ->eagerLoadingCollections [$ name ])) {
3244
+ $ this ->eagerLoadingCollections [$ name ] = [
3245
+ 'items ' => [],
3246
+ 'mapping ' => $ mapping ,
3247
+ ];
3248
+ }
3249
+
3250
+ $ owner = $ collection ->getOwner ();
3251
+ assert ($ owner !== null );
3252
+
3253
+ $ id = $ this ->identifierFlattener ->flattenIdentifier (
3254
+ $ sourceClass ,
3255
+ $ sourceClass ->getIdentifierValues ($ owner )
3256
+ );
3257
+ $ idHash = implode (' ' , $ id );
3258
+
3259
+ $ this ->eagerLoadingCollections [$ name ]['items ' ][$ idHash ] = $ collection ;
3260
+ }
3261
+
3168
3262
/**
3169
3263
* Gets the identity map of the UnitOfWork.
3170
3264
*
0 commit comments