Skip to content

Commit 0cd9c10

Browse files
committed
Improved collection batch fetching (HHH-1775)
1 parent f0644cf commit 0cd9c10

File tree

12 files changed

+144
-38
lines changed

12 files changed

+144
-38
lines changed

src/NHibernate.Test/NHSpecificTest/SetFixture.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ public object NotFoundObject
348348
get { throw new NotImplementedException(); }
349349
}
350350

351+
public int BatchSize { get; } = 0;
352+
351353
public ISessionFactoryImplementor Factory
352354
{
353355
get { return null; }

src/NHibernate/Async/Engine/BatchFetchQueue.cs

Lines changed: 33 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,23 +41,42 @@ 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+
// should never happen
63+
if (log.IsWarnEnabled())
64+
{
65+
log.Warn("Encountered initialized collection in BatchFetchQueue, this should not happen.");
66+
}
67+
continue;
68+
}
69+
5270
if (checkForEnd && i == end)
5371
{
5472
return keys; //the first key found after the given key
5573
}
5674

57-
//if ( end == -1 && count > batchSize*10 ) return keys; //try out ten batches, max
58-
59-
bool isEqual = collectionPersister.KeyType.IsEqual(id, ce.LoadedKey, collectionPersister.Factory);
75+
var isEqual = collectionPersister.KeyType.IsEqual(
76+
id,
77+
ce.LoadedKey,
78+
collectionPersister.Factory
79+
);
6080

6181
if (isEqual)
6282
{
@@ -79,6 +99,7 @@ public async Task<object[]> GetCollectionBatchAsync(ICollectionPersister collect
7999
}
80100
}
81101
}
102+
82103
return keys; //we ran out of keys to try
83104
}
84105

@@ -101,9 +122,9 @@ public async Task<object[]> GetEntityBatchAsync(IEntityPersister persister,objec
101122
int end = -1;
102123
bool checkForEnd = false;
103124

104-
foreach (EntityKey key in batchLoadableEntityKeys.Keys)
125+
if (batchLoadableEntityKeys.TryGetValue(persister.EntityName,out var set))//TODO: this needn't exclude subclasses...
105126
{
106-
if (key.EntityName.Equals(persister.EntityName))
127+
foreach (var key in set)
107128
{
108129
//TODO: this needn't exclude subclasses...
109130
if (checkForEnd && i == end)

src/NHibernate/Collection/AbstractPersistentCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public virtual bool RowUpdatePossible
233233
}
234234

235235
/// <summary></summary>
236-
protected virtual ISessionImplementor Session
236+
public virtual ISessionImplementor Session
237237
{
238238
get { return session; }
239239
}

src/NHibernate/Engine/BatchFetchQueue.cs

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,32 @@
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);
25-
24+
private readonly IDictionary<string, LinkedHashSet<EntityKey>> batchLoadableEntityKeys = new Dictionary<string, LinkedHashSet<EntityKey>>(8);
25+
2626
/// <summary>
2727
/// A map of <see cref="SubselectFetch">subselect-fetch descriptors</see>
2828
/// keyed by the <see cref="EntityKey" /> against which the descriptor is
2929
/// registered.
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,23 +187,42 @@ 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+
// should never happen
209+
if (log.IsWarnEnabled())
210+
{
211+
log.Warn("Encountered initialized collection in BatchFetchQueue, this should not happen.");
212+
}
213+
continue;
214+
}
215+
155216
if (checkForEnd && i == end)
156217
{
157218
return keys; //the first key found after the given key
158219
}
159220

160-
//if ( end == -1 && count > batchSize*10 ) return keys; //try out ten batches, max
161-
162-
bool isEqual = collectionPersister.KeyType.IsEqual(id, ce.LoadedKey, collectionPersister.Factory);
221+
var isEqual = collectionPersister.KeyType.IsEqual(
222+
id,
223+
ce.LoadedKey,
224+
collectionPersister.Factory
225+
);
163226

164227
if (isEqual)
165228
{
@@ -182,6 +245,7 @@ public object[] GetCollectionBatch(ICollectionPersister collectionPersister, obj
182245
}
183246
}
184247
}
248+
185249
return keys; //we ran out of keys to try
186250
}
187251

