From bd1613069ad01cebdf6cbb12adb89fa22500071a Mon Sep 17 00:00:00 2001 From: maca88 Date: Fri, 28 Dec 2018 03:18:25 +0100 Subject: [PATCH 1/3] Port Hibernate's lazy attribute fetch groups --- .../Async/CustomPersister.cs | 5 +- src/NHibernate.DomainModel/CustomPersister.cs | 5 +- .../Async/CacheTest/SerializationFixture.cs | 4 +- .../Async/LazyGroup/LazyGroupFixture.cs | 230 ++++++++++++++++++ .../CacheTest/SerializationFixture.cs | 4 +- src/NHibernate.Test/LazyGroup/Address.cs | 17 ++ .../LazyGroup/LazyGroupFixture.cs | 219 +++++++++++++++++ .../LazyGroup/Mappings.hbm.xml | 20 ++ src/NHibernate.Test/LazyGroup/Person.cs | 23 ++ src/NHibernate/Action/EntityInsertAction.cs | 2 +- src/NHibernate/Action/EntityUpdateAction.cs | 2 +- .../Async/Action/EntityInsertAction.cs | 2 +- .../Async/Action/EntityUpdateAction.cs | 2 +- .../Async/Cache/Entry/CacheEntry.cs | 20 ++ .../Async/Engine/IPersistenceContext.cs | 3 + src/NHibernate/Async/Engine/TwoPhaseLoad.cs | 6 +- .../AbstractReassociateEventListener.cs | 3 +- .../Default/AbstractSaveEventListener.cs | 5 +- .../Default/DefaultDeleteEventListener.cs | 1 - .../Event/Default/DefaultLoadEventListener.cs | 7 +- .../Default/DefaultReplicateEventListener.cs | 3 +- .../DefaultSaveOrUpdateEventListener.cs | 3 +- src/NHibernate/Async/Loader/Loader.cs | 29 ++- .../Entity/AbstractEntityPersister.cs | 4 +- src/NHibernate/Async/Type/TypeHelper.cs | 66 ++++- .../Bytecode/IBytecodeEnhancementMetadata.cs | 11 +- .../Bytecode/LazyPropertiesMetadata.cs | 59 ++++- .../Bytecode/LazyPropertyDescriptor.cs | 16 +- src/NHibernate/Cache/Entry/CacheEntry.cs | 22 +- .../Cache/Entry/StructuredCacheEntry.cs | 5 +- .../Cfg/MappingSchema/Hbm.generated.cs | 8 + .../Cfg/MappingSchema/HbmProperty.cs | 2 + .../MappingSchema/IEntityPropertyMapping.cs | 21 +- .../Cfg/XmlHbmBinding/PropertiesBinder.cs | 1 + src/NHibernate/Engine/EntityEntry.cs | 27 +- src/NHibernate/Engine/IPersistenceContext.cs | 100 ++++++++ .../Engine/StatefulPersistenceContext.cs | 32 ++- src/NHibernate/Engine/TwoPhaseLoad.cs | 52 +++- .../AbstractReassociateEventListener.cs | 3 +- .../Default/AbstractSaveEventListener.cs | 7 +- .../Default/DefaultDeleteEventListener.cs | 1 - .../Event/Default/DefaultLoadEventListener.cs | 7 +- .../Default/DefaultReplicateEventListener.cs | 3 +- .../DefaultSaveOrUpdateEventListener.cs | 3 +- .../Intercept/AbstractFieldInterceptor.cs | 17 +- src/NHibernate/Intercept/IFieldInterceptor.cs | 4 + src/NHibernate/Loader/Loader.cs | 28 ++- .../Mapping/ByCode/IComponentMapper.cs | 29 ++- .../Mapping/ByCode/IPropertyMapper.cs | 13 + .../Mapping/ByCode/Impl/ComponentMapper.cs | 7 +- .../CustomizersImpl/ComponentCustomizer.cs | 7 +- .../ComponentElementCustomizer.cs | 7 +- .../Mapping/ByCode/Impl/PropertyMapper.cs | 5 + src/NHibernate/Mapping/Property.cs | 2 + .../Entity/AbstractEntityPersister.cs | 158 ++++++++++-- .../Persister/Entity/IEntityPersister.cs | 17 ++ .../Tuple/Entity/AbstractEntityTuplizer.cs | 11 +- .../BytecodeEnhancementMetadataNonPocoImpl.cs | 8 +- .../BytecodeEnhancementMetadataPocoImpl.cs | 11 +- .../Tuple/Entity/IEntityTuplizer.cs | 26 ++ .../Tuple/Entity/PocoEntityInstantiator.cs | 2 +- .../Tuple/Entity/PocoEntityTuplizer.cs | 18 +- src/NHibernate/Tuple/StandardProperty.cs | 4 +- src/NHibernate/Type/TypeHelper.cs | 52 +++- src/NHibernate/nhibernate-mapping.xsd | 2 + 65 files changed, 1333 insertions(+), 160 deletions(-) create mode 100644 src/NHibernate.Test/Async/LazyGroup/LazyGroupFixture.cs create mode 100644 src/NHibernate.Test/LazyGroup/Address.cs create mode 100644 src/NHibernate.Test/LazyGroup/LazyGroupFixture.cs create mode 100644 src/NHibernate.Test/LazyGroup/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/LazyGroup/Person.cs diff --git a/src/NHibernate.DomainModel/Async/CustomPersister.cs b/src/NHibernate.DomainModel/Async/CustomPersister.cs index 104cc514c36..9a671f2443b 100644 --- a/src/NHibernate.DomainModel/Async/CustomPersister.cs +++ b/src/NHibernate.DomainModel/Async/CustomPersister.cs @@ -85,9 +85,8 @@ public async Task LoadAsync(object id, object optionalObject, LockMode l if (obj != null) { clone = (Custom)obj.Clone(); - TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, false, - session); - TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, false, session); + TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, session); + TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, session); await (TwoPhaseLoad.InitializeEntityAsync(clone, false, session, new PreLoadEvent((IEventSource) session), new PostLoadEvent((IEventSource) session), cancellationToken)); } diff --git a/src/NHibernate.DomainModel/CustomPersister.cs b/src/NHibernate.DomainModel/CustomPersister.cs index 04ce8d6625b..a4f91c22644 100644 --- a/src/NHibernate.DomainModel/CustomPersister.cs +++ b/src/NHibernate.DomainModel/CustomPersister.cs @@ -310,9 +310,8 @@ public object Load(object id, object optionalObject, LockMode lockMode, ISession if (obj != null) { clone = (Custom)obj.Clone(); - TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, false, - session); - TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, false, session); + TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, session); + TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, session); TwoPhaseLoad.InitializeEntity(clone, false, session, new PreLoadEvent((IEventSource) session), new PostLoadEvent((IEventSource) session)); } diff --git a/src/NHibernate.Test/Async/CacheTest/SerializationFixture.cs b/src/NHibernate.Test/Async/CacheTest/SerializationFixture.cs index 4c16e4bf28a..85d822288c9 100644 --- a/src/NHibernate.Test/Async/CacheTest/SerializationFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/SerializationFixture.cs @@ -142,8 +142,7 @@ private CacheEntry CreateCacheEntry() { DisassembledState = GetAllKnownTypeValues(), Version = 55, - Subclass = "Test", - AreLazyPropertiesUnfetched = true + Subclass = "Test" }; } @@ -240,7 +239,6 @@ private void CheckCacheEntry(CacheEntry original, CacheEntry copy) Assert.That(copy.Version, Is.EqualTo(original.Version)); Assert.That(copy.Version, Is.TypeOf(original.Version.GetType())); Assert.That(copy.Subclass, Is.EqualTo(original.Subclass)); - Assert.That(copy.AreLazyPropertiesUnfetched, Is.EqualTo(original.AreLazyPropertiesUnfetched)); for (var i = 0; i < copy.DisassembledState.Length; i++) { Assert.That(copy.DisassembledState[i], Is.TypeOf(original.DisassembledState[i].GetType())); diff --git a/src/NHibernate.Test/Async/LazyGroup/LazyGroupFixture.cs b/src/NHibernate.Test/Async/LazyGroup/LazyGroupFixture.cs new file mode 100644 index 00000000000..149e7b001f7 --- /dev/null +++ b/src/NHibernate.Test/Async/LazyGroup/LazyGroupFixture.cs @@ -0,0 +1,230 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Cache; +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.LazyGroup +{ + using System.Threading; + [TestFixture] + public class LazyGroupFixtureAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string[] Mappings => new[] { "LazyGroup.Mappings.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.Properties[Environment.CacheProvider] = typeof(HashtableCacheProvider).AssemblyQualifiedName; + configuration.Properties[Environment.UseSecondLevelCache] = "true"; + configuration.Properties[Environment.GenerateStatistics] = "true"; + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + for (var i = 1; i <= 5; i++) + { + var person = GeneratePerson(i); + s.Save(person); + } + + tx.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from Person").ExecuteUpdate(); + tx.Commit(); + } + } + + [Test] + public async Task TestGroupsAsync() + { + using (var s = OpenSession()) + { + var person = await (s.GetAsync(1)); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Name"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + + var nickName = person.NickName; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(nickName, Is.EqualTo("NickName1")); + + var address = person.Address; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(address.City, Is.EqualTo("City1")); + Assert.That(address.Street, Is.EqualTo("Street1")); + Assert.That(address.PostCode, Is.EqualTo(1001)); + + var image = person.Image; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(person.Image, Has.Length.EqualTo(1)); + + var age = person.Age; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.True); + Assert.That(person.Age, Is.EqualTo(1)); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestUpdateAsync(bool fetchBeforeUpdate, CancellationToken cancellationToken = default(CancellationToken)) + { + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.GetAsync(1, cancellationToken)); + if (fetchBeforeUpdate) + { + var nickName = person.NickName; + } + + person.NickName = "test"; + + await (tx.CommitAsync(cancellationToken)); + } + + Assert.That(Sfi.Statistics.EntityUpdateCount, Is.EqualTo(1)); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.GetAsync(1, cancellationToken)); + Assert.That(person.NickName, Is.EqualTo("test")); + + person.NickName = "NickName1"; // reset name + + await (tx.CommitAsync(cancellationToken)); + } + } + + [Test] + public async Task TestCacheAsync() + { + var persister = Sfi.GetEntityPersister(typeof(Person).FullName); + var cache = (HashtableCache) persister.Cache.Cache; + await (cache.ClearAsync(CancellationToken.None)); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.GetAsync(1)); + + var nickName = person.NickName; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(nickName, Is.EqualTo("NickName1")); + + await (tx.CommitAsync()); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.GetAsync(1)); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(person.NickName, Is.EqualTo("NickName1")); + + await (tx.CommitAsync()); + } + } + + [Test] + public async Task TestInitializeFromCacheAsync() + { + var persister = Sfi.GetEntityPersister(typeof(Person).FullName); + var cache = (HashtableCache) persister.Cache.Cache; + await (cache.ClearAsync(CancellationToken.None)); + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.GetAsync(1)); + + await (InitializeImageAsync()); + + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + + var image = person.Image; // Should be initialized from cache + + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(image, Has.Length.EqualTo(1)); + + await (tx.CommitAsync()); + } + } + + private async Task InitializeImageAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = await (s.GetAsync(1, cancellationToken)); + var image = person.Image; + + await (tx.CommitAsync(cancellationToken)); + } + } + + private static Person GeneratePerson(int i) + { + return new Person + { + Id = i, + Name = $"Person{i}", + Address = new Address + { + City = $"City{i}", + PostCode = 1000+i, + Street = $"Street{i}" + }, + Image = new byte[i], + NickName = $"NickName{i}" + }; + + } + } +} diff --git a/src/NHibernate.Test/CacheTest/SerializationFixture.cs b/src/NHibernate.Test/CacheTest/SerializationFixture.cs index 7d8166a297c..eb21721f45b 100644 --- a/src/NHibernate.Test/CacheTest/SerializationFixture.cs +++ b/src/NHibernate.Test/CacheTest/SerializationFixture.cs @@ -131,8 +131,7 @@ private CacheEntry CreateCacheEntry() { DisassembledState = GetAllKnownTypeValues(), Version = 55, - Subclass = "Test", - AreLazyPropertiesUnfetched = true + Subclass = "Test" }; } @@ -229,7 +228,6 @@ private void CheckCacheEntry(CacheEntry original, CacheEntry copy) Assert.That(copy.Version, Is.EqualTo(original.Version)); Assert.That(copy.Version, Is.TypeOf(original.Version.GetType())); Assert.That(copy.Subclass, Is.EqualTo(original.Subclass)); - Assert.That(copy.AreLazyPropertiesUnfetched, Is.EqualTo(original.AreLazyPropertiesUnfetched)); for (var i = 0; i < copy.DisassembledState.Length; i++) { Assert.That(copy.DisassembledState[i], Is.TypeOf(original.DisassembledState[i].GetType())); diff --git a/src/NHibernate.Test/LazyGroup/Address.cs b/src/NHibernate.Test/LazyGroup/Address.cs new file mode 100644 index 00000000000..5699d3db61f --- /dev/null +++ b/src/NHibernate.Test/LazyGroup/Address.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.LazyGroup +{ + public class Address + { + public string City { get; set; } + + public string Street { get; set; } + + public int PostCode { get; set; } + } +} diff --git a/src/NHibernate.Test/LazyGroup/LazyGroupFixture.cs b/src/NHibernate.Test/LazyGroup/LazyGroupFixture.cs new file mode 100644 index 00000000000..82b51aa0c4a --- /dev/null +++ b/src/NHibernate.Test/LazyGroup/LazyGroupFixture.cs @@ -0,0 +1,219 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Cache; +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.LazyGroup +{ + [TestFixture] + public class LazyGroupFixture : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string[] Mappings => new[] { "LazyGroup.Mappings.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.Properties[Environment.CacheProvider] = typeof(HashtableCacheProvider).AssemblyQualifiedName; + configuration.Properties[Environment.UseSecondLevelCache] = "true"; + configuration.Properties[Environment.GenerateStatistics] = "true"; + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + for (var i = 1; i <= 5; i++) + { + var person = GeneratePerson(i); + s.Save(person); + } + + tx.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from Person").ExecuteUpdate(); + tx.Commit(); + } + } + + [Test] + public void TestGroups() + { + using (var s = OpenSession()) + { + var person = s.Get(1); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Name"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + + var nickName = person.NickName; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(nickName, Is.EqualTo("NickName1")); + + var address = person.Address; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(address.City, Is.EqualTo("City1")); + Assert.That(address.Street, Is.EqualTo("Street1")); + Assert.That(address.PostCode, Is.EqualTo(1001)); + + var image = person.Image; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(person.Image, Has.Length.EqualTo(1)); + + var age = person.Age; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.True); + Assert.That(person.Age, Is.EqualTo(1)); + } + } + + [TestCase(true)] + [TestCase(false)] + public void TestUpdate(bool fetchBeforeUpdate) + { + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.Get(1); + if (fetchBeforeUpdate) + { + var nickName = person.NickName; + } + + person.NickName = "test"; + + tx.Commit(); + } + + Assert.That(Sfi.Statistics.EntityUpdateCount, Is.EqualTo(1)); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.Get(1); + Assert.That(person.NickName, Is.EqualTo("test")); + + person.NickName = "NickName1"; // reset name + + tx.Commit(); + } + } + + [Test] + public void TestCache() + { + var persister = Sfi.GetEntityPersister(typeof(Person).FullName); + var cache = (HashtableCache) persister.Cache.Cache; + cache.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.Get(1); + + var nickName = person.NickName; + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(nickName, Is.EqualTo("NickName1")); + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.Get(1); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(person.NickName, Is.EqualTo("NickName1")); + + tx.Commit(); + } + } + + [Test] + public void TestInitializeFromCache() + { + var persister = Sfi.GetEntityPersister(typeof(Person).FullName); + var cache = (HashtableCache) persister.Cache.Cache; + cache.Clear(); + Sfi.Statistics.Clear(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.Get(1); + + InitializeImage(); + + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + + var image = person.Image; // Should be initialized from cache + + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False); + Assert.That(image, Has.Length.EqualTo(1)); + + tx.Commit(); + } + } + + private void InitializeImage() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var person = s.Get(1); + var image = person.Image; + + tx.Commit(); + } + } + + private static Person GeneratePerson(int i) + { + return new Person + { + Id = i, + Name = $"Person{i}", + Address = new Address + { + City = $"City{i}", + PostCode = 1000+i, + Street = $"Street{i}" + }, + Image = new byte[i], + NickName = $"NickName{i}" + }; + + } + } +} diff --git a/src/NHibernate.Test/LazyGroup/Mappings.hbm.xml b/src/NHibernate.Test/LazyGroup/Mappings.hbm.xml new file mode 100644 index 00000000000..68376553227 --- /dev/null +++ b/src/NHibernate.Test/LazyGroup/Mappings.hbm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/LazyGroup/Person.cs b/src/NHibernate.Test/LazyGroup/Person.cs new file mode 100644 index 00000000000..0c7d57ae49b --- /dev/null +++ b/src/NHibernate.Test/LazyGroup/Person.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.LazyGroup +{ + public class Person + { + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual string NickName { get; set; } + + public virtual Address Address { get; set; } + + public virtual byte[] Image { get; set; } + + public virtual long Age { get; protected set; } + } +} diff --git a/src/NHibernate/Action/EntityInsertAction.cs b/src/NHibernate/Action/EntityInsertAction.cs index 6f4f9a71a45..71250f48373 100644 --- a/src/NHibernate/Action/EntityInsertAction.cs +++ b/src/NHibernate/Action/EntityInsertAction.cs @@ -74,7 +74,7 @@ public override void Execute() if (IsCachePutEnabled(persister)) { - CacheEntry ce = CacheEntry.Create(State, persister, persister.HasUninitializedLazyProperties(instance), version, session, instance); + CacheEntry ce = CacheEntry.Create(State, persister, version, session, instance); cacheEntry = persister.CacheEntryStructure.Structure(ce); CacheKey ck = Session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); diff --git a/src/NHibernate/Action/EntityUpdateAction.cs b/src/NHibernate/Action/EntityUpdateAction.cs index dfac0d86120..8b3ded4b24d 100644 --- a/src/NHibernate/Action/EntityUpdateAction.cs +++ b/src/NHibernate/Action/EntityUpdateAction.cs @@ -114,7 +114,7 @@ public override void Execute() } else { - CacheEntry ce = CacheEntry.Create(state, persister, persister.HasUninitializedLazyProperties(instance), nextVersion, Session, instance); + CacheEntry ce = CacheEntry.Create(state, persister, nextVersion, Session, instance); cacheEntry = persister.CacheEntryStructure.Structure(ce); bool put = persister.Cache.Update(ck, cacheEntry, nextVersion, previousVersion); diff --git a/src/NHibernate/Async/Action/EntityInsertAction.cs b/src/NHibernate/Async/Action/EntityInsertAction.cs index 4a0115dc06a..dced95776c3 100644 --- a/src/NHibernate/Async/Action/EntityInsertAction.cs +++ b/src/NHibernate/Async/Action/EntityInsertAction.cs @@ -70,7 +70,7 @@ public override async Task ExecuteAsync(CancellationToken cancellationToken) if (IsCachePutEnabled(persister)) { - CacheEntry ce = await (CacheEntry.CreateAsync(State, persister, persister.HasUninitializedLazyProperties(instance), version, session, instance, cancellationToken)).ConfigureAwait(false); + CacheEntry ce = await (CacheEntry.CreateAsync(State, persister, version, session, instance, cancellationToken)).ConfigureAwait(false); cacheEntry = persister.CacheEntryStructure.Structure(ce); CacheKey ck = Session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); diff --git a/src/NHibernate/Async/Action/EntityUpdateAction.cs b/src/NHibernate/Async/Action/EntityUpdateAction.cs index 33f1cab8fdc..89b065f40aa 100644 --- a/src/NHibernate/Async/Action/EntityUpdateAction.cs +++ b/src/NHibernate/Async/Action/EntityUpdateAction.cs @@ -99,7 +99,7 @@ public override async Task ExecuteAsync(CancellationToken cancellationToken) } else { - CacheEntry ce = await (CacheEntry.CreateAsync(state, persister, persister.HasUninitializedLazyProperties(instance), nextVersion, Session, instance, cancellationToken)).ConfigureAwait(false); + CacheEntry ce = await (CacheEntry.CreateAsync(state, persister, nextVersion, Session, instance, cancellationToken)).ConfigureAwait(false); cacheEntry = persister.CacheEntryStructure.Structure(ce); bool put = await (persister.Cache.UpdateAsync(ck, cacheEntry, nextVersion, previousVersion, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Cache/Entry/CacheEntry.cs b/src/NHibernate/Async/Cache/Entry/CacheEntry.cs index 4fd9c71f288..e6aca27123e 100644 --- a/src/NHibernate/Async/Cache/Entry/CacheEntry.cs +++ b/src/NHibernate/Async/Cache/Entry/CacheEntry.cs @@ -22,6 +22,8 @@ namespace NHibernate.Cache.Entry public sealed partial class CacheEntry { + // Since 5.3 + [Obsolete("Use the overload without unfetched parameter instead.")] public static async Task CreateAsync(object[] state, IEntityPersister persister, bool unfetched, object version, ISessionImplementor session, object owner, CancellationToken cancellationToken) { @@ -41,6 +43,24 @@ public static async Task CreateAsync(object[] state, IEntityPersiste }; } + public static async Task CreateAsync(object[] state, IEntityPersister persister, object version, + ISessionImplementor session, object owner, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return new CacheEntry + { + //disassembled state gets put in a new array (we write to cache by value!) + DisassembledState = await (TypeHelper.DisassembleAsync( + state, + persister.PropertyTypes, + persister.IsLazyPropertiesCacheable ? null : persister.PropertyLaziness, + session, + owner, cancellationToken)).ConfigureAwait(false), + Subclass = persister.EntityName, + Version = version + }; + } + public Task AssembleAsync(object instance, object id, IEntityPersister persister, IInterceptor interceptor, ISessionImplementor session, CancellationToken cancellationToken) { diff --git a/src/NHibernate/Async/Engine/IPersistenceContext.cs b/src/NHibernate/Async/Engine/IPersistenceContext.cs index 82b2a48d3d4..3807b2649a9 100644 --- a/src/NHibernate/Async/Engine/IPersistenceContext.cs +++ b/src/NHibernate/Async/Engine/IPersistenceContext.cs @@ -8,10 +8,13 @@ //------------------------------------------------------------------------------ +using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using NHibernate.Collection; using NHibernate.Engine.Loading; +using NHibernate.Intercept; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; using NHibernate.Proxy; diff --git a/src/NHibernate/Async/Engine/TwoPhaseLoad.cs b/src/NHibernate/Async/Engine/TwoPhaseLoad.cs index bd8948df083..6b9b84048d2 100644 --- a/src/NHibernate/Async/Engine/TwoPhaseLoad.cs +++ b/src/NHibernate/Async/Engine/TwoPhaseLoad.cs @@ -28,7 +28,7 @@ namespace NHibernate.Engine using System.Threading; public static partial class TwoPhaseLoad { - + /// /// Perform the second step of 2-phase load. Fully initialize the entity instance. /// After processing a JDBC result set, we "resolve" all the associations @@ -124,7 +124,7 @@ internal static async Task InitializeEntityAsync(object entity, bool readOnly, I object version = Versioning.GetVersion(hydratedState, persister); CacheEntry entry = - await (CacheEntry.CreateAsync(hydratedState, persister, entityEntry.LoadedWithLazyPropertiesUnfetched, version, session, entity, cancellationToken)).ConfigureAwait(false); + await (CacheEntry.CreateAsync(hydratedState, persister, version, session, entity, cancellationToken)).ConfigureAwait(false); CacheKey cacheKey = session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); if (cacheBatchingHandler != null && persister.IsBatchLoadable) @@ -184,7 +184,7 @@ internal static async Task InitializeEntityAsync(object entity, bool readOnly, I persistenceContext.SetEntryStatus(entityEntry, Status.Loaded); } - persister.AfterInitialize(entity, entityEntry.LoadedWithLazyPropertiesUnfetched, session); + persister.AfterInitialize(entity, session); if (session.IsEventSource) { diff --git a/src/NHibernate/Async/Event/Default/AbstractReassociateEventListener.cs b/src/NHibernate/Async/Event/Default/AbstractReassociateEventListener.cs index 30b74e9089b..9add7a34910 100644 --- a/src/NHibernate/Async/Event/Default/AbstractReassociateEventListener.cs +++ b/src/NHibernate/Async/Event/Default/AbstractReassociateEventListener.cs @@ -59,8 +59,7 @@ protected async Task ReassociateAsync(AbstractEvent @event, object LockMode.None, true, persister, - false, - true); + false); await (new OnLockVisitor(source, id, entity).ProcessAsync(entity, persister, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs b/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs index d58c8ae8377..985ead87deb 100644 --- a/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs +++ b/src/NHibernate/Async/Event/Default/AbstractSaveEventListener.cs @@ -191,7 +191,7 @@ protected virtual async Task PerformSaveOrReplicateAsync(object entity, // Put a placeholder in entries, so we don't recurse back and try to save() the // same object again. QUESTION: should this be done before onSave() is called? // likewise, should it be done before onUpdate()? - source.PersistenceContext.AddEntry(entity, Status.Saving, null, null, id, null, LockMode.Write, useIdentityColumn, persister, false, false); + source.PersistenceContext.AddEntry(entity, Status.Saving, null, null, id, null, LockMode.Write, useIdentityColumn, persister, false); await (CascadeBeforeSaveAsync(source, persister, entity, anything, cancellationToken)).ConfigureAwait(false); @@ -253,8 +253,7 @@ protected virtual async Task PerformSaveOrReplicateAsync(object entity, LockMode.Write, useIdentityColumn, persister, - VersionIncrementDisabled, - false); + VersionIncrementDisabled); //source.getPersistenceContext().removeNonExist( new EntityKey( id, persister, source.getEntityMode() ) ); if (!useIdentityColumn) diff --git a/src/NHibernate/Async/Event/Default/DefaultDeleteEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultDeleteEventListener.cs index fec2ff09a11..be81efc248f 100644 --- a/src/NHibernate/Async/Event/Default/DefaultDeleteEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultDeleteEventListener.cs @@ -93,7 +93,6 @@ public virtual async Task OnDeleteAsync(DeleteEvent @event, ISet transie LockMode.None, true, persister, - false, false); } else diff --git a/src/NHibernate/Async/Event/Default/DefaultLoadEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultLoadEventListener.cs index 38e4a7a1c7f..20a32b8518a 100644 --- a/src/NHibernate/Async/Event/Default/DefaultLoadEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultLoadEventListener.cs @@ -505,7 +505,7 @@ private async Task AssembleCacheEntryAsync(CacheEntry entry, object id, // make it circular-reference safe EntityKey entityKey = session.GenerateEntityKey(id, subclassPersister); - TwoPhaseLoad.AddUninitializedCachedEntity(entityKey, result, subclassPersister, LockMode.None, entry.AreLazyPropertiesUnfetched, entry.Version, session); + TwoPhaseLoad.AddUninitializedCachedEntity(entityKey, result, subclassPersister, LockMode.None, entry.Version, session); IType[] types = subclassPersister.PropertyTypes; object[] values = await (entry.AssembleAsync(result, id, subclassPersister, session.Interceptor, session, cancellationToken)).ConfigureAwait(false); // intializes result by side-effect @@ -543,10 +543,9 @@ private async Task AssembleCacheEntryAsync(CacheEntry entry, object id, LockMode.None, true, subclassPersister, - false, - entry.AreLazyPropertiesUnfetched); + false); - subclassPersister.AfterInitialize(result, entry.AreLazyPropertiesUnfetched, session); + subclassPersister.AfterInitialize(result, session); await (persistenceContext.InitializeNonLazyCollectionsAsync(cancellationToken)).ConfigureAwait(false); // upgrade the lock if necessary: //lock(result, lockMode); diff --git a/src/NHibernate/Async/Event/Default/DefaultReplicateEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultReplicateEventListener.cs index 04f646f475c..124f45d1755 100644 --- a/src/NHibernate/Async/Event/Default/DefaultReplicateEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultReplicateEventListener.cs @@ -127,8 +127,7 @@ private async Task PerformReplicationAsync(object entity, object id, object vers LockMode.None, true, persister, - true, - false); + true); await (CascadeAfterReplicateAsync(entity, persister, replicationMode, source, cancellationToken)).ConfigureAwait(false); } diff --git a/src/NHibernate/Async/Event/Default/DefaultSaveOrUpdateEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultSaveOrUpdateEventListener.cs index 5bb29def684..99aa1b95e7a 100644 --- a/src/NHibernate/Async/Event/Default/DefaultSaveOrUpdateEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultSaveOrUpdateEventListener.cs @@ -223,8 +223,7 @@ protected virtual async Task PerformUpdateAsync(SaveOrUpdateEvent @event, object LockMode.None, true, persister, - false, - true); + false); //persister.AfterReassociate(entity, source); TODO H3.2 not ported diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 9d37f545940..bf7d06b1046 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -785,15 +785,10 @@ private async Task UpdateLazyPropertiesFromResultSetAsync(DbDataReader rs, int i Action cacheBatchingHandler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (!entry.LoadedWithLazyPropertiesUnfetched) - { - return; // All lazy properties were already loaded - } - - var eagerPropertyFetch = IsEagerPropertyFetchEnabled(i); + var fetchAllProperties = IsEagerPropertyFetchEnabled(i); var fetchLazyProperties = GetFetchLazyProperties(i); - if (!eagerPropertyFetch && fetchLazyProperties == null) + if (!fetchAllProperties && fetchLazyProperties == null) { return; // No lazy properties were loaded } @@ -826,11 +821,19 @@ private async Task UpdateLazyPropertiesFromResultSetAsync(DbDataReader rs, int i ? EntityAliases[i].SuffixedPropertyAliases : GetSubclassEntityAliases(i, persister); - if (!await (persister.InitializeLazyPropertiesAsync(rs, id, obj, rootPersister, cols, updateLazyProperties, eagerPropertyFetch, session, cancellationToken)).ConfigureAwait(false)) + if (!await (persister.InitializeLazyPropertiesAsync(rs, id, obj, rootPersister, cols, updateLazyProperties, fetchAllProperties, session, cancellationToken)).ConfigureAwait(false)) { return; } + await (UpdateCacheForEntityAsync(obj, id, entry, persister, session, cacheBatchingHandler, cancellationToken)).ConfigureAwait(false); + } + + internal static async Task UpdateCacheForEntityAsync( + object obj, object id, EntityEntry entry, IEntityPersister persister, ISessionImplementor session, + Action cacheBatchingHandler, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); if (entry.Status == Status.Loading || !persister.HasCache || !session.CacheMode.HasFlag(CacheMode.Put) || !persister.IsLazyPropertiesCacheable) { @@ -845,7 +848,7 @@ private async Task UpdateLazyPropertiesFromResultSetAsync(DbDataReader rs, int i var factory = session.Factory; var state = persister.GetPropertyValues(obj); var version = Versioning.GetVersion(state, persister); - var cacheEntry = await (CacheEntry.CreateAsync(state, persister, entry.LoadedWithLazyPropertiesUnfetched, version, session, obj, cancellationToken)).ConfigureAwait(false); + var cacheEntry = await (CacheEntry.CreateAsync(state, persister, version, session, obj, cancellationToken)).ConfigureAwait(false); var cacheKey = session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); if (cacheBatchingHandler != null && persister.IsBatchLoadable) @@ -895,23 +898,23 @@ private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, st Log.Debug("Initializing object from DataReader: {0}", MessageHelper.InfoString(persister, id)); } - bool eagerPropertyFetch = IsEagerPropertyFetchEnabled(i); + bool fetchAllProperties = IsEagerPropertyFetchEnabled(i); var eagerFetchProperties = GetFetchLazyProperties(i); // add temp entry so that the next step is circular-reference // safe - only needed because some types don't take proper // advantage of two-phase-load (esp. components) - TwoPhaseLoad.AddUninitializedEntity(key, obj, persister, lockMode, !eagerPropertyFetch, session); + TwoPhaseLoad.AddUninitializedEntity(key, obj, persister, lockMode, session); string[][] cols = persister == rootPersister ? EntityAliases[i].SuffixedPropertyAliases : GetSubclassEntityAliases(i, persister); - object[] values = await (persister.HydrateAsync(rs, id, obj, cols, eagerFetchProperties, eagerPropertyFetch, session, cancellationToken)).ConfigureAwait(false); + object[] values = await (persister.HydrateAsync(rs, id, obj, cols, eagerFetchProperties, fetchAllProperties, session, cancellationToken)).ConfigureAwait(false); object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; - TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, !eagerPropertyFetch, session); + TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, session); } /// diff --git a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs index 9746bba3833..43d5f59da49 100644 --- a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs @@ -1131,7 +1131,7 @@ public virtual async Task FindDirtyAsync(object[] currentState, object[] { cancellationToken.ThrowIfCancellationRequested(); int[] props = await (TypeHelper.FindDirtyAsync( - entityMetamodel.Properties, currentState, previousState, propertyColumnUpdateable, HasUninitializedLazyProperties(entity), session, cancellationToken)).ConfigureAwait(false); + entityMetamodel.Properties, currentState, previousState, propertyColumnUpdateable, session, cancellationToken)).ConfigureAwait(false); if (props == null) { @@ -1148,7 +1148,7 @@ public virtual async Task FindModifiedAsync(object[] old, object[] curren { cancellationToken.ThrowIfCancellationRequested(); int[] props = await (TypeHelper.FindModifiedAsync( - entityMetamodel.Properties, current, old, propertyColumnUpdateable, HasUninitializedLazyProperties(entity), session, cancellationToken)).ConfigureAwait(false); + entityMetamodel.Properties, current, old, propertyColumnUpdateable, session, cancellationToken)).ConfigureAwait(false); if (props == null) { return null; diff --git a/src/NHibernate/Async/Type/TypeHelper.cs b/src/NHibernate/Async/Type/TypeHelper.cs index 4fc408ea24c..6208289b5b9 100644 --- a/src/NHibernate/Async/Type/TypeHelper.cs +++ b/src/NHibernate/Async/Type/TypeHelper.cs @@ -262,12 +262,39 @@ public static async Task ReplaceAssociationsAsync(object[] original, o /// The session from which the dirty check request originated. /// A cancellation token that can be used to cancel the work /// Array containing indices of the dirty properties, or null if no properties considered dirty. - public static async Task FindDirtyAsync(StandardProperty[] properties, + // Since 5.3 + [Obsolete("Use overload without anyUninitializedProperties parameter instead")] + public static Task FindDirtyAsync(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, bool anyUninitializedProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return FindDirtyAsync(properties, currentState, previousState, includeColumns, session, cancellationToken); + } + + /// + /// Determine if any of the given field values are dirty, returning an array containing + /// indices of the dirty fields. + /// If it is determined that no fields are dirty, null is returned. + /// + /// The property definitions + /// The current state of the entity + /// The baseline state of the entity + /// Columns to be included in the dirty checking, per property + /// The session from which the dirty check request originated. + /// A cancellation token that can be used to cancel the work + /// Array containing indices of the dirty properties, or null if no properties considered dirty. + public static async Task FindDirtyAsync(StandardProperty[] properties, + object[] currentState, + object[] previousState, + bool[][] includeColumns, + ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); int[] results = null; @@ -276,7 +303,7 @@ public static async Task FindDirtyAsync(StandardProperty[] properties, for (int i = 0; i < span; i++) { - var dirty = await (DirtyAsync(properties, currentState, previousState, includeColumns, anyUninitializedProperties, session, i, cancellationToken)).ConfigureAwait(false); + var dirty = await (DirtyAsync(properties, currentState, previousState, includeColumns, session, i, cancellationToken)).ConfigureAwait(false); if (dirty) { if (results == null) @@ -298,14 +325,14 @@ public static async Task FindDirtyAsync(StandardProperty[] properties, } } - private static async Task DirtyAsync(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, bool anyUninitializedProperties, ISessionImplementor session, int i, CancellationToken cancellationToken) + private static async Task DirtyAsync(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, ISessionImplementor session, int i, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (Equals(LazyPropertyInitializer.UnfetchedProperty, currentState[i])) return false; if (Equals(LazyPropertyInitializer.UnfetchedProperty, previousState[i])) return true; - return properties[i].IsDirtyCheckable(anyUninitializedProperties) && + return properties[i].IsDirtyCheckable() && await (properties[i].Type.IsDirtyAsync(previousState[i], currentState[i], includeColumns[i], session, cancellationToken)).ConfigureAwait(false); } @@ -322,12 +349,39 @@ private static async Task DirtyAsync(StandardProperty[] properties, object /// The session from which the dirty check request originated. /// A cancellation token that can be used to cancel the work /// Array containing indices of the modified properties, or null if no properties considered modified. - public static async Task FindModifiedAsync(StandardProperty[] properties, + // Since 5.3 + [Obsolete("Use the overload without anyUninitializedProperties parameter.")] + public static Task FindModifiedAsync(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, bool anyUninitializedProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return FindModifiedAsync(properties, currentState, previousState, includeColumns, session, cancellationToken); + } + + /// + /// Determine if any of the given field values are modified, returning an array containing + /// indices of the modified fields. + /// If it is determined that no fields are dirty, null is returned. + /// + /// The property definitions + /// The current state of the entity + /// The baseline state of the entity + /// Columns to be included in the mod checking, per property + /// The session from which the dirty check request originated. + /// A cancellation token that can be used to cancel the work + /// Array containing indices of the modified properties, or null if no properties considered modified. + public static async Task FindModifiedAsync(StandardProperty[] properties, + object[] currentState, + object[] previousState, + bool[][] includeColumns, + ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); int[] results = null; @@ -338,7 +392,7 @@ public static async Task FindModifiedAsync(StandardProperty[] properties, { bool dirty = !Equals(LazyPropertyInitializer.UnfetchedProperty, currentState[i]) && - properties[i].IsDirtyCheckable(anyUninitializedProperties) + properties[i].IsDirtyCheckable() && await (properties[i].Type.IsModifiedAsync(previousState[i], currentState[i], includeColumns[i], session, cancellationToken)).ConfigureAwait(false); if (dirty) diff --git a/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs b/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs index df7f58e4fd5..eebd202b490 100644 --- a/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs +++ b/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs @@ -40,11 +40,9 @@ public interface IBytecodeEnhancementMetadata /// Build and inject an interceptor instance into the enhanced entity. /// /// The entity into which built interceptor should be injected. - /// Whether all lazy properties were unfetched or not. /// The session to which the entity instance belongs. /// The built and injected interceptor. - // TODO: Remove lazyPropertiesAreUnfetched when interceptor will be injected on entity instantiation - IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session); + IFieldInterceptor InjectInterceptor(object entity, ISessionImplementor session); /// /// Extract the field interceptor instance from the enhanced entity. @@ -66,5 +64,12 @@ public interface IBytecodeEnhancementMetadata /// The entity state from which to retrieve the uninitialized lazy properties. /// The uninitialized property names. ISet GetUninitializedLazyProperties(object[] entityState); + + /// + /// Check whether the enhanced entity has any uninitialized lazy properties. + /// + /// The entity to check for uninitialized lazy properties. + /// Whether the enhanced entity has any uninitialized lazy properties. + bool HasAnyUninitializedLazyProperties(object entity); } } diff --git a/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs b/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs index d27ec518b3b..59b85ea6810 100644 --- a/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs +++ b/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs @@ -18,24 +18,43 @@ public class LazyPropertiesMetadata { public static LazyPropertiesMetadata NonEnhanced(string entityName) { - return new LazyPropertiesMetadata(entityName, null); + return new LazyPropertiesMetadata(entityName, null, null); } public static LazyPropertiesMetadata From( string entityName, IEnumerable lazyPropertyDescriptors) { - // TODO: port lazy fetch groups + var lazyProperties = new Dictionary(); + var fetchGroups = new Dictionary>(); + var mutableFetchGroups = new Dictionary>(); + + foreach (var propertyDescriptor in lazyPropertyDescriptors) + { + lazyProperties.Add(propertyDescriptor.Name, propertyDescriptor); + if (!mutableFetchGroups.TryGetValue(propertyDescriptor.FetchGroupName, out var fetchGroup)) + { + fetchGroup = new HashSet(); + mutableFetchGroups.Add(propertyDescriptor.FetchGroupName, fetchGroup); + fetchGroups.Add(propertyDescriptor.FetchGroupName, new ReadOnlySet(fetchGroup)); + } + + fetchGroup.Add(propertyDescriptor.Name); + } + return new LazyPropertiesMetadata( entityName, - lazyPropertyDescriptors.ToDictionary(o => o.Name)); + lazyProperties, + fetchGroups); } private readonly IDictionary _lazyPropertyDescriptors; + private readonly IDictionary> _fetchGroups; public LazyPropertiesMetadata( string entityName, - IDictionary lazyPropertyDescriptors) + IDictionary lazyPropertyDescriptors, + IDictionary> fetchGroups) { EntityName = entityName; _lazyPropertyDescriptors = lazyPropertyDescriptors; @@ -43,7 +62,11 @@ public LazyPropertiesMetadata( LazyPropertyNames = HasLazyProperties ? new ReadOnlySet(new HashSet(_lazyPropertyDescriptors.Keys)) : CollectionHelper.EmptySet(); - // TODO: port lazy fetch groups + + _fetchGroups = fetchGroups; + FetchGroupNames = _fetchGroups?.Count > 0 + ? new ReadOnlySet(new HashSet(_fetchGroups.Keys)) + : CollectionHelper.EmptySet(); } public string EntityName { get; } @@ -52,6 +75,8 @@ public LazyPropertiesMetadata( public ISet LazyPropertyNames { get; } + public ISet FetchGroupNames { get; } + public IEnumerable LazyPropertyDescriptors => _lazyPropertyDescriptors?.Values ?? Enumerable.Empty(); @@ -70,5 +95,29 @@ public LazyPropertyDescriptor GetLazyPropertyDescriptor(string propertyName) return descriptor; } + + public string GetFetchGroupName(string propertyName) + { + return GetLazyPropertyDescriptor(propertyName).FetchGroupName; + } + + public ISet GetPropertiesInFetchGroup(string groupName) + { + if (!_fetchGroups.TryGetValue(groupName, out var properties)) + { + throw new InvalidOperationException( + $"Fetch group {groupName} does not exist for entity {EntityName}"); + } + + return properties; + } + + public IEnumerable GetFetchGroupPropertyDescriptors(string groupName) + { + foreach (var propertyName in GetPropertiesInFetchGroup(groupName)) + { + yield return GetLazyPropertyDescriptor(propertyName); + } + } } } diff --git a/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs b/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs index a0640ef9470..1352e605831 100644 --- a/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs +++ b/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs @@ -20,13 +20,16 @@ public static LazyPropertyDescriptor From( int propertyIndex, int lazyIndex) { - // TODO: port lazy fetch groups + var fetchGroupName = string.IsNullOrEmpty(property.LazyGroup) + ? "DEFAULT" + : property.LazyGroup; return new LazyPropertyDescriptor( propertyIndex, lazyIndex, property.Name, - property.Type + property.Type, + fetchGroupName ); } @@ -34,7 +37,8 @@ private LazyPropertyDescriptor( int propertyIndex, int lazyIndex, string name, - IType type) + IType type, + string fetchGroupName) { if (propertyIndex < lazyIndex) { @@ -45,6 +49,7 @@ private LazyPropertyDescriptor( LazyIndex = lazyIndex; Name = name; Type = type; + FetchGroupName = fetchGroupName; } /// @@ -66,5 +71,10 @@ private LazyPropertyDescriptor( /// Access to the property's type /// public IType Type { get; } + + /// + /// Access to the name of the fetch group to which the property belongs + /// + public string FetchGroupName { get; } } } diff --git a/src/NHibernate/Cache/Entry/CacheEntry.cs b/src/NHibernate/Cache/Entry/CacheEntry.cs index 368bb1ed37c..fb431ea489e 100644 --- a/src/NHibernate/Cache/Entry/CacheEntry.cs +++ b/src/NHibernate/Cache/Entry/CacheEntry.cs @@ -34,6 +34,8 @@ public CacheEntry(object[] state, IEntityPersister persister, bool unfetched, ob Version = version; } + // Since 5.3 + [Obsolete("Use the overload without unfetched parameter instead.")] public static CacheEntry Create(object[] state, IEntityPersister persister, bool unfetched, object version, ISessionImplementor session, object owner) { @@ -52,6 +54,23 @@ public static CacheEntry Create(object[] state, IEntityPersister persister, bool }; } + public static CacheEntry Create(object[] state, IEntityPersister persister, object version, + ISessionImplementor session, object owner) + { + return new CacheEntry + { + //disassembled state gets put in a new array (we write to cache by value!) + DisassembledState = TypeHelper.Disassemble( + state, + persister.PropertyTypes, + persister.IsLazyPropertiesCacheable ? null : persister.PropertyLaziness, + session, + owner), + Subclass = persister.EntityName, + Version = version + }; + } + // 6.0 TODO convert to auto-property [DataMember] public object Version @@ -68,8 +87,9 @@ public string Subclass set => subclass = value; } - // 6.0 TODO convert to auto-property [DataMember] + // Since 5.3 + [Obsolete("This property is not used and will be removed in a future version.")] public bool AreLazyPropertiesUnfetched { get => lazyPropertiesAreUnfetched; diff --git a/src/NHibernate/Cache/Entry/StructuredCacheEntry.cs b/src/NHibernate/Cache/Entry/StructuredCacheEntry.cs index bc9bf20372a..1772d1f74ee 100644 --- a/src/NHibernate/Cache/Entry/StructuredCacheEntry.cs +++ b/src/NHibernate/Cache/Entry/StructuredCacheEntry.cs @@ -18,7 +18,6 @@ public StructuredCacheEntry(IEntityPersister persister) public object Destructure(object item, ISessionFactoryImplementor factory) { IDictionary map = (IDictionary)item; - bool lazyPropertiesUnfetched = ((bool)map["_lazyPropertiesUnfetched"]); string subclass = (string)map["_subclass"]; object version = map["_version"]; IEntityPersister subclassPersister = factory.GetEntityPersister(subclass); @@ -32,8 +31,7 @@ public object Destructure(object item, ISessionFactoryImplementor factory) { Subclass = subclass, DisassembledState = state, - Version = version, - AreLazyPropertiesUnfetched = lazyPropertiesUnfetched + Version = version }; } @@ -44,7 +42,6 @@ public object Structure(object item) IDictionary map = new Hashtable(names.Length + 2); map["_subclass"] = entry.Subclass; map["_version"] = entry.Version; - map["_lazyPropertiesUnfetched"] = entry.AreLazyPropertiesUnfetched; for (int i = 0; i < names.Length; i++) { map[names[i]] = entry.DisassembledState[i]; diff --git a/src/NHibernate/Cfg/MappingSchema/Hbm.generated.cs b/src/NHibernate/Cfg/MappingSchema/Hbm.generated.cs index 4c18d55f26d..aec0e5e457e 100644 --- a/src/NHibernate/Cfg/MappingSchema/Hbm.generated.cs +++ b/src/NHibernate/Cfg/MappingSchema/Hbm.generated.cs @@ -982,6 +982,10 @@ public partial class HbmProperty { [System.Xml.Serialization.XmlAttributeAttribute()] [System.ComponentModel.DefaultValueAttribute(false)] public bool lazy; + + /// + [System.Xml.Serialization.XmlAttributeAttribute("lazy-group")] + public string lazygroup; /// [System.Xml.Serialization.XmlAttributeAttribute()] @@ -2234,6 +2238,10 @@ public partial class HbmComponent { [System.Xml.Serialization.XmlAttributeAttribute()] [System.ComponentModel.DefaultValueAttribute(false)] public bool lazy; + + /// + [System.Xml.Serialization.XmlAttributeAttribute("lazy-group")] + public string lazygroup; /// [System.Xml.Serialization.XmlAttributeAttribute("optimistic-lock")] diff --git a/src/NHibernate/Cfg/MappingSchema/HbmProperty.cs b/src/NHibernate/Cfg/MappingSchema/HbmProperty.cs index f99dcbe2ce2..04c0a1f0611 100644 --- a/src/NHibernate/Cfg/MappingSchema/HbmProperty.cs +++ b/src/NHibernate/Cfg/MappingSchema/HbmProperty.cs @@ -20,6 +20,8 @@ public bool IsLazyProperty get { return lazy; } } + public string FetchGroup => lazygroup; + public string Access { get { return access; } diff --git a/src/NHibernate/Cfg/MappingSchema/IEntityPropertyMapping.cs b/src/NHibernate/Cfg/MappingSchema/IEntityPropertyMapping.cs index 675de6155ac..ccdc34bd332 100644 --- a/src/NHibernate/Cfg/MappingSchema/IEntityPropertyMapping.cs +++ b/src/NHibernate/Cfg/MappingSchema/IEntityPropertyMapping.cs @@ -7,4 +7,23 @@ public interface IEntityPropertyMapping: IDecoratable bool OptimisticLock { get; } bool IsLazyProperty { get; } } -} \ No newline at end of file + + internal static class EntityPropertyMappingExtensiosns + { + // 6.0 TODO: Move to IEntityPropertyMapping as a property + public static string GetLazyGroup(this IEntityPropertyMapping mapping) + { + if (mapping is HbmProperty property) + { + return property.lazygroup; + } + + if (mapping is HbmComponent component) + { + return component.lazygroup; + } + + return null; + } + } +} diff --git a/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs b/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs index b2c01800172..a5ad33b51bb 100644 --- a/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs +++ b/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs @@ -419,6 +419,7 @@ private Property CreateProperty(IEntityPropertyMapping propertyMapping, string p PropertyAccessorName = propertyAccessorName, Value = value, IsLazy = propertyMapping.IsLazyProperty, + LazyGroup = propertyMapping.GetLazyGroup(), IsOptimisticLocked = propertyMapping.OptimisticLock, MetaAttributes = GetMetas(propertyMapping, inheritedMetas) }; diff --git a/src/NHibernate/Engine/EntityEntry.cs b/src/NHibernate/Engine/EntityEntry.cs index 1ae1daaa16d..ac702632adb 100644 --- a/src/NHibernate/Engine/EntityEntry.cs +++ b/src/NHibernate/Engine/EntityEntry.cs @@ -45,9 +45,31 @@ public sealed class EntityEntry /// The that is responsible for this Entity. /// /// + // Since 5.3 + [Obsolete("Use the constructor without lazyPropertiesAreUnfetched parameter")] + internal EntityEntry(Status status, object[] loadedState, object rowId, object id, object version, LockMode lockMode, + bool existsInDatabase, IEntityPersister persister, + bool disableVersionIncrement, bool lazyPropertiesAreUnfetched) + :this(status, loadedState, rowId, id, version, lockMode, existsInDatabase, persister, disableVersionIncrement) + { + loadedWithLazyPropertiesUnfetched = lazyPropertiesAreUnfetched; + } + + /// + /// Initializes a new instance of EntityEntry. + /// + /// The current of the Entity. + /// The snapshot of the Entity's state when it was loaded. + /// + /// The identifier of the Entity in the database. + /// The version of the Entity. + /// The for the Entity. + /// A boolean indicating if the Entity exists in the database. + /// The that is responsible for this Entity. + /// internal EntityEntry(Status status, object[] loadedState, object rowId, object id, object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, - bool disableVersionIncrement, bool lazyPropertiesAreUnfetched) + bool disableVersionIncrement) { this.status = status; this.previousStatus = null; @@ -59,7 +81,6 @@ internal EntityEntry(Status status, object[] loadedState, object rowId, object i this.version = version; this.lockMode = lockMode; isBeingReplicated = disableVersionIncrement; - loadedWithLazyPropertiesUnfetched = lazyPropertiesAreUnfetched; this.persister = persister; entityName = persister == null ? null : persister.EntityName; } @@ -180,6 +201,8 @@ public object RowId get { return rowId; } } + // Since 5.3 + [Obsolete("This property is not used and will be removed in a future version.")] public bool LoadedWithLazyPropertiesUnfetched { get { return loadedWithLazyPropertiesUnfetched; } diff --git a/src/NHibernate/Engine/IPersistenceContext.cs b/src/NHibernate/Engine/IPersistenceContext.cs index 9e3e4cf70ce..3544bc2d3a9 100644 --- a/src/NHibernate/Engine/IPersistenceContext.cs +++ b/src/NHibernate/Engine/IPersistenceContext.cs @@ -1,7 +1,10 @@ +using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using NHibernate.Collection; using NHibernate.Engine.Loading; +using NHibernate.Intercept; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; using NHibernate.Proxy; @@ -160,6 +163,8 @@ public partial interface IPersistenceContext CollectionEntry GetCollectionEntry(IPersistentCollection coll); /// Adds an entity to the internal caches. + // Since 5.3 + [Obsolete("Use the AddEntity extension method instead")] EntityEntry AddEntity(object entity, Status status, object[] loadedState, EntityKey entityKey, object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, bool disableVersionIncrement, bool lazyPropertiesAreUnfetched); @@ -168,6 +173,8 @@ EntityEntry AddEntity(object entity, Status status, object[] loadedState, Entity /// Generates an appropriate EntityEntry instance and adds it /// to the event source's internal caches. /// + // Since 5.3 + [Obsolete("Use the AddEntry extension method instead")] EntityEntry AddEntry(object entity, Status status, object[] loadedState, object rowId, object id, object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, bool disableVersionIncrement, bool lazyPropertiesAreUnfetched); @@ -409,4 +416,97 @@ EntityEntry AddEntry(object entity, Status status, object[] loadedState, object /// The child. void RemoveChildParent(object child); } + + public static class PersistenceContextExtensions + { + /// Adds an entity to the internal caches. + public static EntityEntry AddEntity( + this IPersistenceContext context, + object entity, + Status status, + object[] loadedState, + EntityKey entityKey, + object version, + LockMode lockMode, + bool existsInDatabase, + IEntityPersister persister, + bool disableVersionIncrement) + { + if (context is StatefulPersistenceContext statefulPersistence) + { + return statefulPersistence.AddEntity( + entity, + status, + loadedState, + entityKey, + version, + lockMode, + existsInDatabase, + persister, + disableVersionIncrement); + } + +#pragma warning disable 618 + return context.AddEntity( + entity, + status, + loadedState, + entityKey, + version, + lockMode, + existsInDatabase, + persister, + disableVersionIncrement, + loadedState?.Any(o => o == LazyPropertyInitializer.UnfetchedProperty) == true); +#pragma warning restore 618 + } + + /// + /// Generates an appropriate EntityEntry instance and adds it + /// to the event source's internal caches. + /// + public static EntityEntry AddEntry( + this IPersistenceContext context, + object entity, + Status status, + object[] loadedState, + object rowId, + object id, + object version, + LockMode lockMode, + bool existsInDatabase, + IEntityPersister persister, + bool disableVersionIncrement) + { + if (context is StatefulPersistenceContext statefulPersistence) + { + return statefulPersistence.AddEntry( + entity, + status, + loadedState, + rowId, + id, + version, + lockMode, + existsInDatabase, + persister, + disableVersionIncrement); + } + +#pragma warning disable 618 + return context.AddEntry( + entity, + status, + loadedState, + rowId, + id, + version, + lockMode, + existsInDatabase, + persister, + disableVersionIncrement, + loadedState?.Any(o => o == LazyPropertyInitializer.UnfetchedProperty) == true); +#pragma warning restore 618 + } + } } diff --git a/src/NHibernate/Engine/StatefulPersistenceContext.cs b/src/NHibernate/Engine/StatefulPersistenceContext.cs index 0a2bcfe314b..50ccb500d0e 100644 --- a/src/NHibernate/Engine/StatefulPersistenceContext.cs +++ b/src/NHibernate/Engine/StatefulPersistenceContext.cs @@ -522,6 +522,16 @@ public EntityEntry AddEntity(object entity, Status status, object[] loadedState, return AddEntry(entity, status, loadedState, null, entityKey.Identifier, version, lockMode, existsInDatabase, persister, disableVersionIncrement, lazyPropertiesAreUnfetched); } + /// Adds an entity to the internal caches. + public EntityEntry AddEntity(object entity, Status status, object[] loadedState, EntityKey entityKey, object version, + LockMode lockMode, bool existsInDatabase, IEntityPersister persister, + bool disableVersionIncrement) + { + AddEntity(entityKey, entity); + + return AddEntry(entity, status, loadedState, null, entityKey.Identifier, version, lockMode, existsInDatabase, persister, disableVersionIncrement); + } + /// /// Generates an appropriate EntityEntry instance and adds it /// to the event source's internal caches. @@ -531,8 +541,27 @@ public EntityEntry AddEntry(object entity, Status status, object[] loadedState, bool disableVersionIncrement, bool lazyPropertiesAreUnfetched) { EntityEntry e = +#pragma warning disable 618 new EntityEntry(status, loadedState, rowId, id, version, lockMode, existsInDatabase, persister, disableVersionIncrement, lazyPropertiesAreUnfetched); +#pragma warning restore 618 + entityEntries[entity] = e; + + SetHasNonReadOnlyEnties(status); + return e; + } + + /// + /// Generates an appropriate EntityEntry instance and adds it + /// to the event source's internal caches. + /// + public EntityEntry AddEntry(object entity, Status status, object[] loadedState, object rowId, object id, + object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, + bool disableVersionIncrement) + { + EntityEntry e = + new EntityEntry(status, loadedState, rowId, id, version, lockMode, existsInDatabase, persister, + disableVersionIncrement); entityEntries[entity] = e; SetHasNonReadOnlyEnties(status); @@ -1364,8 +1393,7 @@ public void ReplaceDelayedEntityIdentityInsertKeys(EntityKey oldKey, object gene var newKey = Session.GenerateEntityKey(generatedId, oldEntry.Persister); AddEntity(newKey, entity); AddEntry(entity, oldEntry.Status, oldEntry.LoadedState, oldEntry.RowId, generatedId, oldEntry.Version, - oldEntry.LockMode, oldEntry.ExistsInDatabase, oldEntry.Persister, oldEntry.IsBeingReplicated, - oldEntry.LoadedWithLazyPropertiesUnfetched); + oldEntry.LockMode, oldEntry.ExistsInDatabase, oldEntry.Persister, oldEntry.IsBeingReplicated); } public bool IsLoadFinished diff --git a/src/NHibernate/Engine/TwoPhaseLoad.cs b/src/NHibernate/Engine/TwoPhaseLoad.cs index 4dc54578f10..eee2a5a4c06 100644 --- a/src/NHibernate/Engine/TwoPhaseLoad.cs +++ b/src/NHibernate/Engine/TwoPhaseLoad.cs @@ -22,7 +22,7 @@ namespace NHibernate.Engine public static partial class TwoPhaseLoad { private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(TwoPhaseLoad)); - + /// /// Register the "hydrated" state of an entity instance, after the first step of 2-phase loading. /// @@ -30,6 +30,8 @@ public static partial class TwoPhaseLoad /// to resolve any associations yet, because there might be other entities waiting to be /// read from the JDBC result set we are currently processing /// + // Since 5.3 + [Obsolete("Use the overload without lazyPropertiesAreUnfetched parameter instead")] public static void PostHydrate(IEntityPersister persister, object id, object[] values, object rowId, object obj, LockMode lockMode, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { object version = Versioning.GetVersion(values, persister); @@ -41,7 +43,26 @@ public static void PostHydrate(IEntityPersister persister, object id, object[] v log.Debug("Version: {0}", versionStr); } } - + + /// + /// Register the "hydrated" state of an entity instance, after the first step of 2-phase loading. + /// + /// Add the "hydrated state" (an array) of an uninitialized entity to the session. We don't try + /// to resolve any associations yet, because there might be other entities waiting to be + /// read from the JDBC result set we are currently processing + /// + public static void PostHydrate(IEntityPersister persister, object id, object[] values, object rowId, object obj, LockMode lockMode, ISessionImplementor session) + { + object version = Versioning.GetVersion(values, persister); + session.PersistenceContext.AddEntry(obj, Status.Loading, values, rowId, id, version, lockMode, true, persister, false); + + if (log.IsDebugEnabled() && version != null) + { + System.String versionStr = persister.IsVersioned ? persister.VersionType.ToLoggableString(version, session.Factory) : "null"; + log.Debug("Version: {0}", versionStr); + } + } + /// /// Perform the second step of 2-phase load. Fully initialize the entity instance. /// After processing a JDBC result set, we "resolve" all the associations @@ -132,7 +153,7 @@ internal static void InitializeEntity(object entity, bool readOnly, ISessionImpl object version = Versioning.GetVersion(hydratedState, persister); CacheEntry entry = - CacheEntry.Create(hydratedState, persister, entityEntry.LoadedWithLazyPropertiesUnfetched, version, session, entity); + CacheEntry.Create(hydratedState, persister, version, session, entity); CacheKey cacheKey = session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); if (cacheBatchingHandler != null && persister.IsBatchLoadable) @@ -192,7 +213,7 @@ internal static void InitializeEntity(object entity, bool readOnly, ISessionImpl persistenceContext.SetEntryStatus(entityEntry, Status.Loaded); } - persister.AfterInitialize(entity, entityEntry.LoadedWithLazyPropertiesUnfetched, session); + persister.AfterInitialize(entity, session); if (session.IsEventSource) { @@ -219,7 +240,8 @@ internal static void InitializeEntity(object entity, bool readOnly, ISessionImpl private static bool UseMinimalPuts(ISessionImplementor session, EntityEntry entityEntry) { return (session.Factory.Settings.IsMinimalPutsEnabled && session.CacheMode != CacheMode.Refresh) - || (entityEntry.Persister.HasLazyProperties && entityEntry.LoadedWithLazyPropertiesUnfetched && entityEntry.Persister.IsLazyPropertiesCacheable); + // Use minimal puts also for cacheable lazy properties in order to avoid sending large objects (e.g. an image) + || (entityEntry.Persister.HasLazyProperties && entityEntry.Persister.IsLazyPropertiesCacheable); } /// @@ -228,14 +250,34 @@ private static bool UseMinimalPuts(ISessionImplementor session, EntityEntry enti /// Create a "temporary" entry for a newly instantiated entity. The entity is uninitialized, /// but we need the mapping from id to instance in order to guarantee uniqueness. /// + // Since 5.3 + [Obsolete("Use the overload without the lazyPropertiesAreUnfetched parameter")] public static void AddUninitializedEntity(EntityKey key, object obj, IEntityPersister persister, LockMode lockMode, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { session.PersistenceContext.AddEntity(obj, Status.Loading, null, key, null, lockMode, true, persister, false, lazyPropertiesAreUnfetched); } + /// + /// Add an uninitialized instance of an entity class, as a placeholder to ensure object + /// identity. Must be called before postHydrate(). + /// Create a "temporary" entry for a newly instantiated entity. The entity is uninitialized, + /// but we need the mapping from id to instance in order to guarantee uniqueness. + /// + public static void AddUninitializedEntity(EntityKey key, object obj, IEntityPersister persister, LockMode lockMode, ISessionImplementor session) + { + session.PersistenceContext.AddEntity(obj, Status.Loading, null, key, null, lockMode, true, persister, false); + } + + // Since 5.3 + [Obsolete("Use the overload without the lazyPropertiesAreUnfetched parameter")] public static void AddUninitializedCachedEntity(EntityKey key, object obj, IEntityPersister persister, LockMode lockMode, bool lazyPropertiesAreUnfetched, object version, ISessionImplementor session) { session.PersistenceContext.AddEntity(obj, Status.Loading, null, key, version, lockMode, true, persister, false, lazyPropertiesAreUnfetched); } + + public static void AddUninitializedCachedEntity(EntityKey key, object obj, IEntityPersister persister, LockMode lockMode, object version, ISessionImplementor session) + { + session.PersistenceContext.AddEntity(obj, Status.Loading, null, key, version, lockMode, true, persister, false); + } } } diff --git a/src/NHibernate/Event/Default/AbstractReassociateEventListener.cs b/src/NHibernate/Event/Default/AbstractReassociateEventListener.cs index 81b37054009..ee81e52e18a 100644 --- a/src/NHibernate/Event/Default/AbstractReassociateEventListener.cs +++ b/src/NHibernate/Event/Default/AbstractReassociateEventListener.cs @@ -51,8 +51,7 @@ protected EntityEntry Reassociate(AbstractEvent @event, object entity, object id LockMode.None, true, persister, - false, - true); + false); new OnLockVisitor(source, id, entity).Process(entity, persister); diff --git a/src/NHibernate/Event/Default/AbstractSaveEventListener.cs b/src/NHibernate/Event/Default/AbstractSaveEventListener.cs index d3dadbb5dc9..f4fe501ab2e 100644 --- a/src/NHibernate/Event/Default/AbstractSaveEventListener.cs +++ b/src/NHibernate/Event/Default/AbstractSaveEventListener.cs @@ -216,7 +216,7 @@ protected virtual object PerformSaveOrReplicate(object entity, EntityKey key, IE // Put a placeholder in entries, so we don't recurse back and try to save() the // same object again. QUESTION: should this be done before onSave() is called? // likewise, should it be done before onUpdate()? - source.PersistenceContext.AddEntry(entity, Status.Saving, null, null, id, null, LockMode.Write, useIdentityColumn, persister, false, false); + source.PersistenceContext.AddEntry(entity, Status.Saving, null, null, id, null, LockMode.Write, useIdentityColumn, persister, false); CascadeBeforeSave(source, persister, entity, anything); @@ -278,8 +278,7 @@ protected virtual object PerformSaveOrReplicate(object entity, EntityKey key, IE LockMode.Write, useIdentityColumn, persister, - VersionIncrementDisabled, - false); + VersionIncrementDisabled); //source.getPersistenceContext().removeNonExist( new EntityKey( id, persister, source.getEntityMode() ) ); if (!useIdentityColumn) @@ -299,7 +298,7 @@ private void MarkInterceptorDirty(object entity, IEntityPersister persister, IEv if (persister.IsInstrumented) { var interceptor = persister.EntityMetamodel.BytecodeEnhancementMetadata - .InjectInterceptor(entity, false, source); + .InjectInterceptor(entity, source); interceptor?.MarkDirty(); } } diff --git a/src/NHibernate/Event/Default/DefaultDeleteEventListener.cs b/src/NHibernate/Event/Default/DefaultDeleteEventListener.cs index c4f699f863f..13f9a2e20a1 100644 --- a/src/NHibernate/Event/Default/DefaultDeleteEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultDeleteEventListener.cs @@ -81,7 +81,6 @@ public virtual void OnDelete(DeleteEvent @event, ISet transientEntities) LockMode.None, true, persister, - false, false); } else diff --git a/src/NHibernate/Event/Default/DefaultLoadEventListener.cs b/src/NHibernate/Event/Default/DefaultLoadEventListener.cs index 54445a9eeb0..036d9eaf59e 100644 --- a/src/NHibernate/Event/Default/DefaultLoadEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultLoadEventListener.cs @@ -505,7 +505,7 @@ private object AssembleCacheEntry(CacheEntry entry, object id, IEntityPersister // make it circular-reference safe EntityKey entityKey = session.GenerateEntityKey(id, subclassPersister); - TwoPhaseLoad.AddUninitializedCachedEntity(entityKey, result, subclassPersister, LockMode.None, entry.AreLazyPropertiesUnfetched, entry.Version, session); + TwoPhaseLoad.AddUninitializedCachedEntity(entityKey, result, subclassPersister, LockMode.None, entry.Version, session); IType[] types = subclassPersister.PropertyTypes; object[] values = entry.Assemble(result, id, subclassPersister, session.Interceptor, session); // intializes result by side-effect @@ -543,10 +543,9 @@ private object AssembleCacheEntry(CacheEntry entry, object id, IEntityPersister LockMode.None, true, subclassPersister, - false, - entry.AreLazyPropertiesUnfetched); + false); - subclassPersister.AfterInitialize(result, entry.AreLazyPropertiesUnfetched, session); + subclassPersister.AfterInitialize(result, session); persistenceContext.InitializeNonLazyCollections(); // upgrade the lock if necessary: //lock(result, lockMode); diff --git a/src/NHibernate/Event/Default/DefaultReplicateEventListener.cs b/src/NHibernate/Event/Default/DefaultReplicateEventListener.cs index b274eb929eb..5fa2dc26de7 100644 --- a/src/NHibernate/Event/Default/DefaultReplicateEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultReplicateEventListener.cs @@ -119,8 +119,7 @@ private void PerformReplication(object entity, object id, object version, IEntit LockMode.None, true, persister, - true, - false); + true); CascadeAfterReplicate(entity, persister, replicationMode, source); } diff --git a/src/NHibernate/Event/Default/DefaultSaveOrUpdateEventListener.cs b/src/NHibernate/Event/Default/DefaultSaveOrUpdateEventListener.cs index ac654e6afe2..897793a8180 100644 --- a/src/NHibernate/Event/Default/DefaultSaveOrUpdateEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultSaveOrUpdateEventListener.cs @@ -262,8 +262,7 @@ protected virtual void PerformUpdate(SaveOrUpdateEvent @event, object entity, IE LockMode.None, true, persister, - false, - true); + false); //persister.AfterReassociate(entity, source); TODO H3.2 not ported diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index 515381a17fe..87ede553e0a 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -100,13 +100,6 @@ public object Intercept(object target, string fieldName, object value) 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. - if (initializing) - { - return InvokeImplementation; - } - if (setter) { if (IsUninitializedProperty(fieldName)) @@ -128,6 +121,13 @@ public object Intercept(object target, string fieldName, object value, bool sett return value; } + // NH Specific: Hibernate only deals with lazy properties here, we deal with + // both lazy properties and with no-proxy. + if (initializing) + { + return InvokeImplementation; + } + if (IsInitializedField(fieldName)) { return value; @@ -197,8 +197,7 @@ 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; } diff --git a/src/NHibernate/Intercept/IFieldInterceptor.cs b/src/NHibernate/Intercept/IFieldInterceptor.cs index 4fd38f85aa4..111e257dc60 100644 --- a/src/NHibernate/Intercept/IFieldInterceptor.cs +++ b/src/NHibernate/Intercept/IFieldInterceptor.cs @@ -16,6 +16,8 @@ public interface IFieldInterceptor ISessionImplementor Session { get; set; } /// Is the entity to which we are bound completely initialized? + // Since 5.3 + [Obsolete("This property is not used and will be removed in a future version.")] bool IsInitialized { get;} /// The the given field initialized for the entity to which we are bound? @@ -51,7 +53,9 @@ internal static ISet GetUninitializedFields(this IFieldInterceptor inter return fieldInterceptor.GetUninitializedFields(); } +#pragma warning disable 618 if (interceptor.IsInitialized) +#pragma warning restore 618 { return CollectionHelper.EmptySet(); } diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index b79f8a7805d..a89c9b9b007 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1150,15 +1150,10 @@ private void UpdateLazyPropertiesFromResultSet(DbDataReader rs, int i, object ob EntityEntry entry, ILoadable rootPersister, ISessionImplementor session, Action cacheBatchingHandler) { - if (!entry.LoadedWithLazyPropertiesUnfetched) - { - return; // All lazy properties were already loaded - } - - var eagerPropertyFetch = IsEagerPropertyFetchEnabled(i); + var fetchAllProperties = IsEagerPropertyFetchEnabled(i); var fetchLazyProperties = GetFetchLazyProperties(i); - if (!eagerPropertyFetch && fetchLazyProperties == null) + if (!fetchAllProperties && fetchLazyProperties == null) { return; // No lazy properties were loaded } @@ -1191,11 +1186,18 @@ private void UpdateLazyPropertiesFromResultSet(DbDataReader rs, int i, object ob ? EntityAliases[i].SuffixedPropertyAliases : GetSubclassEntityAliases(i, persister); - if (!persister.InitializeLazyProperties(rs, id, obj, rootPersister, cols, updateLazyProperties, eagerPropertyFetch, session)) + if (!persister.InitializeLazyProperties(rs, id, obj, rootPersister, cols, updateLazyProperties, fetchAllProperties, session)) { return; } + UpdateCacheForEntity(obj, id, entry, persister, session, cacheBatchingHandler); + } + + internal static void UpdateCacheForEntity( + object obj, object id, EntityEntry entry, IEntityPersister persister, ISessionImplementor session, + Action cacheBatchingHandler) + { if (entry.Status == Status.Loading || !persister.HasCache || !session.CacheMode.HasFlag(CacheMode.Put) || !persister.IsLazyPropertiesCacheable) { @@ -1210,7 +1212,7 @@ private void UpdateLazyPropertiesFromResultSet(DbDataReader rs, int i, object ob var factory = session.Factory; var state = persister.GetPropertyValues(obj); var version = Versioning.GetVersion(state, persister); - var cacheEntry = CacheEntry.Create(state, persister, entry.LoadedWithLazyPropertiesUnfetched, version, session, obj); + var cacheEntry = CacheEntry.Create(state, persister, version, session, obj); var cacheKey = session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); if (cacheBatchingHandler != null && persister.IsBatchLoadable) @@ -1259,23 +1261,23 @@ private void LoadFromResultSet(DbDataReader rs, int i, object obj, string instan Log.Debug("Initializing object from DataReader: {0}", MessageHelper.InfoString(persister, id)); } - bool eagerPropertyFetch = IsEagerPropertyFetchEnabled(i); + bool fetchAllProperties = IsEagerPropertyFetchEnabled(i); var eagerFetchProperties = GetFetchLazyProperties(i); // add temp entry so that the next step is circular-reference // safe - only needed because some types don't take proper // advantage of two-phase-load (esp. components) - TwoPhaseLoad.AddUninitializedEntity(key, obj, persister, lockMode, !eagerPropertyFetch, session); + TwoPhaseLoad.AddUninitializedEntity(key, obj, persister, lockMode, session); string[][] cols = persister == rootPersister ? EntityAliases[i].SuffixedPropertyAliases : GetSubclassEntityAliases(i, persister); - object[] values = persister.Hydrate(rs, id, obj, cols, eagerFetchProperties, eagerPropertyFetch, session); + object[] values = persister.Hydrate(rs, id, obj, cols, eagerFetchProperties, fetchAllProperties, session); object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; - TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, !eagerPropertyFetch, session); + TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, session); } private string[][] GetSubclassEntityAliases(int i, ILoadable persister) diff --git a/src/NHibernate/Mapping/ByCode/IComponentMapper.cs b/src/NHibernate/Mapping/ByCode/IComponentMapper.cs index 41a06406c05..a4ca3ac0100 100644 --- a/src/NHibernate/Mapping/ByCode/IComponentMapper.cs +++ b/src/NHibernate/Mapping/ByCode/IComponentMapper.cs @@ -1,6 +1,8 @@ using System; using System.Linq.Expressions; using System.Reflection; +using NHibernate.Mapping.ByCode.Impl; +using NHibernate.Mapping.ByCode.Impl.CustomizersImpl; namespace NHibernate.Mapping.ByCode { @@ -31,4 +33,29 @@ public interface IComponentAttributesMapper : IEntityPropertyMapper public interface IComponentMapper : IComponentAttributesMapper, IPropertyContainerMapper {} -} \ No newline at end of file + + public static class ComponentAttributesMapper + { + // 6.0 TODO: Move to IComponentAttributesMapper + public static void LazyGroup(this IComponentAttributesMapper mapper, string name) + { + if (mapper is ComponentMapper component) + { + component.LazyGroup(name); + } + } + + // 6.0 TODO: Move to IComponentAttributesMapper + public static void LazyGroup(this IComponentAttributesMapper mapper, string name) + { + if (mapper is ComponentCustomizer component) + { + component.LazyGroup(name); + } + else if (mapper is ComponentElementCustomizer componentElement) + { + componentElement.LazyGroup(name); + } + } + } +} diff --git a/src/NHibernate/Mapping/ByCode/IPropertyMapper.cs b/src/NHibernate/Mapping/ByCode/IPropertyMapper.cs index b552c7d55d5..be463579543 100644 --- a/src/NHibernate/Mapping/ByCode/IPropertyMapper.cs +++ b/src/NHibernate/Mapping/ByCode/IPropertyMapper.cs @@ -1,3 +1,4 @@ +using NHibernate.Mapping.ByCode.Impl; using NHibernate.Type; namespace NHibernate.Mapping.ByCode @@ -23,4 +24,16 @@ public interface IPropertyMapper : IEntityPropertyMapper, IColumnsMapper void Lazy(bool isLazy); void Generated(PropertyGeneration generation); } + + public static class PropertyMapperExtensions + { + // 6.0 TODO: Move to IPropertyMapper + public static void FetchGroup(this IPropertyMapper mapper, string name) + { + if (mapper is PropertyMapper property) + { + property.FetchGroup(name); + } + } + } } diff --git a/src/NHibernate/Mapping/ByCode/Impl/ComponentMapper.cs b/src/NHibernate/Mapping/ByCode/Impl/ComponentMapper.cs index c414b03e9f3..801da3689e2 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/ComponentMapper.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/ComponentMapper.cs @@ -70,6 +70,11 @@ public void Lazy(bool isLazy) _component.lazy = isLazy; } + public void LazyGroup(string name) + { + _component.lazygroup = name; + } + public void Unique(bool unique) { _component.unique = unique; @@ -111,4 +116,4 @@ private IComponentParentMapper GetParentMapper(MemberInfo parent) return _parentMapper = new ComponentParentMapper(_component.parent, parent); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentCustomizer.cs b/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentCustomizer.cs index d6979a5cac4..470b5a7b020 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentCustomizer.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentCustomizer.cs @@ -64,6 +64,11 @@ public void Lazy(bool isLazy) AddCustomizer(m => m.Lazy(isLazy)); } + public void LazyGroup(string name) + { + AddCustomizer(m => m.LazyGroup(name)); + } + public void Unique(bool unique) { AddCustomizer(m=>m.Unique(unique)); @@ -121,4 +126,4 @@ IModelExplicitDeclarationsHolder IConformistHoldersProvider.ExplicitDeclarations #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentElementCustomizer.cs b/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentElementCustomizer.cs index 11453438bb3..60c529de125 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentElementCustomizer.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/ComponentElementCustomizer.cs @@ -65,6 +65,11 @@ public void Lazy(bool isLazy) _customizersHolder.AddCustomizer(typeof (TComponent), (IComponentAttributesMapper x) => x.Lazy(isLazy)); } + public void LazyGroup(string name) + { + _customizersHolder.AddCustomizer(typeof(TComponent), x => x.LazyGroup(name)); + } + public void Unique(bool unique) { _customizersHolder.AddCustomizer(typeof(TComponent), (IComponentAttributesMapper x) => x.Unique(unique)); @@ -173,4 +178,4 @@ public void OptimisticLock(bool takeInConsiderationForOptimisticLock) #endregion } -} \ No newline at end of file +} diff --git a/src/NHibernate/Mapping/ByCode/Impl/PropertyMapper.cs b/src/NHibernate/Mapping/ByCode/Impl/PropertyMapper.cs index 6d590e36d3b..b95843319e9 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/PropertyMapper.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/PropertyMapper.cs @@ -241,6 +241,11 @@ public void Lazy(bool isLazy) propertyMapping.lazy = isLazy; } + public void FetchGroup(string name) + { + propertyMapping.lazygroup = name; + } + public void Generated(PropertyGeneration generation) { if (generation == null) diff --git a/src/NHibernate/Mapping/Property.cs b/src/NHibernate/Mapping/Property.cs index f02dd580574..1207d10f68a 100644 --- a/src/NHibernate/Mapping/Property.cs +++ b/src/NHibernate/Mapping/Property.cs @@ -264,6 +264,8 @@ public bool IsLazy set { isLazy = value; } } + public string LazyGroup { get; set; } + public virtual bool BackRef { get { return false; } diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index c741f7cc912..7cb14034d14 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -211,7 +211,8 @@ public virtual void BindValues(DbCommand ps) private SqlString sqlVersionSelectString; private SqlString sqlSnapshotSelectString; - private SqlString sqlLazySelectString; + private SqlString sqlLazySelectString; // 6.0 TODO: Remove + private IDictionary _sqlLazySelectStringsByFetchGroup; private SqlCommandInfo sqlIdentityInsertString; private SqlCommandInfo sqlUpdateByRowIdString; @@ -604,11 +605,17 @@ protected SqlString SQLSnapshotSelectString get { return sqlSnapshotSelectString; } } + [Obsolete("Use GetSQLLazySelectString method instead")] protected SqlString SQLLazySelectString { get { return sqlLazySelectString; } } + protected SqlString GetSQLLazySelectString(string fetchGroup) + { + return _sqlLazySelectStringsByFetchGroup.TryGetValue(fetchGroup, out var value) ? value : null; + } + /// /// The queries that delete rows by id (and version) /// @@ -1246,6 +1253,8 @@ public virtual bool HasRowId get { return rowIdName != null; } } + // Since 5.3 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] protected internal virtual SqlString GenerateLazySelectString() { if (!entityMetamodel.HasLazyProperties) @@ -1292,6 +1301,60 @@ protected internal virtual SqlString GenerateLazySelectString() return RenderSelect(tableNumbers.ToArray(), columnNumbers.ToArray(), formulaNumbers.ToArray()); } + protected virtual IDictionary GenerateLazySelectStringsByFetchGroup() + { + var enhancementMetadata = entityMetamodel.BytecodeEnhancementMetadata; + if (!enhancementMetadata.EnhancedForLazyLoading || !enhancementMetadata.LazyPropertiesMetadata.HasLazyProperties) + { + return CollectionHelper.EmptyDictionary(); + } + + var result = new Dictionary(); + var lazyPropertiesMetadata = enhancementMetadata.LazyPropertiesMetadata; + + foreach (var groupName in lazyPropertiesMetadata.FetchGroupNames) + { + var tableNumbers = new HashSet(); + var columnNumbers = new List(); + var formulaNumbers = new List(); + + foreach (var lazyPropertyDescriptor in lazyPropertiesMetadata.GetFetchGroupPropertyDescriptors(groupName)) + { + // all this only really needs to consider properties + // of this class, not its subclasses, but since we + // are reusing code used for sequential selects, we + // use the subclass closure + var propertyNumber = GetSubclassPropertyIndex(lazyPropertyDescriptor.Name); + + var tableNumber = GetSubclassPropertyTableNumber(propertyNumber); + tableNumbers.Add(tableNumber); + + var colNumbers = subclassPropertyColumnNumberClosure[propertyNumber]; + columnNumbers.AddRange(colNumbers.Where(colNumber => colNumber != -1)); + + var formNumbers = subclassPropertyFormulaNumberClosure[propertyNumber]; + formulaNumbers.AddRange(formNumbers.Where(colNumber => colNumber != -1)); + } + + if (columnNumbers.Count == 0 && formulaNumbers.Count == 0) + { + // only one-to-one is lazy fetched + continue; + } + + result.Add( + groupName, + RenderSelect( + tableNumbers.ToArray(), + columnNumbers.ToArray(), + formulaNumbers.ToArray() + ) + ); + } + + return result; + } + public virtual object InitializeLazyProperty(string fieldName, object entity, ISessionImplementor session) { object id = session.GetContextEntityIdentifier(entity); @@ -1308,22 +1371,27 @@ public virtual object InitializeLazyProperty(string fieldName, object entity, IS } var uninitializedLazyProperties = InstrumentationMetadata.GetUninitializedLazyProperties(entity); - if (HasCache && session.CacheMode.HasFlag(CacheMode.Get)) + if (HasCache && session.CacheMode.HasFlag(CacheMode.Get) && IsLazyPropertiesCacheable) { CacheKey cacheKey = session.GenerateCacheKey(id, IdentifierType, EntityName); object ce = Cache.Get(cacheKey, session.Timestamp); if (ce != null) { CacheEntry cacheEntry = (CacheEntry)CacheEntryStructure.Destructure(ce, factory); - if (!cacheEntry.AreLazyPropertiesUnfetched) + var initializedValue = InitializeLazyPropertiesFromCache(fieldName, entity, session, entry, cacheEntry, uninitializedLazyProperties); + if (initializedValue != LazyPropertyInitializer.UnfetchedProperty) { - //note early exit here: - return InitializeLazyPropertiesFromCache(fieldName, entity, session, entry, cacheEntry, uninitializedLazyProperties); + // NOTE EARLY EXIT!!! + return initializedValue; } } } - return InitializeLazyPropertiesFromDatastore(fieldName, entity, session, id, entry, uninitializedLazyProperties); + var result = InitializeLazyPropertiesFromDatastore(fieldName, entity, session, id, entry, uninitializedLazyProperties); + // Update cache + Loader.Loader.UpdateCacheForEntity(entity, id, entry, this, session, null); + + return result; } public void InitializeLazyProperties( @@ -1388,6 +1456,11 @@ private object InitializeLazyPropertiesFromDatastore( log.Debug("initializing lazy properties from datastore"); + var lazyPropertiesMetadata = EntityMetamodel.BytecodeEnhancementMetadata.LazyPropertiesMetadata; + var fetchGroup = lazyPropertiesMetadata.GetFetchGroupName(fieldName); + var fetchGroupPropertyDescriptors = lazyPropertiesMetadata.GetFetchGroupPropertyDescriptors(fetchGroup); + var lazySelect = GetSQLLazySelectString(fetchGroup); + using (session.BeginProcess()) try { @@ -1396,7 +1469,6 @@ private object InitializeLazyPropertiesFromDatastore( DbDataReader rs = null; try { - SqlString lazySelect = SQLLazySelectString; if (lazySelect != null) { // null sql means that the only lazy properties @@ -1408,12 +1480,32 @@ private object InitializeLazyPropertiesFromDatastore( rs.Read(); } object[] snapshot = entry.LoadedState; - for (int j = 0; j < lazyPropertyNames.Length; j++) + foreach (var fetchGroupPropertyDescriptor in fetchGroupPropertyDescriptors) { - object propValue = lazyPropertyTypes[j].NullSafeGet(rs, lazyPropertyColumnAliases[j], session, entity); - if (InitializeLazyProperty(fieldName, entity, snapshot, j, propValue, uninitializedLazyProperties)) + // Iterate over all fetched properties even if they are already set + // as their state may not be populated yet. InitializeLazyProperty + // method will take care of not overriding their property values and + // will only update the states, if they have one. + + var selectedValue = fetchGroupPropertyDescriptor.Type.NullSafeGet( + rs, + lazyPropertyColumnAliases[fetchGroupPropertyDescriptor.LazyIndex], + session, + entity + ); + + var set = InitializeLazyProperty( + fieldName, + entity, + snapshot, + fetchGroupPropertyDescriptor.LazyIndex, + selectedValue, + uninitializedLazyProperties + ); + + if (set) { - result = propValue; + result = selectedValue; } } } @@ -1433,7 +1525,7 @@ private object InitializeLazyPropertiesFromDatastore( SqlException = sqle, Message = "could not initialize lazy properties: " + MessageHelper.InfoString(this, id, Factory), - Sql = SQLLazySelectString.ToString(), + Sql = lazySelect?.ToString(), EntityName = EntityName, EntityId = id }; @@ -1452,10 +1544,22 @@ private object InitializeLazyPropertiesFromCache( object[] snapshot = entry.LoadedState; for (int j = 0; j < lazyPropertyNames.Length; j++) { - object propValue = lazyPropertyTypes[j].Assemble(disassembledValues[lazyPropertyNumbers[j]], session, entity); - if (InitializeLazyProperty(fieldName, entity, snapshot, j, propValue, uninitializedLazyProperties)) + var cachedValue = disassembledValues[lazyPropertyNumbers[j]]; + if (cachedValue == LazyPropertyInitializer.UnfetchedProperty) { - result = propValue; + if (fieldName == lazyPropertyNames[j]) + { + result = LazyPropertyInitializer.UnfetchedProperty; + } + // don't try to initialize the unfetched property + } + else + { + var propValue = lazyPropertyTypes[j].Assemble(cachedValue, session, entity); + if (InitializeLazyProperty(fieldName, entity, snapshot, j, propValue, uninitializedLazyProperties)) + { + result = propValue; + } } } @@ -3363,7 +3467,7 @@ 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 InstrumentationMetadata.GetUninitializedLazyProperties(obj).Count > 0 && dirtyFields.Any(i => PropertyLaziness[i]); + return InstrumentationMetadata.HasAnyUninitializedLazyProperties(obj) && dirtyFields.Any(i => PropertyLaziness[i]); } public object Insert(object[] fields, object obj, ISessionImplementor session) @@ -3509,9 +3613,9 @@ protected void LogStaticSQL() if (log.IsDebugEnabled()) { log.Debug("Static SQL for entity: {0}", EntityName); - if (sqlLazySelectString != null) + foreach (var pair in _sqlLazySelectStringsByFetchGroup) { - log.Debug(" Lazy select: {0}", sqlLazySelectString); + log.Debug(" Lazy select ({0}): {1}", pair.Key, pair.Value); } if (sqlVersionSelectString != null) { @@ -3836,7 +3940,10 @@ public virtual void PostInstantiate() //select SQL sqlSnapshotSelectString = GenerateSnapshotSelectString(); +#pragma warning disable 618 sqlLazySelectString = GenerateLazySelectString(); +#pragma warning restore 618 + _sqlLazySelectStringsByFetchGroup = GenerateLazySelectStringsByFetchGroup(); sqlVersionSelectString = GenerateSelectVersionString(); if (HasInsertGeneratedProperties) { @@ -3985,7 +4092,7 @@ protected bool[] GetPropertiesToInsert(object[] fields) public virtual int[] FindDirty(object[] currentState, object[] previousState, object entity, ISessionImplementor session) { int[] props = TypeHelper.FindDirty( - entityMetamodel.Properties, currentState, previousState, propertyColumnUpdateable, HasUninitializedLazyProperties(entity), session); + entityMetamodel.Properties, currentState, previousState, propertyColumnUpdateable, session); if (props == null) { @@ -4001,7 +4108,7 @@ public virtual int[] FindDirty(object[] currentState, object[] previousState, ob public virtual int[] FindModified(object[] old, object[] current, object entity, ISessionImplementor session) { int[] props = TypeHelper.FindModified( - entityMetamodel.Properties, current, old, propertyColumnUpdateable, HasUninitializedLazyProperties(entity), session); + entityMetamodel.Properties, current, old, propertyColumnUpdateable, session); if (props == null) { return null; @@ -4080,7 +4187,7 @@ public virtual void AfterReassociate(object entity, ISessionImplementor session) } else { - var fieldInterceptor = InstrumentationMetadata.InjectInterceptor(entity, false, session); + var fieldInterceptor = InstrumentationMetadata.InjectInterceptor(entity, session); fieldInterceptor?.MarkDirty(); } } @@ -4247,7 +4354,12 @@ public bool HasUpdateGeneratedProperties public void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { - EntityTuplizer.AfterInitialize(entity, lazyPropertiesAreUnfetched, session); + EntityTuplizer.AfterInitialize(entity, session); + } + + public void AfterInitialize(object entity, ISessionImplementor session) + { + EntityTuplizer.AfterInitialize(entity, session); } public virtual bool[] PropertyUpdateability @@ -4335,7 +4447,7 @@ public bool IsInstance(object entity) public virtual bool HasUninitializedLazyProperties(object obj) { - return EntityTuplizer.HasUninitializedLazyProperties(obj); + return EntityMetamodel.BytecodeEnhancementMetadata.HasAnyUninitializedLazyProperties(obj); } public virtual void ResetIdentifier(object entity, object currentId, object currentVersion) diff --git a/src/NHibernate/Persister/Entity/IEntityPersister.cs b/src/NHibernate/Persister/Entity/IEntityPersister.cs index 7ba82488bb9..495e6147ddd 100644 --- a/src/NHibernate/Persister/Entity/IEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/IEntityPersister.cs @@ -442,6 +442,8 @@ void Update( #region stuff that is tuplizer-centric, but is passed a session /// Called just after the entities properties have been initialized + // Since 5.3 + [Obsolete("Use the extension method instead")] void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session); /// Called just after the entity has been reassociated with the session @@ -623,5 +625,20 @@ public static int GetBatchSize(this IEntityPersister persister) return 1; } + + /// Called just after the entities properties have been initialized + //6.0 TODO: Merge into IEntityPersister. + public static void AfterInitialize(this IEntityPersister persister, object entity, ISessionImplementor session) + { + if (persister is AbstractEntityPersister abstractEntityPersister) + { + abstractEntityPersister.AfterInitialize(entity, session); + return; + } + +#pragma warning disable 618 + persister.AfterInitialize(entity, true, session); +#pragma warning restore 618 + } } } diff --git a/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs index 20fb11edb62..68406fb252b 100644 --- a/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs @@ -228,6 +228,10 @@ public virtual void AfterInitialize(object entity, bool lazyPropertiesAreUnfetch { } + public virtual void AfterInitialize(object entity, ISessionImplementor session) + { + } + public bool HasProxy { get { return entityMetamodel.IsLazy; } @@ -401,7 +405,12 @@ protected virtual IProxyFactory ProxyFactory protected virtual bool ShouldGetAllProperties(object entity) { - return !HasUninitializedLazyProperties(entity); + if (!EntityMetamodel.BytecodeEnhancementMetadata.EnhancedForLazyLoading) + { + return true; + } + + return !EntityMetamodel.BytecodeEnhancementMetadata.HasAnyUninitializedLazyProperties(entity); } protected EntityMetamodel EntityMetamodel diff --git a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs index cd4dc440fa3..1d3fc563d1c 100644 --- a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs +++ b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs @@ -39,7 +39,7 @@ public BytecodeEnhancementMetadataNonPocoImpl(string entityName) public UnwrapProxyPropertiesMetadata UnwrapProxyPropertiesMetadata { get; } /// - public IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) + public IFieldInterceptor InjectInterceptor(object entity, ISessionImplementor session) { throw new NotInstrumentedException(_errorMessage); } @@ -61,5 +61,11 @@ public ISet GetUninitializedLazyProperties(object[] entityState) { return CollectionHelper.EmptySet(); } + + /// + public bool HasAnyUninitializedLazyProperties(object entity) + { + return false; + } } } diff --git a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs index 2f1ef4007d3..988e8767b63 100644 --- a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs +++ b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs @@ -120,7 +120,7 @@ public BytecodeEnhancementMetadataPocoImpl( public UnwrapProxyPropertiesMetadata UnwrapProxyPropertiesMetadata { get; } /// - public IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) + public IFieldInterceptor InjectInterceptor(object entity, ISessionImplementor session) { if (!EnhancedForLazyLoading) { @@ -140,7 +140,7 @@ public IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAre var fieldInterceptorImpl = new DefaultFieldInterceptor( session, - LazyPropertiesMetadata.HasLazyProperties && lazyPropertiesAreUnfetched + LazyPropertiesMetadata.HasLazyProperties ? new HashSet(LazyPropertiesMetadata.LazyPropertyNames) : null, UnwrapProxyPropertiesMetadata.UnwrapProxyPropertyNames, @@ -210,5 +210,12 @@ public ISet GetUninitializedLazyProperties(object[] entityState) return uninitializedProperties; } + + /// + public bool HasAnyUninitializedLazyProperties(object entity) + { + var interceptor = LazyPropertiesMetadata.HasLazyProperties ? ExtractInterceptor(entity) : null; + return interceptor?.GetUninitializedFields().Count > 0; + } } } diff --git a/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs index 397bdd595d6..75ba17dbc0c 100644 --- a/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs @@ -37,6 +37,8 @@ public interface IEntityTuplizer : ITuplizer System.Type ConcreteProxyClass { get; } /// Is it an instrumented POCO? + // Since 5.3 + [Obsolete("This property is not used and will be removed in a future version.")] bool IsInstrumented { get; } /// Create an entity instance initialized with the given identifier. @@ -100,6 +102,8 @@ public interface IEntityTuplizer : ITuplizer /// The entity being initialized. /// Are defined lazy properties currently unfecthed /// The session initializing this entity. + // Since 5.3 + [Obsolete("Use the extension method instead")] void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session); /// Does this entity, for this mode, present a possibility for proxying? @@ -117,6 +121,28 @@ public interface IEntityTuplizer : ITuplizer /// Does the given entity instance have any currently uninitialized lazy properties? /// The entity to be check for uninitialized lazy properties. /// True if uninitialized lazy properties were found; false otherwise. + // Since 5.3 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] bool HasUninitializedLazyProperties(object entity); } + + public static class EntityTuplizerExtensions + { + /// Called just after the entities properties have been initialized. + /// The entity tupilizer. + /// The entity being initialized. + /// The session initializing this entity. + public static void AfterInitialize(this IEntityTuplizer entityTuplizer, object entity, ISessionImplementor session) + { + if (entityTuplizer is AbstractEntityTuplizer abstractEntityTuplizer) + { + abstractEntityTuplizer.AfterInitialize(entity, session); + return; + } + +#pragma warning disable 618 + entityTuplizer.AfterInitialize(entity, true, session); +#pragma warning restore 618 + } + } } diff --git a/src/NHibernate/Tuple/Entity/PocoEntityInstantiator.cs b/src/NHibernate/Tuple/Entity/PocoEntityInstantiator.cs index c7aed855d50..0a4075ca643 100644 --- a/src/NHibernate/Tuple/Entity/PocoEntityInstantiator.cs +++ b/src/NHibernate/Tuple/Entity/PocoEntityInstantiator.cs @@ -38,7 +38,7 @@ protected override object CreateInstance() } var entity = _proxyFactory.GetFieldInterceptionProxy(base.CreateInstance); - _entityMetamodel.BytecodeEnhancementMetadata.InjectInterceptor(entity, true, null); + _entityMetamodel.BytecodeEnhancementMetadata.InjectInterceptor(entity, null); return entity; } diff --git a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs index 78c80515141..a2a669914ee 100644 --- a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs @@ -28,6 +28,7 @@ public class PocoEntityTuplizer : AbstractEntityTuplizer [NonSerialized] private IReflectionOptimizer optimizer; private readonly IProxyValidator proxyValidator; + private readonly IBytecodeEnhancementMetadata _enhancementMetadata; [NonSerialized] private bool isBytecodeProviderImpl; // 6.0 TODO: remove @@ -60,6 +61,7 @@ public PocoEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappe proxyInterface = mappedEntity.ProxyInterface; islifecycleImplementor = typeof(ILifecycle).IsAssignableFrom(mappedClass); isValidatableImplementor = typeof(IValidatable).IsAssignableFrom(mappedClass); + _enhancementMetadata = EntityMetamodel.BytecodeEnhancementMetadata; SetReflectionOptimizer(); @@ -75,7 +77,7 @@ public override System.Type ConcreteProxyClass get { return proxyInterface; } } - public override bool IsInstrumented => EntityMetamodel.BytecodeEnhancementMetadata.EnhancedForLazyLoading; + public override bool IsInstrumented => _enhancementMetadata.EnhancedForLazyLoading; public override System.Type MappedClass { @@ -214,13 +216,18 @@ protected virtual IProxyFactory BuildProxyFactoryInternal(PersistentClass @class } public override void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) + { + AfterInitialize(entity, session); + } + + public override void AfterInitialize(object entity, ISessionImplementor session) { if (IsInstrumented) { - var interceptor = EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(entity); + var interceptor = _enhancementMetadata.ExtractInterceptor(entity); if (interceptor == null) { - interceptor = EntityMetamodel.BytecodeEnhancementMetadata.InjectInterceptor(entity, lazyPropertiesAreUnfetched, session); + interceptor = _enhancementMetadata.InjectInterceptor(entity, session); } else { @@ -274,8 +281,7 @@ public override bool HasUninitializedLazyProperties(object entity) { if (EntityMetamodel.HasLazyProperties) { - var callback = EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(entity); - return callback != null && !callback.IsInitialized; + return _enhancementMetadata.HasAnyUninitializedLazyProperties(entity); } else { @@ -290,7 +296,7 @@ internal override ISet GetUninitializedLazyProperties(object entity) return CollectionHelper.EmptySet(); } - return EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(entity); + return _enhancementMetadata.GetUninitializedLazyProperties(entity); } public override bool IsLifecycleImplementor diff --git a/src/NHibernate/Tuple/StandardProperty.cs b/src/NHibernate/Tuple/StandardProperty.cs index 4349e815b99..59bb5b10782 100644 --- a/src/NHibernate/Tuple/StandardProperty.cs +++ b/src/NHibernate/Tuple/StandardProperty.cs @@ -96,6 +96,8 @@ public bool IsNullable get { return nullable; } } + // Since 5.3 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] public bool IsDirtyCheckable(bool hasUninitializedProperties) { return IsDirtyCheckable() && (!hasUninitializedProperties || !IsLazy); @@ -121,4 +123,4 @@ public FetchMode? FetchMode get { return fetchMode; } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Type/TypeHelper.cs b/src/NHibernate/Type/TypeHelper.cs index a447ef7f64e..56a676cc87a 100644 --- a/src/NHibernate/Type/TypeHelper.cs +++ b/src/NHibernate/Type/TypeHelper.cs @@ -265,12 +265,34 @@ public static object[] ReplaceAssociations(object[] original, object[] target, I /// Does the entity currently hold any uninitialized property values? /// The session from which the dirty check request originated. /// Array containing indices of the dirty properties, or null if no properties considered dirty. + // Since 5.3 + [Obsolete("Use overload without anyUninitializedProperties parameter instead")] public static int[] FindDirty(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, bool anyUninitializedProperties, ISessionImplementor session) + { + return FindDirty(properties, currentState, previousState, includeColumns, session); + } + + /// + /// Determine if any of the given field values are dirty, returning an array containing + /// indices of the dirty fields. + /// If it is determined that no fields are dirty, null is returned. + /// + /// The property definitions + /// The current state of the entity + /// The baseline state of the entity + /// Columns to be included in the dirty checking, per property + /// The session from which the dirty check request originated. + /// Array containing indices of the dirty properties, or null if no properties considered dirty. + public static int[] FindDirty(StandardProperty[] properties, + object[] currentState, + object[] previousState, + bool[][] includeColumns, + ISessionImplementor session) { int[] results = null; int count = 0; @@ -278,7 +300,7 @@ public static int[] FindDirty(StandardProperty[] properties, for (int i = 0; i < span; i++) { - var dirty = Dirty(properties, currentState, previousState, includeColumns, anyUninitializedProperties, session, i); + var dirty = Dirty(properties, currentState, previousState, includeColumns, session, i); if (dirty) { if (results == null) @@ -300,13 +322,13 @@ public static int[] FindDirty(StandardProperty[] properties, } } - private static bool Dirty(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, bool anyUninitializedProperties, ISessionImplementor session, int i) + private static bool Dirty(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, ISessionImplementor session, int i) { if (Equals(LazyPropertyInitializer.UnfetchedProperty, currentState[i])) return false; if (Equals(LazyPropertyInitializer.UnfetchedProperty, previousState[i])) return true; - return properties[i].IsDirtyCheckable(anyUninitializedProperties) && + return properties[i].IsDirtyCheckable() && properties[i].Type.IsDirty(previousState[i], currentState[i], includeColumns[i], session); } @@ -322,12 +344,34 @@ private static bool Dirty(StandardProperty[] properties, object[] currentState, /// Does the entity currently hold any uninitialized property values? /// The session from which the dirty check request originated. /// Array containing indices of the modified properties, or null if no properties considered modified. + // Since 5.3 + [Obsolete("Use the overload without anyUninitializedProperties parameter.")] public static int[] FindModified(StandardProperty[] properties, object[] currentState, object[] previousState, bool[][] includeColumns, bool anyUninitializedProperties, ISessionImplementor session) + { + return FindModified(properties, currentState, previousState, includeColumns, session); + } + + /// + /// Determine if any of the given field values are modified, returning an array containing + /// indices of the modified fields. + /// If it is determined that no fields are dirty, null is returned. + /// + /// The property definitions + /// The current state of the entity + /// The baseline state of the entity + /// Columns to be included in the mod checking, per property + /// The session from which the dirty check request originated. + /// Array containing indices of the modified properties, or null if no properties considered modified. + public static int[] FindModified(StandardProperty[] properties, + object[] currentState, + object[] previousState, + bool[][] includeColumns, + ISessionImplementor session) { int[] results = null; int count = 0; @@ -337,7 +381,7 @@ public static int[] FindModified(StandardProperty[] properties, { bool dirty = !Equals(LazyPropertyInitializer.UnfetchedProperty, currentState[i]) && - properties[i].IsDirtyCheckable(anyUninitializedProperties) + properties[i].IsDirtyCheckable() && properties[i].Type.IsModified(previousState[i], currentState[i], includeColumns[i], session); if (dirty) diff --git a/src/NHibernate/nhibernate-mapping.xsd b/src/NHibernate/nhibernate-mapping.xsd index 1b4c13b99a7..ed1ce498221 100644 --- a/src/NHibernate/nhibernate-mapping.xsd +++ b/src/NHibernate/nhibernate-mapping.xsd @@ -280,6 +280,7 @@ + @@ -1168,6 +1169,7 @@ + From 6801e63ce31e306289b47782d2c3d86b87087c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Sun, 10 Feb 2019 19:37:08 +0100 Subject: [PATCH 2/3] Obsolete some more members And do a renaming --- src/NHibernate/Engine/StatefulPersistenceContext.cs | 6 ++++-- src/NHibernate/Mapping/ByCode/IComponentMapper.cs | 2 +- src/NHibernate/Persister/Entity/AbstractEntityPersister.cs | 2 ++ src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs | 3 +++ src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs | 5 ----- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/NHibernate/Engine/StatefulPersistenceContext.cs b/src/NHibernate/Engine/StatefulPersistenceContext.cs index 50ccb500d0e..021bf96bd23 100644 --- a/src/NHibernate/Engine/StatefulPersistenceContext.cs +++ b/src/NHibernate/Engine/StatefulPersistenceContext.cs @@ -513,6 +513,8 @@ public CollectionEntry GetCollectionEntry(IPersistentCollection coll) } /// Adds an entity to the internal caches. + // Since v5.3 + [Obsolete("Use overload without lazyPropertiesAreUnfetched parameter")] public EntityEntry AddEntity(object entity, Status status, object[] loadedState, EntityKey entityKey, object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, bool disableVersionIncrement, bool lazyPropertiesAreUnfetched) @@ -536,15 +538,15 @@ public EntityEntry AddEntity(object entity, Status status, object[] loadedState, /// Generates an appropriate EntityEntry instance and adds it /// to the event source's internal caches. /// + // Since v5.3 + [Obsolete("Use overload without lazyPropertiesAreUnfetched parameter")] public EntityEntry AddEntry(object entity, Status status, object[] loadedState, object rowId, object id, object version, LockMode lockMode, bool existsInDatabase, IEntityPersister persister, bool disableVersionIncrement, bool lazyPropertiesAreUnfetched) { EntityEntry e = -#pragma warning disable 618 new EntityEntry(status, loadedState, rowId, id, version, lockMode, existsInDatabase, persister, disableVersionIncrement, lazyPropertiesAreUnfetched); -#pragma warning restore 618 entityEntries[entity] = e; SetHasNonReadOnlyEnties(status); diff --git a/src/NHibernate/Mapping/ByCode/IComponentMapper.cs b/src/NHibernate/Mapping/ByCode/IComponentMapper.cs index a4ca3ac0100..a13d769b6cd 100644 --- a/src/NHibernate/Mapping/ByCode/IComponentMapper.cs +++ b/src/NHibernate/Mapping/ByCode/IComponentMapper.cs @@ -34,7 +34,7 @@ public interface IComponentAttributesMapper : IEntityPropertyMapper public interface IComponentMapper : IComponentAttributesMapper, IPropertyContainerMapper {} - public static class ComponentAttributesMapper + public static class ComponentAttributesMapperExtensions { // 6.0 TODO: Move to IComponentAttributesMapper public static void LazyGroup(this IComponentAttributesMapper mapper, string name) diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index 7cb14034d14..ef644bba041 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -4352,6 +4352,8 @@ public bool HasUpdateGeneratedProperties get { return entityMetamodel.HasUpdateGeneratedValues; } } + // Since v5.3 + [Obsolete("Use overload without lazyPropertiesAreUnfetched parameter")] public void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { EntityTuplizer.AfterInitialize(entity, session); diff --git a/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs index 68406fb252b..d6af0c552e0 100644 --- a/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/AbstractEntityTuplizer.cs @@ -224,8 +224,11 @@ public object GetPropertyValue(object entity, string propertyPath) } } + // Since v5.3 + [Obsolete("Use overload without lazyPropertiesAreUnfetched parameter")] public virtual void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { + AfterInitialize(entity, session); } public virtual void AfterInitialize(object entity, ISessionImplementor session) diff --git a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs index a2a669914ee..79285ce1ea9 100644 --- a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs @@ -215,11 +215,6 @@ protected virtual IProxyFactory BuildProxyFactoryInternal(PersistentClass @class return Cfg.Environment.BytecodeProvider.ProxyFactoryFactory.BuildProxyFactory(); } - public override void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) - { - AfterInitialize(entity, session); - } - public override void AfterInitialize(object entity, ISessionImplementor session) { if (IsInstrumented) From c11c84f2300d5ceaf2ea64973eccfb7523c7f6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Sun, 10 Feb 2019 20:08:40 +0100 Subject: [PATCH 3/3] Document lazy-groups --- doc/reference/modules/basic_mapping.xml | 44 ++++++++++++++++++------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/doc/reference/modules/basic_mapping.xml b/doc/reference/modules/basic_mapping.xml index bf6b2f4342d..f30fa154507 100644 --- a/doc/reference/modules/basic_mapping.xml +++ b/doc/reference/modules/basic_mapping.xml @@ -1694,6 +1694,7 @@ + lazy (optional - defaults to false): specifies that this property is lazy. A lazy property is not loaded when the object is initially loaded, unless the fetch mode has been overridden - in a specific query. Values for lazy properties are loaded when any lazy - property of the object is accessed. Having lazy properties causes instances - of the entity to be loaded as proxies. Theses proxies ignore the class - proxy setting and always derives from the persistent class, - requiring its members to be overridable. + in a specific query. Values for lazy properties are loaded per lazy-group. + + + Having lazy properties causes instances of the entity to be loaded as proxies. + Theses proxies ignore the class proxy setting and always + derives from the persistent class, requiring its members to be overridable. + + + + + lazy-group (optional - defaults to DEFAULT): + if the property is lazy, its lazy-loading group. When a lazy property is accessed, + the other lazy properties of the lazy group are also loaded with it. - + not-null (optional - defaults to false): sets the column nullability for DDL generation. - + unique (optional - defaults to false): sets the column uniqueness for DDL generation. Use unique-key instead if the value is unique only in combination with other properties. - + unique-key (optional): a logical name for an unique index for DDL generation. The column will be included in @@ -1801,7 +1811,7 @@ logical name. The actual index name depends on the dialect. - + index (optional): a logical name for an index for DDL generation. The column will be included in @@ -1809,19 +1819,19 @@ name. The actual index name depends on the dialect. - + length (optional): if the type takes a length and does not already specify it, its length. - + precision (optional): if the type takes a precision and does not already specify it, its precision. - + scale (optional): if the type takes a scale and does not already specify it, its scale. @@ -2598,6 +2608,7 @@ + @@ -2660,6 +2672,14 @@ + + lazy-group (optional - defaults to DEFAULT): + If the component is lazy, its lazy-loading group. When a lazy property is accessed + on an object, included when the property is a component, the other lazy properties + of the lazy group are also loaded with it. + + + unique (optional - defaults to false): Specifies that an unique constraint exists upon all mapped columns of the component.