Skip to content

Commit 7c7c628

Browse files
Fix extra-lazy collection forgetting changes
Fixes #1874
1 parent 539a814 commit 7c7c628

File tree

11 files changed

+191
-21
lines changed

11 files changed

+191
-21
lines changed

src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
using System.Collections;
1212
using System.Collections.Generic;
13+
using System.Linq;
1314
using NUnit.Framework;
1415

1516
namespace NHibernate.Test.Extralazy
@@ -33,6 +34,16 @@ protected override string CacheConcurrencyStrategy
3334
get { return null; }
3435
}
3536

37+
protected override void OnTearDown()
38+
{
39+
using (var s = OpenSession())
40+
using (var t = s.BeginTransaction())
41+
{
42+
s.Delete("from System.Object");
43+
t.Commit();
44+
}
45+
}
46+
3647
[Test]
3748
public async Task ExtraLazyWithWhereClauseAsync()
3849
{
@@ -94,6 +105,10 @@ public async Task OrphanDeleteAsync()
94105
gavin = await (s.GetAsync<User>("gavin"));
95106
Assert.AreEqual(2, gavin.Documents.Count);
96107
gavin.Documents.Remove(hia2);
108+
// Do not convert Documents.Contains to Does.Contain/Does.Not.Contain: NUnit constraints will trigger
109+
// initialization of the collection. Moreover, with extra-lazy, collection.Contains works even if
110+
// the entity does not properly overrides Equals and GetHashCode and a detached instance is used,
111+
// provided the collection is not yet initialized, which is the case here.
97112
Assert.IsFalse(gavin.Documents.Contains(hia2));
98113
Assert.IsTrue(gavin.Documents.Contains(hia));
99114
Assert.AreEqual(1, gavin.Documents.Count);
@@ -117,6 +132,47 @@ public async Task OrphanDeleteAsync()
117132
}
118133
}
119134

135+
[Test]
136+
public async Task OrphanDeleteWithEnumerationAsync()
137+
{
138+
using (var s = OpenSession())
139+
using (var t = s.BeginTransaction())
140+
{
141+
var gavin = new User("gavin", "secret");
142+
gavin.Documents.Add(new Document("HiA", "blah blah blah", gavin));
143+
gavin.Documents.Add(new Document("HiA2", "blah blah blah blah", gavin));
144+
await (s.PersistAsync(gavin));
145+
await (t.CommitAsync());
146+
}
147+
148+
using (var s = OpenSession())
149+
using (var t = s.BeginTransaction())
150+
{
151+
var hia2 = await (s.GetAsync<Document>("HiA2"));
152+
var hia = await (s.GetAsync<Document>("HiA"));
153+
var gavin = await (s.GetAsync<User>("gavin"));
154+
Assert.That(gavin.Documents, Has.Count.EqualTo(2));
155+
Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Expecting non initialized collection");
156+
gavin.Documents.Remove(hia2);
157+
// Force an enumeration
158+
using (var e = gavin.Documents.GetEnumerator())
159+
e.MoveNext();
160+
Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Expecting initialized collection");
161+
Assert.That(gavin.Documents, Does.Not.Contain(hia2).And.Contain(hia).And.Count.EqualTo(1));
162+
await (t.CommitAsync());
163+
}
164+
165+
using (var s = OpenSession())
166+
using (var t = s.BeginTransaction())
167+
{
168+
var hia = await (s.GetAsync<Document>("HiA"));
169+
var gavin = await (s.GetAsync<User>("gavin"));
170+
Assert.That(gavin.Documents, Has.Count.EqualTo(1).And.Contain(hia));
171+
Assert.That(await (s.GetAsync<Document>("HiA2")), Is.Null);
172+
await (t.CommitAsync());
173+
}
174+
}
175+
120176
[Test]
121177
public async Task GetAsync()
122178
{
@@ -340,4 +396,4 @@ public async Task AddToUninitializedSetWithLaterLazyLoadAsync()
340396
}
341397
}
342398
}
343-
}
399+
}

src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using NUnit.Framework;
45

56
namespace NHibernate.Test.Extralazy
@@ -22,6 +23,16 @@ protected override string CacheConcurrencyStrategy
2223
get { return null; }
2324
}
2425