@@ -202,9 +266,9 @@ public object[] GetEntityBatch(IEntityPersister persister,object id,int batchSiz
202266
int end = -1;
203267
bool checkForEnd = false;
204268

205-
foreach (EntityKey key in batchLoadableEntityKeys.Keys)
269+
if (batchLoadableEntityKeys.TryGetValue(persister.EntityName,out var set))//TODO: this needn't exclude subclasses...
206270
{
207-
if (key.EntityName.Equals(persister.EntityName))
271+
foreach (var key in set)
208272
{
209273
//TODO: this needn't exclude subclasses...
210274
if (checkForEnd && i == end)

src/NHibernate/Engine/CollectionEntry.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ public void PostInitialize(IPersistentCollection collection)
302302
{
303303
snapshot = LoadedPersister.IsMutable ? collection.GetSnapshot(LoadedPersister) : null;
304304
collection.SetSnapshot(loadedKey, role, snapshot);
305+
if (LoadedPersister.BatchSize > 1)
306+
{
307+
((AbstractPersistentCollection) collection).Session.PersistenceContext.BatchFetchQueue.RemoveBatchLoadableCollection(this);
308+
}
305309
}
306310

307311
/// <summary>

src/NHibernate/Engine/StatefulPersistenceContext.cs

Lines changed: 4 additions & 0 deletions
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.BatchSize > 0)
839+
{
840+
batchFetchQueue.AddBatchLoadableCollection(collection, ce);
841+
}
838842
}
839843

840844
/// <summary> add a detached uninitialized collection</summary>

src/NHibernate/Event/Default/EvictVisitor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ private void EvictCollection(IPersistentCollection collection)
5353
Session.PersistenceContext.CollectionEntries.Remove(collection);
5454
if (log.IsDebugEnabled())
5555
log.Debug("evicting collection: {0}", MessageHelper.CollectionInfoString(ce.LoadedPersister, collection, ce.LoadedKey, Session));
56+
if (ce.LoadedPersister?.BatchSize > 1)
57+
{
58+
Session.PersistenceContext.BatchFetchQueue.RemoveBatchLoadableCollection(ce);
59+
}
5660
if (ce.LoadedPersister != null && ce.LoadedKey != null)
5761
{
5862
//TODO: is this 100% correct?

src/NHibernate/Persister/Collection/AbstractCollectionPersister.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,11 @@ public string[] RootTableKeyColumnNames
20432043
get { return new string[] {IdentifierColumnName}; }
20442044
}
20452045

2046+
public int BatchSize
2047+
{
2048+
get { return batchSize; }
2049+
}
2050+
20462051
public SqlString GetSelectByUniqueKeyString(string propertyName)
20472052
{
20482053
return

src/NHibernate/Persister/Collection/BasicCollectionPersister.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ private string ManyToManySelectFragment(IJoinable rhs, string rhsAlias, string l
281281
/// </summary>
282282
protected override ICollectionInitializer CreateCollectionInitializer(IDictionary<string, IFilter> enabledFilters)
283283
{
284-
return BatchingCollectionInitializer.CreateBatchingCollectionInitializer(this, batchSize, Factory, enabledFilters);
284+
return BatchingCollectionInitializer.CreateBatchingCollectionInitializer(this, BatchSize, Factory, enabledFilters);
285285
}
286286

287287
public override SqlString FromJoinFragment(string alias, bool innerJoin, bool includeSubclasses)

src/NHibernate/Persister/Collection/ICollectionPersister.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,5 +282,7 @@ public partial interface ICollectionPersister
282282
/// A place-holder to inform that the data-reader was empty.
283283
/// </summary>
284284
object NotFoundObject { get; }
285+
286+
int BatchSize { get; }
285287
}
286288
}

src/NHibernate/Persister/Collection/OneToManyPersister.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ protected override SelectFragment GenerateSelectFragment(string alias, string co
343343
/// </summary>
344344
protected override ICollectionInitializer CreateCollectionInitializer(IDictionary<string, IFilter> enabledFilters)
345345
{
346-
return BatchingCollectionInitializer.CreateBatchingOneToManyInitializer(this, batchSize, Factory, enabledFilters);
346+
return BatchingCollectionInitializer.CreateBatchingOneToManyInitializer(this, BatchSize, Factory, enabledFilters);
347347
}
348348

349349
public override SqlString FromJoinFragment(string alias, bool innerJoin, bool includeSubclasses)

src/NHibernate/Type/ManyToOneType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ private void ScheduleBatchLoadIfNeeded(object id, ISessionImplementor session)
101101
{
102102
IEntityPersister persister = session.Factory.GetEntityPersister(GetAssociatedEntityName());
103103
EntityKey entityKey = session.GenerateEntityKey(id, persister);
104-
if (!session.PersistenceContext.ContainsEntity(entityKey))
104+
if (entityKey.IsBatchLoadable && !session.PersistenceContext.ContainsEntity(entityKey))
105105
{
106106
session.PersistenceContext.BatchFetchQueue.AddBatchLoadableEntityKey(entityKey);
107107
}

0 commit comments

Comments
 (0)