diff --git a/src/NHibernate.Test/Async/GhostProperty/GhostPropertyFixture.cs b/src/NHibernate.Test/Async/GhostProperty/GhostPropertyFixture.cs index de24065308b..0118302f356 100644 --- a/src/NHibernate.Test/Async/GhostProperty/GhostPropertyFixture.cs +++ b/src/NHibernate.Test/Async/GhostProperty/GhostPropertyFixture.cs @@ -167,6 +167,21 @@ public async Task WhenGetThenLoadOnlyNoLazyPlainPropertiesAsync() } Assert.That(NHibernateUtil.IsPropertyInitialized(order, "ALazyProperty"), Is.True); } - } + } + + [Test] + public async Task AcceptPropertySetWithTransientObjectAsync() + { + Order order; + using (var s = OpenSession()) + { + order = await (s.GetAsync(1)); + } + + var newPayment = new WireTransfer(); + order.Payment = newPayment; + + Assert.That(order.Payment, Is.EqualTo(newPayment)); + } } } diff --git a/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs b/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs index 63343f89e1e..476dab5f19a 100644 --- a/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs +++ b/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs @@ -10,14 +10,17 @@ using System.Collections; using System.Linq; +using NHibernate.Cfg; using NHibernate.Intercept; using NHibernate.Tuple.Entity; using NUnit.Framework; +using NUnit.Framework.Constraints; using NHibernate.Linq; namespace NHibernate.Test.LazyProperty { using System.Threading.Tasks; + using System.Threading; [TestFixture] public class LazyPropertyFixtureAsync : TestCase { @@ -43,6 +46,11 @@ protected override DebugSessionFactory BuildSessionFactory() } } + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.GenerateStatistics, "true"); + } + protected override void OnSetUp() { Assert.That( @@ -58,6 +66,7 @@ protected override void OnSetUp() Name = "some name", Id = 1, ALotOfText = "a lot of text ...", + Image = new byte[10], FieldInterceptor = "Why not that name?" }); tx.Commit(); @@ -122,6 +131,94 @@ public async Task CanGetValueForLazyPropertyAsync() } } + [Test] + public async Task CanSetValueForLazyPropertyAsync() + { + Book book; + using (ISession s = OpenSession()) + { + book = await (s.GetAsync(1)); + } + + book.ALotOfText = "text"; + + Assert.That(book.ALotOfText, Is.EqualTo("text")); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.True); + } + + [TestCase(false)] + [TestCase(true)] + public async Task CanUpdateValueForLazyPropertyAsync(bool initializeAfterSet, CancellationToken cancellationToken = default(CancellationToken)) + { + Book book; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + book = await (s.GetAsync(1, cancellationToken)); + book.ALotOfText = "update-text"; + if (initializeAfterSet) + { + var image = book.Image; + } + + await (tx.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + { + book = await (s.GetAsync(1, cancellationToken)); + var text = book.ALotOfText; + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "Image"), Is.True); + Assert.That(book.ALotOfText, Is.EqualTo("update-text")); + Assert.That(book.Image, Has.Length.EqualTo(10)); + } + + [TestCase(false)] + [TestCase(true)] + public async Task UpdateValueForLazyPropertyToSameValueAsync(bool initializeAfterSet, CancellationToken cancellationToken = default(CancellationToken)) + { + Book book; + string text; + + using (var s = OpenSession()) + { + book = await (s.GetAsync(1, cancellationToken)); + text = book.ALotOfText; + } + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + book = await (s.GetAsync(1, cancellationToken)); + book.ALotOfText = text; + if (initializeAfterSet) + { + var image = book.Image; + } + + await (tx.CommitAsync(cancellationToken)); + } + + Assert.That(Sfi.Statistics.EntityUpdateCount, Is.EqualTo(initializeAfterSet ? 0 : 1)); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "Image"), initializeAfterSet ? (Constraint) Is.True : Is.False); + Assert.That(book.ALotOfText, Is.EqualTo(text)); + + using (var s = OpenSession()) + { + book = await (s.GetAsync(1, cancellationToken)); + text = book.ALotOfText; + } + + Assert.That(book.Image, Has.Length.EqualTo(10)); + Assert.That(book.ALotOfText, Is.EqualTo(text)); + } + [Test] public async Task CanGetValueForNonLazyPropertyAsync() { diff --git a/src/NHibernate.Test/GhostProperty/GhostPropertyFixture.cs b/src/NHibernate.Test/GhostProperty/GhostPropertyFixture.cs index 239a1c0f1b0..ddee24f148f 100644 --- a/src/NHibernate.Test/GhostProperty/GhostPropertyFixture.cs +++ b/src/NHibernate.Test/GhostProperty/GhostPropertyFixture.cs @@ -162,6 +162,21 @@ public void WhenGetThenLoadOnlyNoLazyPlainProperties() } Assert.That(NHibernateUtil.IsPropertyInitialized(order, "ALazyProperty"), Is.True); } - } + } + + [Test] + public void AcceptPropertySetWithTransientObject() + { + Order order; + using (var s = OpenSession()) + { + order = s.Get(1); + } + + var newPayment = new WireTransfer(); + order.Payment = newPayment; + + Assert.That(order.Payment, Is.EqualTo(newPayment)); + } } } diff --git a/src/NHibernate.Test/LazyProperty/Book.cs b/src/NHibernate.Test/LazyProperty/Book.cs index 40942a6af47..c4fdf9c5639 100644 --- a/src/NHibernate.Test/LazyProperty/Book.cs +++ b/src/NHibernate.Test/LazyProperty/Book.cs @@ -13,6 +13,8 @@ public virtual string ALotOfText set { _aLotOfText = value; } } + public virtual byte[] Image { get; set; } + public virtual string FieldInterceptor { get; set; } } } diff --git a/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs b/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs index 1bc83ffa367..4d514387893 100644 --- a/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs +++ b/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs @@ -1,8 +1,10 @@ using System.Collections; using System.Linq; +using NHibernate.Cfg; using NHibernate.Intercept; using NHibernate.Tuple.Entity; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace NHibernate.Test.LazyProperty { @@ -31,6 +33,11 @@ protected override DebugSessionFactory BuildSessionFactory() } } + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.GenerateStatistics, "true"); + } + protected override void OnSetUp() { Assert.That( @@ -46,6 +53,7 @@ protected override void OnSetUp() Name = "some name", Id = 1, ALotOfText = "a lot of text ...", + Image = new byte[10], FieldInterceptor = "Why not that name?" }); tx.Commit(); @@ -116,6 +124,94 @@ public void CanGetValueForLazyProperty() } } + [Test] + public void CanSetValueForLazyProperty() + { + Book book; + using (ISession s = OpenSession()) + { + book = s.Get(1); + } + + book.ALotOfText = "text"; + + Assert.That(book.ALotOfText, Is.EqualTo("text")); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.True); + } + + [TestCase(false)] + [TestCase(true)] + public void CanUpdateValueForLazyProperty(bool initializeAfterSet) + { + Book book; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + book = s.Get(1); + book.ALotOfText = "update-text"; + if (initializeAfterSet) + { + var image = book.Image; + } + + tx.Commit(); + } + + using (var s = OpenSession()) + { + book = s.Get(1); + var text = book.ALotOfText; + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "Image"), Is.True); + Assert.That(book.ALotOfText, Is.EqualTo("update-text")); + Assert.That(book.Image, Has.Length.EqualTo(10)); + } + + [TestCase(false)] + [TestCase(true)] + public void UpdateValueForLazyPropertyToSameValue(bool initializeAfterSet) + { + Book book; + string text; + + using (var s = OpenSession()) + { + book = s.Get(1); + text = book.ALotOfText; + } + + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + book = s.Get(1); + book.ALotOfText = text; + if (initializeAfterSet) + { + var image = book.Image; + } + + tx.Commit(); + } + + Assert.That(Sfi.Statistics.EntityUpdateCount, Is.EqualTo(initializeAfterSet ? 0 : 1)); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(book, "Image"), initializeAfterSet ? (Constraint) Is.True : Is.False); + Assert.That(book.ALotOfText, Is.EqualTo(text)); + + using (var s = OpenSession()) + { + book = s.Get(1); + text = book.ALotOfText; + } + + Assert.That(book.Image, Has.Length.EqualTo(10)); + Assert.That(book.ALotOfText, Is.EqualTo(text)); + } + [Test] public void CanGetValueForNonLazyProperty() { diff --git a/src/NHibernate.Test/LazyProperty/Mappings.hbm.xml b/src/NHibernate.Test/LazyProperty/Mappings.hbm.xml index 25eaafa3554..a4a141e5268 100644 --- a/src/NHibernate.Test/LazyProperty/Mappings.hbm.xml +++ b/src/NHibernate.Test/LazyProperty/Mappings.hbm.xml @@ -9,6 +9,7 @@ + diff --git a/src/NHibernate/Async/Engine/Cascade.cs b/src/NHibernate/Async/Engine/Cascade.cs index dd70abda133..e33a941ba1e 100644 --- a/src/NHibernate/Async/Engine/Cascade.cs +++ b/src/NHibernate/Async/Engine/Cascade.cs @@ -60,12 +60,12 @@ public async Task CascadeOnAsync(IEntityPersister persister, object parent, obje IType[] types = persister.PropertyTypes; CascadeStyle[] cascadeStyles = persister.PropertyCascadeStyles; - bool hasUninitializedLazyProperties = persister.HasUninitializedLazyProperties(parent); + var uninitializedLazyProperties = persister.GetUninitializedLazyProperties(parent); for (int i = 0; i < types.Length; i++) { CascadeStyle style = cascadeStyles[i]; string propertyName = persister.PropertyNames[i]; - if (hasUninitializedLazyProperties && persister.PropertyLaziness[i] && !action.PerformOnLazyProperty) + if (uninitializedLazyProperties.Contains(propertyName) && persister.PropertyLaziness[i] && !action.PerformOnLazyProperty) { //do nothing to avoid a lazy property initialization continue; diff --git a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs index de93d260b81..d2d0a5e3361 100644 --- a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs @@ -876,8 +876,8 @@ public async Task UpdateAsync(object id, object[] fields, int[] dirtyFields, boo // in the process of being deleted. if (entry == null && !IsMutable) throw new InvalidOperationException("Updating immutable entity that is not in session yet!"); - - if (entityMetamodel.IsDynamicUpdate && dirtyFields != null) + + if (dirtyFields != null && (entityMetamodel.IsDynamicUpdate || HasDirtyLazyProperties(dirtyFields, obj))) { // For the case of dynamic-update="true", we need to generate the UPDATE SQL propsToUpdate = GetPropertiesToUpdate(dirtyFields, hasDirtyCollection); diff --git a/src/NHibernate/Async/Persister/Entity/IEntityPersister.cs b/src/NHibernate/Async/Persister/Entity/IEntityPersister.cs index c9055f69c50..db1e4a16c1c 100644 --- a/src/NHibernate/Async/Persister/Entity/IEntityPersister.cs +++ b/src/NHibernate/Async/Persister/Entity/IEntityPersister.cs @@ -8,6 +8,7 @@ //------------------------------------------------------------------------------ +using System; using NHibernate.Cache; using NHibernate.Cache.Entry; using NHibernate.Engine; @@ -16,6 +17,9 @@ using NHibernate.Tuple.Entity; using NHibernate.Type; using System.Collections; +using System.Collections.Generic; +using NHibernate.Intercept; +using NHibernate.Util; namespace NHibernate.Persister.Entity { diff --git a/src/NHibernate/Engine/Cascade.cs b/src/NHibernate/Engine/Cascade.cs index d2a2de9832c..85500f7c9f2 100644 --- a/src/NHibernate/Engine/Cascade.cs +++ b/src/NHibernate/Engine/Cascade.cs @@ -113,12 +113,12 @@ public void CascadeOn(IEntityPersister persister, object parent, object anything IType[] types = persister.PropertyTypes; CascadeStyle[] cascadeStyles = persister.PropertyCascadeStyles; - bool hasUninitializedLazyProperties = persister.HasUninitializedLazyProperties(parent); + var uninitializedLazyProperties = persister.GetUninitializedLazyProperties(parent); for (int i = 0; i < types.Length; i++) { CascadeStyle style = cascadeStyles[i]; string propertyName = persister.PropertyNames[i]; - if (hasUninitializedLazyProperties && persister.PropertyLaziness[i] && !action.PerformOnLazyProperty) + if (uninitializedLazyProperties.Contains(propertyName) && persister.PropertyLaziness[i] && !action.PerformOnLazyProperty) { //do nothing to avoid a lazy property initialization continue; diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index 1a170d921d3..9936e4563ec 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using Iesi.Collections.Generic; using NHibernate.Engine; using NHibernate.Proxy; +using NHibernate.Util; namespace NHibernate.Intercept { @@ -13,6 +15,7 @@ public abstract class AbstractFieldInterceptor : IFieldInterceptor [NonSerialized] private ISessionImplementor session; private ISet uninitializedFields; + private ISet uninitializedFieldsReadOnly; private readonly ISet unwrapProxyFieldNames; private readonly HashSet loadedUnwrapProxyFieldNames = new HashSet(); private readonly string entityName; @@ -29,6 +32,7 @@ protected internal AbstractFieldInterceptor(ISessionImplementor session, ISet(); this.entityName = entityName; this.mappedClass = mappedClass; + this.uninitializedFieldsReadOnly = uninitializedFields != null ? new ReadOnlySet(uninitializedFields) : null; } #region IFieldInterceptor Members @@ -76,6 +80,8 @@ public System.Type MappedClass #endregion + // Since v5.3 + [Obsolete("Please use GetUninitializedFields extension method instead")] public ISet UninitializedFields { get { return uninitializedFields; } @@ -87,6 +93,11 @@ public bool Initializing } public object Intercept(object target, string fieldName, object value) + { + return Intercept(target, fieldName, value, false); + } + + public object Intercept(object target, string fieldName, object value, bool setter) { // NH Specific: Hibernate only deals with lazy properties here, we deal with // both lazy properties and with no-proxy. @@ -95,6 +106,19 @@ public object Intercept(object target, string fieldName, object value) return InvokeImplementation; } + if (setter) + { + if (IsUninitializedProperty(fieldName)) + { + uninitializedFields.Remove(fieldName); + } + + if (IsUninitializedAssociation(fieldName)) + { + loadedUnwrapProxyFieldNames.Add(fieldName); + } + } + if (IsInitializedField(fieldName)) { if (value.IsProxy() && IsInitializedAssociation(fieldName)) @@ -165,7 +189,13 @@ private object InitializeField(string fieldName, object target) initializing = false; } uninitializedFields = null; //let's assume that there is only one lazy fetch group, for now! + uninitializedFieldsReadOnly = null; return result; } + + public ISet GetUninitializedFields() + { + return uninitializedFieldsReadOnly ?? CollectionHelper.EmptySet(); + } } } diff --git a/src/NHibernate/Intercept/IFieldInterceptor.cs b/src/NHibernate/Intercept/IFieldInterceptor.cs index dafcdc00f15..4fd38f85aa4 100644 --- a/src/NHibernate/Intercept/IFieldInterceptor.cs +++ b/src/NHibernate/Intercept/IFieldInterceptor.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections.Generic; using NHibernate.Engine; +using NHibernate.Util; namespace NHibernate.Intercept { @@ -27,6 +30,8 @@ public interface IFieldInterceptor void ClearDirty(); /// Intercept field set/get + // Since v5.3 + [Obsolete("Please use 'Intercept(object target, string fieldName, object value, bool setter)' extension method instead")] object Intercept(object target, string fieldName, object value); /// Get the entity-name of the field DeclaringType. @@ -35,4 +40,35 @@ public interface IFieldInterceptor /// Get the MappedClass (field container). System.Type MappedClass { get; } } -} \ No newline at end of file + + public static class FieldInterceptorExtensions + { + // 6.0 TODO: merge into IFieldInterceptor + internal static ISet GetUninitializedFields(this IFieldInterceptor interceptor) + { + if (interceptor is AbstractFieldInterceptor fieldInterceptor) + { + return fieldInterceptor.GetUninitializedFields(); + } + + if (interceptor.IsInitialized) + { + return CollectionHelper.EmptySet(); + } + + return null; // The caller should use all lazy properties as the result + } + + // 6.0 TODO: merge into IFieldInterceptor + public static object Intercept(this IFieldInterceptor interceptor, object target, string fieldName, object value, bool setter) + { + if (interceptor is AbstractFieldInterceptor fieldInterceptor) + { + return fieldInterceptor.Intercept(target, fieldName, value, setter); + } +#pragma warning disable 618 + return interceptor.Intercept(target, fieldName, value); +#pragma warning restore 618 + } + } +} diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index d64012e055f..bb5d959f453 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -1281,6 +1281,7 @@ public virtual object InitializeLazyProperty(string fieldName, object entity, IS fieldName); } + var uninitializedLazyProperties = GetUninitializedLazyProperties(entity); if (HasCache && session.CacheMode.HasFlag(CacheMode.Get)) { CacheKey cacheKey = session.GenerateCacheKey(id, IdentifierType, EntityName); @@ -1291,15 +1292,16 @@ public virtual object InitializeLazyProperty(string fieldName, object entity, IS if (!cacheEntry.AreLazyPropertiesUnfetched) { //note early exit here: - return InitializeLazyPropertiesFromCache(fieldName, entity, session, entry, cacheEntry); + return InitializeLazyPropertiesFromCache(fieldName, entity, session, entry, cacheEntry, uninitializedLazyProperties); } } } - return InitializeLazyPropertiesFromDatastore(fieldName, entity, session, id, entry); + return InitializeLazyPropertiesFromDatastore(fieldName, entity, session, id, entry, uninitializedLazyProperties); } - private object InitializeLazyPropertiesFromDatastore(string fieldName, object entity, ISessionImplementor session, object id, EntityEntry entry) + private object InitializeLazyPropertiesFromDatastore(string fieldName, object entity, ISessionImplementor session, object id, EntityEntry entry, + ISet uninitializedLazyProperties) { if (!HasLazyProperties) throw new AssertionFailure("no lazy properties"); @@ -1329,7 +1331,7 @@ private object InitializeLazyPropertiesFromDatastore(string fieldName, object en for (int j = 0; j < lazyPropertyNames.Length; j++) { object propValue = lazyPropertyTypes[j].NullSafeGet(rs, lazyPropertyColumnAliases[j], session, entity); - if (InitializeLazyProperty(fieldName, entity, session, snapshot, j, propValue)) + if (InitializeLazyProperty(fieldName, entity, session, snapshot, j, propValue, uninitializedLazyProperties)) { result = propValue; } @@ -1359,7 +1361,8 @@ private object InitializeLazyPropertiesFromDatastore(string fieldName, object en } } - private object InitializeLazyPropertiesFromCache(string fieldName, object entity, ISessionImplementor session, EntityEntry entry, CacheEntry cacheEntry) + private object InitializeLazyPropertiesFromCache(string fieldName, object entity, ISessionImplementor session, EntityEntry entry, CacheEntry cacheEntry, + ISet uninitializedLazyProperties) { log.Debug("initializing lazy properties from second-level cache"); @@ -1369,7 +1372,7 @@ private object InitializeLazyPropertiesFromCache(string fieldName, object entity for (int j = 0; j < lazyPropertyNames.Length; j++) { object propValue = lazyPropertyTypes[j].Assemble(disassembledValues[lazyPropertyNumbers[j]], session, entity); - if (InitializeLazyProperty(fieldName, entity, session, snapshot, j, propValue)) + if (InitializeLazyProperty(fieldName, entity, session, snapshot, j, propValue, uninitializedLazyProperties)) { result = propValue; } @@ -1380,9 +1383,14 @@ private object InitializeLazyPropertiesFromCache(string fieldName, object entity return result; } - private bool InitializeLazyProperty(string fieldName, object entity, ISessionImplementor session, object[] snapshot, int j, object propValue) + private bool InitializeLazyProperty(string fieldName, object entity, ISessionImplementor session, object[] snapshot, int j, object propValue, + ISet uninitializedLazyProperties) { - SetPropertyValue(entity, lazyPropertyNumbers[j], propValue); + if (uninitializedLazyProperties.Contains(lazyPropertyNames[j])) + { + SetPropertyValue(entity, lazyPropertyNumbers[j], propValue); + } + if (snapshot != null) { // object have been loaded with setReadOnly(true); HHH-2236 @@ -3158,8 +3166,8 @@ public void Update(object id, object[] fields, int[] dirtyFields, bool hasDirtyC // in the process of being deleted. if (entry == null && !IsMutable) throw new InvalidOperationException("Updating immutable entity that is not in session yet!"); - - if (entityMetamodel.IsDynamicUpdate && dirtyFields != null) + + if (dirtyFields != null && (entityMetamodel.IsDynamicUpdate || HasDirtyLazyProperties(dirtyFields, obj))) { // For the case of dynamic-update="true", we need to generate the UPDATE SQL propsToUpdate = GetPropertiesToUpdate(dirtyFields, hasDirtyCollection); @@ -3208,6 +3216,13 @@ public void Update(object id, object[] fields, int[] dirtyFields, bool hasDirtyC } } + private bool HasDirtyLazyProperties(int[] dirtyFields, object obj) + { + // When having a dirty lazy property and the entity is not yet initialized we have to use a dynamic update for + // it even if it is disabled in order to have it updated. + return GetUninitializedLazyProperties(obj).Count > 0 && dirtyFields.Any(i => PropertyLaziness[i]); + } + public object Insert(object[] fields, object obj, ISessionImplementor session) { int span = TableSpan; @@ -4092,6 +4107,30 @@ public void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISes EntityTuplizer.AfterInitialize(entity, lazyPropertiesAreUnfetched, session); } + internal ISet GetUninitializedLazyProperties(object entity) + { + return EntityTuplizer.GetUninitializedLazyProperties(entity) ?? new HashSet(lazyPropertyNames); + } + + internal ISet GetUninitializedLazyProperties(object[] state) + { + if (!HasLazyProperties) + { + return CollectionHelper.EmptySet(); + } + + var uninitializedProperties = new HashSet(); + for (var j = 0; j < lazyPropertyNames.Length; j++) + { + if (state[lazyPropertyNumbers[j]] == LazyPropertyInitializer.UnfetchedProperty) + { + uninitializedProperties.Add(lazyPropertyNames[j]); + } + } + + return uninitializedProperties; + } + public virtual bool[] PropertyUpdateability { get { return entityMetamodel.PropertyUpdateability; } diff --git a/src/NHibernate/Persister/Entity/IEntityPersister.cs b/src/NHibernate/Persister/Entity/IEntityPersister.cs index 585222d4be4..b757d8a7cf8 100644 --- a/src/NHibernate/Persister/Entity/IEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/IEntityPersister.cs @@ -1,3 +1,4 @@ +using System; using NHibernate.Cache; using NHibernate.Cache.Entry; using NHibernate.Engine; @@ -6,6 +7,9 @@ using NHibernate.Tuple.Entity; using NHibernate.Type; using System.Collections; +using System.Collections.Generic; +using NHibernate.Intercept; +using NHibernate.Util; namespace NHibernate.Persister.Entity { @@ -619,5 +623,59 @@ public static int GetBatchSize(this IEntityPersister persister) return 1; } + + //6.0 TODO: Merge into IEntityPersister + internal static ISet GetUninitializedLazyProperties(this IEntityPersister persister, object entity) + { + if (persister is AbstractEntityPersister abstractEntityPersister) + { + return abstractEntityPersister.GetUninitializedLazyProperties(entity); + } + + if (!persister.HasUninitializedLazyProperties(entity)) + { + return CollectionHelper.EmptySet(); + } + + // Assume they are all uninitialized. + var result = new HashSet(); + for (var i = 0; i < persister.PropertyLaziness.Length; i++) + { + if (persister.PropertyLaziness[i]) + { + result.Add(persister.PropertyNames[i]); + } + } + + return result; + } + + /// + /// Get uninitialized lazy properties from entity state + /// + //6.0 TODO: Merge into IEntityPersister + public static ISet GetUninitializedLazyProperties(this IEntityPersister persister, object[] state) + { + if (persister is AbstractEntityPersister abstractEntityPersister) + { + return abstractEntityPersister.GetUninitializedLazyProperties(state); + } + + if (!persister.HasLazyProperties) + { + return CollectionHelper.EmptySet(); + } + + var result = new HashSet(); + for (var i = 0; i < persister.PropertyLaziness.Length; i++) + { + if (persister.PropertyLaziness[i] && state[i] == LazyPropertyInitializer.UnfetchedProperty) + { + result.Add(persister.PropertyNames[i]); + } + } + + return result; + } } } diff --git a/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs b/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs index 8198634cc71..4c2965f17d6 100644 --- a/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs +++ b/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs @@ -16,8 +16,10 @@ internal static class FieldInterceptorProxyBuilder private static readonly PropertyInfo AccessorTypeFieldInterceptorProperty = FieldInterceptorAccessorType.GetProperty(nameof(IFieldInterceptorAccessor.FieldInterceptor)); private static readonly System.Type FieldInterceptorType = typeof(IFieldInterceptor); + private static readonly System.Type FieldInterceptorExtensionsType = typeof(FieldInterceptorExtensions); private static readonly MethodInfo FieldInterceptorInterceptMethod = FieldInterceptorType.GetMethod(nameof(IFieldInterceptor.Intercept)); private static readonly MethodInfo FieldInterceptorMarkDirtyMethod = FieldInterceptorType.GetMethod(nameof(IFieldInterceptor.MarkDirty)); + private static readonly MethodInfo FieldInterceptorInterceptExtensionMethod = FieldInterceptorExtensionsType.GetMethod(nameof(FieldInterceptorExtensions.Intercept)); private static readonly System.Type AbstractFieldInterceptorType = typeof(AbstractFieldInterceptor); private static readonly FieldInfo AbstractFieldInterceptorInvokeImplementationField = AbstractFieldInterceptorType.GetField(nameof(AbstractFieldInterceptor.InvokeImplementation)); @@ -367,7 +369,7 @@ private static void ImplementSet(TypeBuilder typeBuilder, MethodInfo setter, Fie if (this.__fieldInterceptor != null) { this.__fieldInterceptor.MarkDirty(); - this.__fieldInterceptor.Intercept(this, , value); + this.__fieldInterceptor.Intercept(this, , value, true); } base.(value); */ @@ -387,7 +389,7 @@ private static void ImplementSet(TypeBuilder typeBuilder, MethodInfo setter, Fie IL.Emit(OpCodes.Ldfld, fieldInterceptorField); IL.Emit(OpCodes.Callvirt, FieldInterceptorMarkDirtyMethod); - // this.__fieldInterceptor.Intercept(this, , propValue); + // this.__fieldInterceptor.Intercept(this, , propValue, true); IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldfld, fieldInterceptorField); IL.Emit(OpCodes.Ldarg_0); @@ -396,7 +398,8 @@ private static void ImplementSet(TypeBuilder typeBuilder, MethodInfo setter, Fie var propertyType = setter.GetParameters()[0].ParameterType; if (propertyType.IsValueType) IL.Emit(OpCodes.Box, propertyType); - IL.Emit(OpCodes.Callvirt, FieldInterceptorInterceptMethod); + IL.Emit(OpCodes.Ldc_I4_1); + IL.EmitCall(OpCodes.Call, FieldInterceptorInterceptExtensionMethod, null); IL.Emit(OpCodes.Pop); // end if (this.__fieldInterceptor != null) diff --git a/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs index f4f6c4c0b60..626c1823213 100644 --- a/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs @@ -8,6 +8,7 @@ using NHibernate.Properties; using NHibernate.Proxy; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate.Tuple.Entity { @@ -242,6 +243,11 @@ public virtual bool HasUninitializedLazyProperties(object entity) return false; } + internal virtual ISet GetUninitializedLazyProperties(object entity) + { + return CollectionHelper.EmptySet(); + } + #endregion #region ITuplizer Members @@ -250,14 +256,14 @@ public virtual bool HasUninitializedLazyProperties(object entity) public virtual object[] GetPropertyValues(object entity) { - bool getAll = ShouldGetAllProperties(entity); + var uninitializedPropNames = GetUninitializedLazyProperties(entity); int span = entityMetamodel.PropertySpan; object[] result = new object[span]; for (int j = 0; j < span; j++) { StandardProperty property = entityMetamodel.Properties[j]; - if (getAll || !property.IsLazy) + if (!uninitializedPropNames.Contains(property.Name) || !property.IsLazy) { result[j] = getters[j].Get(entity); } diff --git a/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs index e0ddffcce79..6f697e2c04d 100644 --- a/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs @@ -1,5 +1,8 @@ +using System; using System.Collections; +using System.Collections.Generic; using NHibernate.Engine; +using NHibernate.Util; namespace NHibernate.Tuple.Entity { @@ -116,4 +119,23 @@ public interface IEntityTuplizer : ITuplizer /// True if uninitialized lazy properties were found; false otherwise. bool HasUninitializedLazyProperties(object entity); } + + internal static class EntityTuplizerExtensions + { + //6.0 TODO: Merge into IEntityTuplizer + internal static ISet GetUninitializedLazyProperties(this IEntityTuplizer entityTuplizer, object entity) + { + if (entityTuplizer is AbstractEntityTuplizer abstractEntityTuplizer) + { + return abstractEntityTuplizer.GetUninitializedLazyProperties(entity); + } + + if (!entityTuplizer.HasUninitializedLazyProperties(entity)) + { + return CollectionHelper.EmptySet(); + } + + return null; // The caller should use all lazy properties as the result + } + } } diff --git a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs index 9f57cba92cf..2deb5f607ab 100644 --- a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs @@ -231,9 +231,7 @@ public override void AfterInitialize(object entity, bool lazyPropertiesAreUnfetc { if (IsInstrumented && (EntityMetamodel.HasLazyProperties || EntityMetamodel.HasUnwrapProxyForProperties)) { - HashSet lazyProps = lazyPropertiesAreUnfetched && EntityMetamodel.HasLazyProperties ? lazyPropertyNames : null; - //TODO: if we support multiple fetch groups, we would need - // to clone the set of lazy properties! + HashSet lazyProps = lazyPropertiesAreUnfetched && EntityMetamodel.HasLazyProperties ? new HashSet(lazyPropertyNames) : null; FieldInterceptionHelper.InjectFieldInterceptor(entity, EntityName, this.MappedClass ,lazyProps, unwrapProxyPropertyNames, session); } } @@ -280,6 +278,22 @@ public override bool HasUninitializedLazyProperties(object entity) } } + internal override ISet GetUninitializedLazyProperties(object entity) + { + if (!EntityMetamodel.HasLazyProperties) + { + return CollectionHelper.EmptySet(); + } + + var interceptor = FieldInterceptionHelper.ExtractFieldInterceptor(entity); + if (interceptor == null) + { + return CollectionHelper.EmptySet(); + } + + return interceptor.GetUninitializedFields() ?? lazyPropertyNames; + } + public override bool IsLifecycleImplementor { get { return islifecycleImplementor; } diff --git a/src/NHibernate/Util/CollectionHelper.cs b/src/NHibernate/Util/CollectionHelper.cs index bda783cca42..52e1ef8b12a 100644 --- a/src/NHibernate/Util/CollectionHelper.cs +++ b/src/NHibernate/Util/CollectionHelper.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Iesi.Collections.Generic; namespace NHibernate.Util { @@ -220,6 +221,8 @@ public static IDictionary EmptyDictionary() return EmptyMapClass.Instance; } + internal static ISet EmptySet() => EmptyReadOnlySet.Instance; + public static readonly ICollection EmptyCollection = EmptyMap; // Since v5 [Obsolete("It has no more usages in NHibernate and will be removed in a future version.")] @@ -462,6 +465,12 @@ public object Current #endregion } + [Serializable] + private class EmptyReadOnlySet + { + public static readonly ISet Instance = new ReadOnlySet(new HashSet()); + } + /// /// A read-only dictionary that is always empty and permits lookup by key. ///