26+
protected override void OnTearDown()
27+
{
28+
using (var s = OpenSession())
29+
using (var t = s.BeginTransaction())
30+
{
31+
s.Delete("from System.Object");
32+
t.Commit();
33+
}
34+
}
35+
2536
[Test]
2637
public void ExtraLazyWithWhereClause()
2738
{
@@ -83,6 +94,10 @@ public void OrphanDelete()
8394
gavin = s.Get<User>("gavin");
8495
Assert.AreEqual(2, gavin.Documents.Count);
8596
gavin.Documents.Remove(hia2);
97+
// Do not convert Documents.Contains to Does.Contain/Does.Not.Contain: NUnit constraints will trigger
98+
// initialization of the collection. Moreover, with extra-lazy, collection.Contains works even if
99+
// the entity does not properly overrides Equals and GetHashCode and a detached instance is used,
100+
// provided the collection is not yet initialized, which is the case here.
86101
Assert.IsFalse(gavin.Documents.Contains(hia2));
87102
Assert.IsTrue(gavin.Documents.Contains(hia));
88103
Assert.AreEqual(1, gavin.Documents.Count);
@@ -106,6 +121,47 @@ public void OrphanDelete()
106121
}
107122
}
108123

124+
[Test]
125+
public void OrphanDeleteWithEnumeration()
126+
{
127+
using (var s = OpenSession())
128+
using (var t = s.BeginTransaction())
129+
{
130+
var gavin = new User("gavin", "secret");
131+
gavin.Documents.Add(new Document("HiA", "blah blah blah", gavin));
132+
gavin.Documents.Add(new Document("HiA2", "blah blah blah blah", gavin));
133+
s.Persist(gavin);
134+
t.Commit();
135+
}
136+
137+
using (var s = OpenSession())
138+
using (var t = s.BeginTransaction())
139+
{
140+
var hia2 = s.Get<Document>("HiA2");
141+
var hia = s.Get<Document>("HiA");
142+
var gavin = s.Get<User>("gavin");
143+
Assert.That(gavin.Documents, Has.Count.EqualTo(2));
144+
Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Expecting non initialized collection");
145+
gavin.Documents.Remove(hia2);
146+
// Force an enumeration
147+
using (var e = gavin.Documents.GetEnumerator())
148+
e.MoveNext();
149+
Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Expecting initialized collection");
150+
Assert.That(gavin.Documents, Does.Not.Contain(hia2).And.Contain(hia).And.Count.EqualTo(1));
151+
t.Commit();
152+
}
153+
154+
using (var s = OpenSession())
155+
using (var t = s.BeginTransaction())
156+
{
157+
var hia = s.Get<Document>("HiA");
158+
var gavin = s.Get<User>("gavin");
159+
Assert.That(gavin.Documents, Has.Count.EqualTo(1).And.Contain(hia));
160+
Assert.That(s.Get<Document>("HiA2"), Is.Null);
161+
t.Commit();
162+
}
163+
}
164+
109165
[Test]
110166
public void Get()
111167
{
@@ -329,4 +385,4 @@ public void AddToUninitializedSetWithLaterLazyLoad()
329385
}
330386
}
331387
}
332-
}
388+
}

src/NHibernate/Async/Cache/Entry/CollectionCacheEntry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public virtual async Task AssembleAsync(IPersistentCollection collection, IColle
3434
{
3535
cancellationToken.ThrowIfCancellationRequested();
3636
await (collection.InitializeFromCacheAsync(persister, state, owner, cancellationToken)).ConfigureAwait(false);
37-
collection.AfterInitialize(persister);
37+
var hasNoQueuedOperations = collection.AfterInitialize(persister);
3838
}
3939
}
4040
}

src/NHibernate/Async/Collection/IPersistentCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//------------------------------------------------------------------------------
99

1010

11+
using System;
1112
using System.Collections;
1213
using System.Data.Common;
1314
using NHibernate.Collection.Generic;

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private async Task EndLoadingCollectionAsync(LoadingCollectionEntry lce, ICollec
144144
stopWath.Start();
145145
}
146146

