Skip to content

Commit c90b08c

Browse files
gliljashazzik
authored andcommitted
Improved collection batch fetching (#1558)
1 parent 4e84dbc commit c90b08c

12 files changed

+179
-47
lines changed

src/NHibernate/Async/Engine/BatchFetchQueue.cs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using NHibernate.Persister.Entity;
1616
using NHibernate.Util;
1717
using System.Collections.Generic;
18+
using Iesi.Collections.Generic;
1819

1920
namespace NHibernate.Engine
2021
{
@@ -40,22 +41,33 @@ public async Task<object[]> GetCollectionBatchAsync(ICollectionPersister collect
4041
int end = -1;
4142
bool checkForEnd = false;
4243

43-
// this only works because collection entries are kept in a sequenced
44-
// map by persistence context (maybe we should do like entities and
45-
// keep a separate sequences set...)
46-
foreach (DictionaryEntry me in context.CollectionEntries)
44+
if (batchLoadableCollections.TryGetValue(collectionPersister.Role, out var map))
4745
{
48-
CollectionEntry ce = (CollectionEntry) me.Value;
49-
IPersistentCollection collection = (IPersistentCollection) me.Key;
50-
if (!collection.WasInitialized && ce.LoadedPersister == collectionPersister)
46+
foreach (KeyValuePair<CollectionEntry, IPersistentCollection> me in map)
5147
{
48+
var ce = me.Key;
49+
var collection = me.Value;
50+
if (ce.LoadedKey == null)
51+
{
52+
// the LoadedKey of the CollectionEntry might be null as it might have been reset to null
53+
// (see for example Collections.ProcessDereferencedCollection()
54+
// and CollectionEntry.AfterAction())
55+
// though we clear the queue on flush, it seems like a good idea to guard
56+
// against potentially null LoadedKey:s
57+
continue;
58+
}
59+
60+
if (collection.WasInitialized)
61+
{
62+
log.Warn("Encountered initialized collection in BatchFetchQueue, this should not happen.");
63+
continue;
64+
}
65+
5266
if (checkForEnd && i == end)
5367
{
5468
return keys; //the first key found after the given key
5569
}
5670

57-
//if ( end == -1 && count > batchSize*10 ) return keys; //try out ten batches, max
58-
5971
bool isEqual = collectionPersister.KeyType.IsEqual(id, ce.LoadedKey, collectionPersister.Factory);
6072

6173
if (isEqual)
@@ -79,6 +91,7 @@ public async Task<object[]> GetCollectionBatchAsync(ICollectionPersister collect
7991
}
8092
}
8193
}
94+
8295
return keys; //we ran out of keys to try
8396
}
8497

@@ -92,7 +105,7 @@ public async Task<object[]> GetCollectionBatchAsync(ICollectionPersister collect
92105
/// <param name="batchSize">The maximum number of keys to return</param>
93106
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
94107
/// <returns>an array of identifiers, of length batchSize (possibly padded with nulls)</returns>
95-
public async Task<object[]> GetEntityBatchAsync(IEntityPersister persister,object id,int batchSize, CancellationToken cancellationToken)
108+
public async Task<object[]> GetEntityBatchAsync(IEntityPersister persister, object id, int batchSize, CancellationToken cancellationToken)
96109
{
97110
cancellationToken.ThrowIfCancellationRequested();
98111
object[] ids = new object[batchSize];
@@ -101,9 +114,9 @@ public async Task<object[]> GetEntityBatchAsync(IEntityPersister persister,objec
101114
int end = -1;
102115
bool checkForEnd = false;
103116

104-
foreach (EntityKey key in batchLoadableEntityKeys.Keys)
117+
if (batchLoadableEntityKeys.TryGetValue(persister.EntityName, out var set))
105118
{
106-
if (key.EntityName.Equals(persister.EntityName))
119+
foreach (var key in set)
107120
{
108121
//TODO: this needn't exclude subclasses...
109122
if (checkForEnd && i == end)

src/NHibernate/Async/Engine/Loading/CollectionLoadContext.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ private async Task EndLoadingCollectionAsync(LoadingCollectionEntry lce, ICollec
128128
{
129129
log.Debug("ending loading collection [{0}]", lce);
130130
}
131-
ISessionImplementor session = LoadContext.PersistenceContext.Session;
131+
132+
var persistenceContext = LoadContext.PersistenceContext;
133+
var session = persistenceContext.Session;
132134

133135
bool statsEnabled = session.Factory.Statistics.IsStatisticsEnabled;
134136
var stopWath = new Stopwatch();
@@ -141,17 +143,17 @@ private async Task EndLoadingCollectionAsync(LoadingCollectionEntry lce, ICollec
141143

142144
if (persister.CollectionType.HasHolder())
143145
{
144-
LoadContext.PersistenceContext.AddCollectionHolder(lce.Collection);
146+
persistenceContext.AddCollectionHolder(lce.Collection);
145147
}
146148

147-
CollectionEntry ce = LoadContext.PersistenceContext.GetCollectionEntry(lce.Collection);
149+
CollectionEntry ce = persistenceContext.GetCollectionEntry(lce.Collection);
148150
if (ce == null)
149151
{
150-
ce = LoadContext.PersistenceContext.AddInitializedCollection(persister, lce.Collection, lce.Key);
152+
ce = persistenceContext.AddInitializedCollection(persister, lce.Collection, lce.Key);
151153
}
152154
else
153155
{
154-
ce.PostInitialize(lce.Collection);
156+
ce.PostInitialize(lce.Collection, persistenceContext);
155157
}
156158

157159
bool addToCache = hasNoQueuedAdds && persister.HasCache &&

src/NHibernate/Async/Event/Default/DefaultInitializeCollectionEventListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private async Task<bool> InitializeCollectionFromCacheAsync(object id, ICollecti
127127
CollectionCacheEntry cacheEntry = (CollectionCacheEntry)persister.CacheEntryStructure.Destructure(ce, factory);
128128
await (cacheEntry.AssembleAsync(collection, persister, persistenceContext.GetCollectionOwner(id, persister), cancellationToken)).ConfigureAwait(false);
129129

130-
persistenceContext.GetCollectionEntry(collection).PostInitialize(collection);
130+
persistenceContext.GetCollectionEntry(collection).PostInitialize(collection, persistenceContext);
131131
return true;
132132
}
133133
}

src/NHibernate/Engine/BatchFetchQueue.cs

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@
55
using NHibernate.Persister.Entity;
66
using NHibernate.Util;
77
using System.Collections.Generic;
8+
using Iesi.Collections.Generic;
89

910
namespace NHibernate.Engine
1011
{
1112
public partial class BatchFetchQueue
1213
{
13-
private static readonly object Marker = new object();
14+
private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(BatchFetchQueue));
1415

1516
/// <summary>
16-
/// Defines a sequence of <see cref="EntityKey" /> elements that are currently
17-
/// eligible for batch fetching.
17+
/// Used to hold information about the entities that are currently eligible for batch-fetching. Ultimately
18+
/// used by <see cref="GetEntityBatch" /> to build entity load batches.
1819
/// </summary>
1920
/// <remarks>
20-
/// Even though this is a map, we only use the keys. A map was chosen in
21-
/// order to utilize a <see cref="LinkedHashMap{K, V}" /> to maintain sequencing
22-
/// as well as uniqueness.
21+
/// A Map structure is used to segment the keys by entity type since loading can only be done for a particular entity
22+
/// type at a time.
2323
/// </remarks>
24-
private readonly IDictionary<EntityKey, object> batchLoadableEntityKeys = new LinkedHashMap<EntityKey, object>(8);
24+
private readonly IDictionary<string, LinkedHashSet<EntityKey>> batchLoadableEntityKeys = new Dictionary<string, LinkedHashSet<EntityKey>>(8);
2525

2626
/// <summary>
2727
/// A map of <see cref="SubselectFetch">subselect-fetch descriptors</see>
@@ -30,6 +30,7 @@ public partial class BatchFetchQueue
3030
/// </summary>
3131
private readonly IDictionary<EntityKey, SubselectFetch> subselectsByEntityKey = new Dictionary<EntityKey, SubselectFetch>(8);
3232

33+
private readonly IDictionary<string, LinkedHashMap<CollectionEntry, IPersistentCollection>> batchLoadableCollections = new Dictionary<string, LinkedHashMap<CollectionEntry, IPersistentCollection>>(8);
3334
/// <summary>
3435
/// The owning persistence context.
3536
/// </summary>
@@ -50,6 +51,7 @@ public BatchFetchQueue(IPersistenceContext context)
5051
public void Clear()
5152
{
5253
batchLoadableEntityKeys.Clear();
54+
batchLoadableCollections.Clear();
5355
subselectsByEntityKey.Clear();
5456
}
5557

@@ -113,7 +115,12 @@ public void AddBatchLoadableEntityKey(EntityKey key)
113115
{
114116
if (key.IsBatchLoadable)
115117
{
116-
batchLoadableEntityKeys[key] = Marker;
118+
if (!batchLoadableEntityKeys.TryGetValue(key.EntityName, out var set))
119+
{
120+
set = new LinkedHashSet<EntityKey>();
121+
batchLoadableEntityKeys.Add(key.EntityName, set);
122+
}
123+
set.Add(key);
117124
}
118125
}
119126

@@ -125,7 +132,44 @@ public void AddBatchLoadableEntityKey(EntityKey key)
125132
public void RemoveBatchLoadableEntityKey(EntityKey key)
126133
{
127134
if (key.IsBatchLoadable)
128-
batchLoadableEntityKeys.Remove(key);
135+
{
136+
if (batchLoadableEntityKeys.TryGetValue(key.EntityName, out var set))
137+
{
138+
set.Remove(key);
139+
}
140+
}
141+
}
142+
143+
/// <summary>
144+
/// If a CollectionEntry represents a batch loadable collection, add
145+
/// it to the queue.
146+
/// </summary>
147+
/// <param name="collection"></param>
148+
/// <param name="ce"></param>
149+
public void AddBatchLoadableCollection(IPersistentCollection collection, CollectionEntry ce)
150+
{
151+
var persister = ce.LoadedPersister;
152+
153+
if (!batchLoadableCollections.TryGetValue(persister.Role, out var map))
154+
{
155+
map = new LinkedHashMap<CollectionEntry, IPersistentCollection>();
156+
batchLoadableCollections.Add(persister.Role, map);
157+
}
158+
map[ce] = collection;
159+
}
160+
161+
/// <summary>
162+
/// After a collection was initialized or evicted, we don't
163+
/// need to batch fetch it anymore, remove it from the queue
164+
/// if necessary
165+
/// </summary>
166+
/// <param name="ce"></param>
167+
public void RemoveBatchLoadableCollection(CollectionEntry ce)
168+
{
169+
if (batchLoadableCollections.TryGetValue(ce.LoadedPersister.Role, out var map))
170+
{
171+
map.Remove(ce);
172+
}
129173
}
130174

131175
/// <summary>
@@ -143,22 +187,33 @@ public object[] GetCollectionBatch(ICollectionPersister collectionPersister, obj
143187
int end = -1;
144188
bool checkForEnd = false;
145189

146-
// this only works because collection entries are kept in a sequenced
147-
// map by persistence context (maybe we should do like entities and
148-
// keep a separate sequences set...)
149-
foreach (DictionaryEntry me in context.CollectionEntries)
190+
if (batchLoadableCollections.TryGetValue(collectionPersister.Role, out var map))
150191
{
151-
CollectionEntry ce = (CollectionEntry) me.Value;
152-
IPersistentCollection collection = (IPersistentCollection) me.Key;
153-
if (!collection.WasInitialized && ce.LoadedPersister == collectionPersister)
192+
foreach (KeyValuePair<CollectionEntry, IPersistentCollection> me in map)
154193
{
194+
var ce = me.Key;
195+
var collection = me.Value;
196+
if (ce.LoadedKey == null)
197+
{
198+
// the LoadedKey of the CollectionEntry might be null as it might have been reset to null
199+
// (see for example Collections.ProcessDereferencedCollection()
200+
// and CollectionEntry.AfterAction())
201+
// though we clear the queue on flush, it seems like a good idea to guard
202+
// against potentially null LoadedKey:s
203+
continue;
204+
}
205+
206+
if (collection.WasInitialized)
207+
{
208+
log.Warn("Encountered initialized collection in BatchFetchQueue, this should not happen.");
209+
continue;
210+
}
211+
155212
if (checkForEnd && i == end)
156213
{
157214
return keys; //the first key found after the given key
158215
}
159216

160-
//if ( end == -1 && count > batchSize*10 ) return keys; //try out ten batches, max
161-
162217
bool isEqual = collectionPersister.KeyType.IsEqual(id, ce.LoadedKey, collectionPersister.Factory);
163218

164219
if (isEqual)
@@ -182,6 +237,7 @@ public object[] GetCollectionBatch(ICollectionPersister collectionPersister, obj
182237
}
183238
}
184239
}
240+
185241
return keys; //we ran out of keys to try
186242
}
187243

@@ -194,17 +250,17 @@ public object[] GetCollectionBatch(ICollectionPersister collectionPersister, obj
194250
/// <param name="id">The identifier of the entity currently demanding load.</param>
195251
/// <param name="batchSize">The maximum number of keys to return</param>
196252
/// <returns>an array of identifiers, of length batchSize (possibly padded with nulls)</returns>
197-
public object[] GetEntityBatch(IEntityPersister persister,object id,int batchSize)
253+
public object[] GetEntityBatch(IEntityPersister persister, object id, int batchSize)
198254
{
199255
object[] ids = new object[batchSize];
200256
ids[0] = id; //first element of array is reserved for the actual instance we are loading!
201257
int i = 1;
202258
int end = -1;
203259
bool checkForEnd = false;
204260

205-
foreach (EntityKey key in batchLoadableEntityKeys.Keys)
261+
if (batchLoadableEntityKeys.TryGetValue(persister.EntityName, out var set))
206262
{
207-
if (key.EntityName.Equals(persister.EntityName))
263+
foreach (var key in set)
208264
{
209265
//TODO: this needn't exclude subclasses...
210266
if (checkForEnd && i == end)

src/NHibernate/Engine/CollectionEntry.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,32 @@ public void PreFlush(IPersistentCollection collection)
298298
/// has been initialized.
299299
/// </summary>
300300
/// <param name="collection">The initialized <see cref="AbstractPersistentCollection"/> that this Entry is for.</param>
301+
//Since v5.1
302+
[Obsolete("Please use PostInitialize(collection, persistenceContext) instead.")]
301303
public void PostInitialize(IPersistentCollection collection)
302304
{
303305
snapshot = LoadedPersister.IsMutable ? collection.GetSnapshot(LoadedPersister) : null;
304306
collection.SetSnapshot(loadedKey, role, snapshot);
305307
}
306308

309+
/// <summary>
310+
/// Updates the CollectionEntry to reflect that the <see cref="IPersistentCollection"/>
311+
/// has been initialized.
312+
/// </summary>
313+
/// <param name="collection">The initialized <see cref="AbstractPersistentCollection"/> that this Entry is for.</param>
314+
/// <param name="persistenceContext"></param>
315+
public void PostInitialize(IPersistentCollection collection, IPersistenceContext persistenceContext)
316+
{
317+
#pragma warning disable 618
318+
//6.0 TODO: Inline PostInitialize here.
319+
PostInitialize(collection);
320+
#pragma warning restore 618
321+
if (LoadedPersister.GetBatchSize() > 1)
322+
{
323+
persistenceContext.BatchFetchQueue.RemoveBatchLoadableCollection(this);
324+
}
325+
}
326+
307327
/// <summary>
308328
/// Updates the CollectionEntry to reflect that it is has been successfully flushed to the database.
309329
/// </summary>

src/NHibernate/Engine/Loading/CollectionLoadContext.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ private void EndLoadingCollection(LoadingCollectionEntry lce, ICollectionPersist
234234
{
235235
log.Debug("ending loading collection [{0}]", lce);
236236
}
237-
ISessionImplementor session = LoadContext.PersistenceContext.Session;
237+
238+
var persistenceContext = LoadContext.PersistenceContext;
239+
var session = persistenceContext.Session;
238240

239241
bool statsEnabled = session.Factory.Statistics.IsStatisticsEnabled;
240242
var stopWath = new Stopwatch();
@@ -247,17 +249,17 @@ private void EndLoadingCollection(LoadingCollectionEntry lce, ICollectionPersist
247249

248250
if (persister.CollectionType.HasHolder())
249251
{
250-
LoadContext.PersistenceContext.AddCollectionHolder(lce.Collection);
252+
persistenceContext.AddCollectionHolder(lce.Collection);
251253
}
252254

253-
CollectionEntry ce = LoadContext.PersistenceContext.GetCollectionEntry(lce.Collection);
255+
CollectionEntry ce = persistenceContext.GetCollectionEntry(lce.Collection);
254256
if (ce == null)
255257
{
256-
ce = LoadContext.PersistenceContext.AddInitializedCollection(persister, lce.Collection, lce.Key);
258+
ce = persistenceContext.AddInitializedCollection(persister, lce.Collection, lce.Key);
257259
}
258260
else
259261
{
260-
ce.PostInitialize(lce.Collection);
262+
ce.PostInitialize(lce.Collection, persistenceContext);
261263
}
262264

263265
bool addToCache = hasNoQueuedAdds && persister.HasCache &&

src/NHibernate/Engine/StatefulPersistenceContext.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,10 @@ public void AddUninitializedCollection(ICollectionPersister persister, IPersiste
835835
{
836836
CollectionEntry ce = new CollectionEntry(collection, persister, id, flushing);
837837
AddCollection(collection, ce, id);
838+
if (persister.GetBatchSize() > 1)
839+
{
840+
batchFetchQueue.AddBatchLoadableCollection(collection, ce);
841+
}
838842
}
839843

840844
/// <summary> add a detached uninitialized collection</summary>
@@ -913,7 +917,7 @@ public CollectionEntry AddInitializedCollection(ICollectionPersister persister,
913917
object id)
914918
{
915919
CollectionEntry ce = new CollectionEntry(collection, persister, id, flushing);
916-
ce.PostInitialize(collection);
920+
ce.PostInitialize(collection, this);
917921
AddCollection(collection, ce, id);
918922
return ce;
919923
}

src/NHibernate/Event/Default/DefaultInitializeCollectionEventListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ private bool InitializeCollectionFromCache(object id, ICollectionPersister persi
115115
CollectionCacheEntry cacheEntry = (CollectionCacheEntry)persister.CacheEntryStructure.Destructure(ce, factory);
116116
cacheEntry.Assemble(collection, persister, persistenceContext.GetCollectionOwner(id, persister));
117117

118-
persistenceContext.GetCollectionEntry(collection).PostInitialize(collection);
118+
persistenceContext.GetCollectionEntry(collection).PostInitialize(collection, persistenceContext);
119119
return true;
120120
}
121121
}

0 commit comments

Comments
 (0)