147-
bool hasNoQueuedAdds = lce.Collection.EndRead(persister); // warning: can cause a recursive calls! (proxy initialization)
147+
bool hasNoQueuedOperations = lce.Collection.EndRead(persister); // warning: can cause a recursive calls! (proxy initialization)
148148

149149
if (persister.CollectionType.HasHolder())
150150
{
@@ -161,14 +161,17 @@ private async Task EndLoadingCollectionAsync(LoadingCollectionEntry lce, ICollec
161161
ce.PostInitialize(lce.Collection, persistenceContext);
162162
}
163163

164-
bool addToCache = hasNoQueuedAdds && persister.HasCache &&
164+
bool addToCache = hasNoQueuedOperations && persister.HasCache &&
165165
session.CacheMode.HasFlag(CacheMode.Put) && !ce.IsDoremove; // and this is not a forced initialization during flush
166166

167167
if (addToCache)
168168
{
169169
await (AddCollectionToCacheAsync(lce, persister, cacheBatchingHandler, cancellationToken)).ConfigureAwait(false);
170170
}
171171

172+
if (!hasNoQueuedOperations)
173+
lce.Collection.ApplyQueuedOperations();
174+
172175
if (log.IsDebugEnabled())
173176
{
174177
log.Debug("collection fully initialized: {0}", MessageHelper.CollectionInfoString(persister, lce.Collection, lce.Key, session));

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ private async Task<bool> AssembleAsync(CacheKey ck, object ce, ICollectionPersis
161161
await (cacheEntry.AssembleAsync(collection, persister, persistenceContext.GetCollectionOwner(id, persister), cancellationToken)).ConfigureAwait(false);
162162

163163
persistenceContext.GetCollectionEntry(collection).PostInitialize(collection, persistenceContext);
164+
165+
if (collection.HasQueuedOperations)
166+
collection.ApplyQueuedOperations();
164167
return true;
165168
}
166169
}

src/NHibernate/Cache/Entry/CollectionCacheEntry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public virtual object[] State
4242
public virtual void Assemble(IPersistentCollection collection, ICollectionPersister persister, object owner)
4343
{
4444
collection.InitializeFromCache(persister, state, owner);
45-
collection.AfterInitialize(persister);
45+
var hasNoQueuedOperations = collection.AfterInitialize(persister);
4646
}
4747

4848
public override string ToString()

src/NHibernate/Collection/AbstractPersistentCollection.cs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,12 @@ protected virtual void QueueOperation(IDelayedOperation element)
379379
dirty = true; //needed so that we remove this collection from the second-level cache
380380
}
381381

382-
/// <summary>
382+
// Obsolete since v5.2
383+
/// <summary>
383384
/// After reading all existing elements from the database,
384385
/// add the queued elements to the underlying collection.
385386
/// </summary>
387+
[Obsolete("Use or override ApplyQueuedOperations instead")]
386388
protected virtual void PerformQueuedOperations()
387389
{
388390
for (int i = 0; i < operationQueue.Count; i++)
@@ -391,6 +393,22 @@ protected virtual void PerformQueuedOperations()
391393
}
392394
}
393395

396+
/// <summary>
397+
/// After reading all existing elements from the database, do the queued operations
398+
/// (adds or removes) on the underlying collection.
399+
/// </summary>
400+
public virtual void ApplyQueuedOperations()
401+
{
402+
if (operationQueue == null)
403+
throw new InvalidOperationException("There are no operation queue.");
404+
405+
#pragma warning disable 618
406+
PerformQueuedOperations();
407+
#pragma warning restore 618
408+
409+
operationQueue = null;
410+
}
411+
394412
public void SetSnapshot(object key, string role, object snapshot)
395413
{
396414
this.key = key;
@@ -437,18 +455,8 @@ public virtual bool EndRead(ICollectionPersister persister)
437455
public virtual bool AfterInitialize(ICollectionPersister persister)
438456
{
439457
SetInitialized();
440-
//do this bit after setting initialized to true or it will recurse
441-
if (operationQueue != null)
442-
{
443-
PerformQueuedOperations();
444-
operationQueue = null;
445-
cachedSize = -1;
446-
return false;
447-
}
448-
else
449-
{
450-
return true;
451-
}
458+
cachedSize = -1;
459+
return operationQueue == null;
452460
}
453461

454462
/// <summary>

src/NHibernate/Collection/IPersistentCollection.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections;
23
using System.Data.Common;
34
using NHibernate.Collection.Generic;
@@ -337,4 +338,40 @@ public partial interface IPersistentCollection
337338
/// </returns>
338339
ICollection GetOrphans(object snapshot, string entityName);
339340
}
341+
342+
// 6.0 TODO: merge into IPersistentCollection
343+
public static class PersistentCollectionExtensions
344+
{
345+
private static readonly INHibernateLogger Logger = NHibernateLogger.For(typeof(PersistentCollectionExtensions));
346+
347+
/// <summary>
348+
/// After reading all existing elements from the database, do the queued operations
349+
/// (adds or removes) on the underlying collection.
350+
/// </summary>
351+
/// <param name="collection">The collection.</param>
352+
public static void ApplyQueuedOperations(this IPersistentCollection collection)
353+
{
354+
if (collection is AbstractPersistentCollection baseImpl)
355+
{
356+
baseImpl.ApplyQueuedOperations();
357+
return;
358+
}
359+
360+
// Fallback on reflection for custom implementations
361+
var collectionType = collection.GetType();
362+
var applyQueuedOperationsMethod = collectionType.GetMethod(
363+
nameof(AbstractPersistentCollection.ApplyQueuedOperations),
364+
Array.Empty<System.Type>());
365+
if (applyQueuedOperationsMethod != null)
366+
{
367+
applyQueuedOperationsMethod.Invoke(collection, Array.Empty<object>());
368+
return;
369+
}
370+
371+
Logger.Warn(
372+
"{0} does not implement 'void ApplyQueuedOperations()'. It should move any queued operations" +
373+
"processing out of 'AfterInitialize' and put it in a 'public void ApplyQueuedOperations()'.",
374+
collectionType);
375+
}
376+
}
340377
}

src/NHibernate/Engine/Loading/CollectionLoadContext.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ private void EndLoadingCollection(LoadingCollectionEntry lce, ICollectionPersist
252252
stopWath.Start();
253253
}
254254

255-
bool hasNoQueuedAdds = lce.Collection.EndRead(persister); // warning: can cause a recursive calls! (proxy initialization)
255+
bool hasNoQueuedOperations = lce.Collection.EndRead(persister); // warning: can cause a recursive calls! (proxy initialization)
256256

257257
if (persister.CollectionType.HasHolder())
258258
{
@@ -269,14 +269,17 @@ private void EndLoadingCollection(LoadingCollectionEntry lce, ICollectionPersist
269269
ce.PostInitialize(lce.Collection, persistenceContext);
270270
}
271271

272-
bool addToCache = hasNoQueuedAdds && persister.HasCache &&
272+
bool addToCache = hasNoQueuedOperations && persister.HasCache &&
273273
session.CacheMode.HasFlag(CacheMode.Put) && !ce.IsDoremove; // and this is not a forced initialization during flush
274274

275275
if (addToCache)
276276
{
277277
AddCollectionToCache(lce, persister, cacheBatchingHandler);
278278
}
279279

280+
if (!hasNoQueuedOperations)
281+
lce.Collection.ApplyQueuedOperations();
282+
280283
if (log.IsDebugEnabled())
281284
{
282285
log.Debug("collection fully initialized: {0}", MessageHelper.CollectionInfoString(persister, lce.Collection, lce.Key, session));

src/NHibernate/Event/Default/DefaultInitializeCollectionEventListener.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ private bool Assemble(CacheKey ck, object ce, ICollectionPersister persister, I
148148
cacheEntry.Assemble(collection, persister, persistenceContext.GetCollectionOwner(id, persister));
149149

150150
persistenceContext.GetCollectionEntry(collection).PostInitialize(collection, persistenceContext);
151+
152+
if (collection.HasQueuedOperations)
153+
collection.ApplyQueuedOperations();
151154
return true;
152155
}
153156
}

0 commit comments

Comments
 (0)