From 116398d6d16b1f1415285936aab07c98508d2149 Mon Sep 17 00:00:00 2001 From: maca88 Date: Wed, 30 Jan 2019 20:01:43 +0100 Subject: [PATCH] Add support for fetching an individual lazy property with hql and linq provider (#1922) --- .../FetchLazyPropertiesFixture.cs | 1008 +++++++++++++++++ .../FetchLazyProperties/Address.cs | 17 + .../FetchLazyProperties/Animal.cs | 18 + .../FetchLazyProperties/Cat.cs | 10 + .../FetchLazyProperties/Continent.cs | 15 + .../FetchLazyProperties/Dog.cs | 7 + .../FetchLazyPropertiesFixture.cs | 996 ++++++++++++++++ .../FetchLazyProperties/Mappings.hbm.xml | 60 + .../FetchLazyProperties/Person.cs | 28 + .../Async/Impl/MultiCriteriaImpl.cs | 8 +- src/NHibernate/Async/Impl/MultiQueryImpl.cs | 8 +- src/NHibernate/Async/Loader/Loader.cs | 144 ++- .../Async/Multi/QueryBatchItemBase.cs | 10 +- .../Entity/AbstractEntityPersister.cs | 112 +- .../Async/Persister/Entity/ILoadable.cs | 65 ++ .../Bytecode/LazyPropertiesMetadata.cs | 16 + src/NHibernate/Hql/Ast/ANTLR/Hql.g | 2 +- src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs | 66 +- src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g | 10 +- .../Hql/Ast/ANTLR/Tree/FromElement.cs | 14 +- .../Hql/Ast/ANTLR/Tree/FromElementType.cs | 16 +- src/NHibernate/Hql/Ast/HqlTreeBuilder.cs | 5 + src/NHibernate/Impl/MultiCriteriaImpl.cs | 8 +- src/NHibernate/Impl/MultiQueryImpl.cs | 8 +- .../Linq/EagerFetchingExtensionMethods.cs | 22 +- .../Linq/FetchLazyPropertiesExpressionNode.cs | 24 + .../Linq/FetchLazyPropertiesResultOperator.cs | 33 + src/NHibernate/Linq/IntermediateHqlTree.cs | 12 +- src/NHibernate/Linq/NhRelinqQueryParser.cs | 3 + .../QueryReferenceExpressionFlattener.cs | 1 + .../Linq/ReWriters/ResultOperatorRewriter.cs | 1 + .../Linq/Visitors/QueryModelVisitor.cs | 1 + .../Linq/Visitors/QuerySourceLocator.cs | 2 +- .../ResultOperatorProcessors/ProcessFetch.cs | 106 +- .../ProcessFetchLazyProperties.cs | 11 + .../Visitors/SubQueryFromClauseFlattener.cs | 3 +- src/NHibernate/Loader/Hql/QueryLoader.cs | 10 + src/NHibernate/Loader/Loader.cs | 157 ++- src/NHibernate/Multi/QueryBatchItemBase.cs | 10 +- .../Entity/AbstractEntityPersister.cs | 166 ++- src/NHibernate/Persister/Entity/ILoadable.cs | 53 + src/NHibernate/Persister/Entity/IQueryable.cs | 16 + 42 files changed, 3169 insertions(+), 113 deletions(-) create mode 100644 src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/Address.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/Animal.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/Cat.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/Continent.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/Dog.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs create mode 100644 src/NHibernate.Test/FetchLazyProperties/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/FetchLazyProperties/Person.cs create mode 100644 src/NHibernate/Linq/FetchLazyPropertiesExpressionNode.cs create mode 100644 src/NHibernate/Linq/FetchLazyPropertiesResultOperator.cs create mode 100644 src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetchLazyProperties.cs diff --git a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs new file mode 100644 index 00000000000..d6a85b966c0 --- /dev/null +++ b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -0,0 +1,1008 @@ +//------------------------------------------------------------------------------ +// +// 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; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Cfg; +using NHibernate.Hql.Ast.ANTLR; +using NHibernate.Linq; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.FetchLazyProperties +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class FetchLazyPropertiesFixtureAsync : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override string[] Mappings + { + get { return new[] { "FetchLazyProperties.Mappings.hbm.xml" }; } + } + + protected override bool AppliesTo(Dialect.Dialect dialect) + { + return dialect.SupportsTemporaryTables; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.Properties[Environment.CacheProvider] = typeof(HashtableCacheProvider).AssemblyQualifiedName; + configuration.Properties[Environment.UseSecondLevelCache] = "true"; + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var currAnimalId = 1; + Person lastPerson = null; + for (var i = 2; i > 0; i--) + { + var person = lastPerson = GeneratePerson(i, lastPerson); + person.Pets.Add(GenerateCat(currAnimalId++, person)); + person.Pets.Add(GenerateDog(currAnimalId++, person)); + s.Save(person); + } + + tx.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from Animal").ExecuteUpdate(); + s.CreateQuery("update Person set BestFriend = null").ExecuteUpdate(); + s.CreateQuery("delete from Person").ExecuteUpdate(); + s.CreateQuery("delete from Continent").ExecuteUpdate(); + tx.Commit(); + } + } + + #region FetchComponent + + [Test] + public async Task TestHqlFetchComponentAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person fetch Address where Id = 1").UniqueResultAsync()); + } + + AssertFetchComponent(person); + } + + [Test] + public async Task TestLinqFetchComponentAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query().Fetch(o => o.Address).FirstOrDefaultAsync(o => o.Id == 1)); + } + + AssertFetchComponent(person); + } + + private static void AssertFetchComponent(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + } + + #endregion + + #region FetchFormula + + [Test] + public async Task TestHqlFetchFormulaAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person fetch Formula where Id = 1").UniqueResultAsync()); + } + + AssertFetchFormula(person); + } + + [Test] + public async Task TestLinqFetchFormulaAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query().Fetch(o => o.Formula).FirstOrDefaultAsync(o => o.Id == 1)); + } + + AssertFetchFormula(person); + } + + private static void AssertFetchFormula(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(person.Formula, Is.EqualTo(1)); + } + + #endregion + + #region FetchProperty + + [Test] + public async Task TestHqlFetchPropertyAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person fetch Image where Id = 1").UniqueResultAsync()); + } + + AssertFetchProperty(person); + } + + [Test] + public async Task TestLinqFetchPropertyAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query().Fetch(o => o.Image).FirstOrDefaultAsync(o => o.Id == 1)); + } + + AssertFetchProperty(person); + } + + private static void AssertFetchProperty(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(person.Image, Has.Length.EqualTo(1)); + } + + #endregion + + + #region FetchComponentAndFormulaTwoQueryReadOnly + + [TestCase(true)] + [TestCase(false)] + public async Task TestHqlFetchComponentAndFormulaTwoQueryReadOnlyAsync(bool readOnly, CancellationToken cancellationToken = default(CancellationToken)) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.CreateQuery("from Person fetch Address where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + person = await (s.CreateQuery("from Person fetch Formula where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + + await (tx.CommitAsync(cancellationToken)); + } + + AssertFetchComponentAndFormulaTwoQuery(person); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestLinqFetchComponentAndFormulaTwoQueryAsync(bool readOnly, CancellationToken cancellationToken = default(CancellationToken)) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.Query().Fetch(o => o.Address).WithOptions(o => o.SetReadOnly(readOnly)).FirstOrDefaultAsync(o => o.Id == 1, cancellationToken)); + person = await (s.Query().Fetch(o => o.Formula).WithOptions(o => o.SetReadOnly(readOnly)).FirstOrDefaultAsync(o => o.Id == 1, cancellationToken)); + + await (tx.CommitAsync(cancellationToken)); + } + + AssertFetchComponentAndFormulaTwoQuery(person); + } + + private static void AssertFetchComponentAndFormulaTwoQuery(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + Assert.That(person.Formula, Is.EqualTo(1)); + } + + #endregion + + #region FetchAllProperties + + [Test] + public async Task TestHqlFetchAllPropertiesAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person fetch all properties where Id = 1").UniqueResultAsync()); + } + + AssertFetchAllProperties(person); + } + + [Test] + public async Task TestLinqFetchAllPropertiesAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query().FetchLazyProperties().FirstOrDefaultAsync(o => o.Id == 1)); + } + + AssertFetchAllProperties(person); + } + + private static void AssertFetchAllProperties(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(person.Image, Has.Length.EqualTo(1)); + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + Assert.That(person.Formula, Is.EqualTo(1)); + } + + #endregion + + #region FetchAllPropertiesIndividually + + [Test] + public async Task TestHqlFetchAllPropertiesIndividuallyAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person fetch Image fetch Address fetch Formula fetch Image where Id = 1").UniqueResultAsync()); + } + + AssertFetchAllProperties(person); + } + + [Test] + public async Task TestLinqFetchAllPropertiesIndividuallyAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query().Fetch(o => o.Image).Fetch(o => o.Address).Fetch(o => o.Formula).FirstOrDefaultAsync(o => o.Id == 1)); + } + + AssertFetchAllProperties(person); + } + + #endregion + + #region FetchFormulaAndManyToOneComponent + + [Test] + public async Task TestHqlFetchFormulaAndManyToOneComponentAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address where p.Id = 1") + .UniqueResultAsync()); + } + + AssertFetchFormulaAndManyToOneComponent(person); + } + + [Test] + public async Task TestLinqFetchFormulaAndManyToOneComponentAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query() + .Fetch(o => o.Formula) + .Fetch(o => o.BestFriend).ThenFetch(o => o.Address) + .FirstOrDefaultAsync(o => o.Id == 1)); + + } + + AssertFetchFormulaAndManyToOneComponent(person); + } + + private static void AssertFetchFormulaAndManyToOneComponent(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.False); + + Assert.That(person.Formula, Is.EqualTo(1)); + Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2")); + Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2")); + } + + #endregion + + #region FetchFormulaAndManyToOneComponentList + + [TestCase(true)] + [TestCase(false)] + public async Task TestHqlFetchFormulaAndManyToOneComponentListAsync(bool descending, CancellationToken cancellationToken = default(CancellationToken)) + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address order by p.Id" + (descending ? " desc" : "")) + .ListAsync(cancellationToken))).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchFormulaAndManyToOneComponentList(person); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestLinqFetchFormulaAndManyToOneComponentListAsync(bool descending, CancellationToken cancellationToken = default(CancellationToken)) + { + Person person; + using (var s = OpenSession()) + { + IQueryable query = s.Query() + .Fetch(o => o.Formula) + .Fetch(o => o.BestFriend).ThenFetch(o => o.Address); + query = descending ? query.OrderByDescending(o => o.Id) : query.OrderBy(o => o.Id); + person = (await (query + .ToListAsync(cancellationToken))) + .FirstOrDefault(o => o.Id == 1); + } + + AssertFetchFormulaAndManyToOneComponentList(person); + } + + private static void AssertFetchFormulaAndManyToOneComponentList(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.True); + + Assert.That(person.Formula, Is.EqualTo(1)); + Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2")); + Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2")); + } + + #endregion + + #region FetchManyToOneAllProperties + + [Test] + public async Task TestHqlFetchManyToOneAllPropertiesAsync() + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.CreateQuery("from Person p left join fetch p.BestFriend fetch all properties") + .ListAsync())).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchManyToOneAllProperties(person); + } + + [Test] + public async Task TestLinqFetchManyToOneAllPropertiesAsync() + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.Query() + .Fetch(o => o.BestFriend).FetchLazyProperties() + .ToListAsync())) + .FirstOrDefault(o => o.Id == 1); + } + + AssertFetchManyToOneAllProperties(person); + } + + private static void AssertFetchManyToOneAllProperties(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.True); + + Assert.That(person.BestFriend.Formula, Is.EqualTo(2)); + Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2")); + Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2")); + Assert.That(person.BestFriend.Image, Has.Length.EqualTo(2)); + } + + #endregion + + #region FetchFormulaAndOneToManyComponent + + [Test] + public async Task TestHqlFetchFormulaAndOneToManyComponentAsync() + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.CreateQuery("from Person p fetch p.Formula left join fetch p.Dogs dog fetch dog.Address where p.Id = 1") + .ListAsync())) + .FirstOrDefault(); + } + + AssertFetchFormulaAndOneToManyComponent(person); + } + + [Test] + public async Task TestLinqFetchFormulaAndOneToManyComponentAsync() + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.Query() + .Fetch(o => o.Formula) + .FetchMany(o => o.Dogs).ThenFetch(o => o.Address) + .Where(o => o.Id == 1) + .ToListAsync())) + .FirstOrDefault(); + } + + AssertFetchFormulaAndOneToManyComponent(person); + } + + private static void AssertFetchFormulaAndOneToManyComponent(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(NHibernateUtil.IsInitialized(person.Dogs), Is.True); + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.False); + Assert.That(NHibernateUtil.IsInitialized(person.Pets), Is.False); + + Assert.That(person.Formula, Is.EqualTo(1)); + Assert.That(person.Dogs, Has.Count.EqualTo(1)); + foreach (var dog in person.Dogs) + { + Assert.That(NHibernateUtil.IsPropertyInitialized(dog, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(dog, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(dog, "Address"), Is.True); + + Assert.That(dog.Address.City, Is.EqualTo("City1")); + Assert.That(dog.Address.Country, Is.EqualTo("Country1")); + } + } + + #endregion + + #region FetchOneToManyProperty + + [Test] + public async Task TestHqlFetchOneToManyPropertyAsync() + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.CreateQuery("from Person p left join fetch p.Cats cat fetch cat.SecondImage where p.Id = 1") + .ListAsync())) + .FirstOrDefault(); + } + + AssertFetchOneToManyProperty(person); + } + + [Test] + public async Task TestLinqFetchOneToManyPropertyAsync() + { + Person person; + using (var s = OpenSession()) + { + person = (await (s.Query() + .FetchMany(o => o.Cats).ThenFetch(o => o.SecondImage) + .Where(o => o.Id == 1) + .ToListAsync())) + .FirstOrDefault(); + } + + AssertFetchOneToManyProperty(person); + } + + private static void AssertFetchOneToManyProperty(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.False); + Assert.That(NHibernateUtil.IsInitialized(person.Pets), Is.False); + Assert.That(NHibernateUtil.IsInitialized(person.Cats), Is.True); + + Assert.That(person.Cats, Has.Count.EqualTo(1)); + foreach (var cat in person.Cats) + { + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.False); + + Assert.That(cat.SecondImage, Has.Length.EqualTo(6)); + } + } + + #endregion + + #region FetchNotMappedProperty + + [Test] + public void TestHqlFetchNotMappedPropertyAsync() + { + using (var s = OpenSession()) + { + Assert.ThrowsAsync( + async () => + { + var person = await (s.CreateQuery("from Person p fetch p.BirthYear where p.Id = 1").UniqueResultAsync()); + }); + } + } + + [Test] + public void TestLinqFetchNotMappedPropertyAsync() + { + using (var s = OpenSession()) + { + Assert.ThrowsAsync( + async () => + { + var person = await (s.Query().Fetch(o => o.BirthYear).FirstOrDefaultAsync(o => o.Id == 1)); + }); + } + } + + #endregion + + #region FetchComponentManyToOne + + [Test] + public async Task TestHqlFetchComponentManyToOneAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person p fetch p.Address left join fetch p.Address.Continent where p.Id = 1").UniqueResultAsync()); + } + + AssertFetchComponentManyToOne(person); + } + + [Test] + public async Task TestLinqFetchComponentManyToOneAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.Query().Fetch(o => o.Address).ThenFetch(o => o.Continent).FirstOrDefaultAsync(o => o.Id == 1)); + } + + AssertFetchComponentManyToOne(person); + } + + private static void AssertFetchComponentManyToOne(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(NHibernateUtil.IsInitialized(person.Address.Continent), Is.True); + + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + Assert.That(person.Address.Continent.Name, Is.EqualTo("Continent1")); + } + + #endregion + + #region FetchSubClassFormula + + [Test] + public async Task TestHqlFetchSubClassFormulaAsync() + { + Animal animal; + using (var s = OpenSession()) + { + animal = await (s.CreateQuery("from Animal a fetch a.SecondFormula where a.Id = 1").UniqueResultAsync()); + } + + AssertFetchSubClassFormula(animal); + } + + [Test] + public async Task TestLinqFetchSubClassFormulaAsync() + { + Animal animal; + using (var s = OpenSession()) + { + animal = await (s.Query().Fetch(o => ((Cat) o).SecondFormula).FirstAsync(o => o.Id == 1)); + } + + AssertFetchSubClassFormula(animal); + } + + private static void AssertFetchSubClassFormula(Animal animal) + { + Assert.That(animal, Is.AssignableTo(typeof(Cat))); + var cat = (Cat) animal; + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.False); + } + + #endregion + + #region FetchSubClassProperty + + [Test] + public async Task TestHqlFetchSubClassPropertyAsync() + { + Animal animal; + using (var s = OpenSession()) + { + animal = await (s.CreateQuery("from Animal a fetch a.SecondImage where a.Id = 1").UniqueResultAsync()); + } + + AssertFetchSubClassProperty(animal); + } + + [Test] + public async Task TestLinqFetchSubClassPropertyAsync() + { + Animal animal; + using (var s = OpenSession()) + { + animal = await (s.Query().Fetch(o => ((Cat) o).SecondImage).FirstAsync(o => o.Id == 1)); + } + + AssertFetchSubClassProperty(animal); + } + + private static void AssertFetchSubClassProperty(Animal animal) + { + Assert.That(animal, Is.AssignableTo(typeof(Cat))); + var cat = (Cat) animal; + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.False); + } + + #endregion + + #region FetchSubClassAllProperty + + [Test] + public async Task TestHqlFetchSubClassAllPropertiesAsync() + { + Animal animal; + using (var s = OpenSession()) + { + animal = await (s.CreateQuery("from Animal a fetch all properties where a.Id = 1").UniqueResultAsync()); + } + + AssertFetchSubClassAllProperties(animal); + } + + [Test] + public async Task TestLinqFetchSubClassAllPropertiesAsync() + { + Animal animal; + using (var s = OpenSession()) + { + animal = await (s.Query().FetchLazyProperties().FirstAsync(o => o.Id == 1)); + } + + AssertFetchSubClassAllProperties(animal); + } + + private static void AssertFetchSubClassAllProperties(Animal animal) + { + Assert.That(animal, Is.AssignableTo(typeof(Cat))); + var cat = (Cat) animal; + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.True); + } + + #endregion + + #region FetchAllPropertiesWithFetchProperty + + [Test] + public void TestHqlFetchAllPropertiesWithFetchPropertyAsync() + { + using (var s = OpenSession()) + { + Assert.ThrowsAsync( + async () => + { + var person = await (s.CreateQuery("from Person p fetch p.Address fetch all properties where p.Id = 1").UniqueResultAsync()); + }); + Assert.ThrowsAsync( + async () => + { + var person = await (s.CreateQuery("from Person p fetch all properties fetch p.Address where p.Id = 1").UniqueResultAsync()); + }); + } + } + + [Test] + public void TestLinqFetchAllPropertiesWithFetchPropertyAsync() + { + using (var s = OpenSession()) + { + Assert.ThrowsAsync( + async () => + { + var person = await (s.Query().Fetch(o => o.Address).FetchLazyProperties().FirstOrDefaultAsync(o => o.Id == 1)); + }); + Assert.ThrowsAsync( + async () => + { + var person = await (s.Query().FetchLazyProperties().Fetch(o => o.Address).FirstOrDefaultAsync(o => o.Id == 1)); + }); + } + } + + #endregion + + [Test] + public async Task TestHqlFetchComponentAliasAsync() + { + Person person; + using (var s = OpenSession()) + { + person = await (s.CreateQuery("from Person p fetch p.Address where p.Id = 1").UniqueResultAsync()); + } + + AssertFetchComponent(person); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestFetchComponentAndFormulaTwoQueryCacheAsync(bool readOnly, CancellationToken cancellationToken = default(CancellationToken)) + { + await (TestLinqFetchComponentAndFormulaTwoQueryAsync(readOnly, cancellationToken)); + + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.GetAsync(1, cancellationToken)); + + await (tx.CommitAsync(cancellationToken)); + } + + AssertFetchComponentAndFormulaTwoQuery(person); + } + + [Test] + public async Task TestFetchComponentCacheAsync() + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.Query().Fetch(o => o.Address).FirstOrDefaultAsync(o => o.Id == 1)); + AssertFetchComponent(person); + await (tx.CommitAsync()); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.GetAsync(1)); + AssertFetchComponent(person); + // Will reset the cache item + person.Name = "Test"; + + await (tx.CommitAsync()); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.GetAsync(1)); + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + await (tx.CommitAsync()); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestFetchAfterPropertyIsInitializedAsync(bool readOnly, CancellationToken cancellationToken = default(CancellationToken)) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.CreateQuery("from Person fetch Address where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + person.Image = new byte[10]; + person = await (s.CreateQuery("from Person fetch Image where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + + await (tx.CommitAsync(cancellationToken)); + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(person.Image, Has.Length.EqualTo(10)); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.CreateQuery("from Person where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + person.Image = new byte[1]; + + await (tx.CommitAsync(cancellationToken)); + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestFetchAfterEntityIsInitializedAsync(bool readOnly, CancellationToken cancellationToken = default(CancellationToken)) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = await (s.CreateQuery("from Person where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + var image = person.Image; + person = await (s.CreateQuery("from Person fetch Image where Id = 1").SetReadOnly(readOnly).UniqueResultAsync(cancellationToken)); + + await (tx.CommitAsync(cancellationToken)); + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + } + + private static Person GeneratePerson(int i, Person bestFriend) + { + return new Person + { + Id = i, + Name = $"Person{i}", + Address = new Address + { + City = $"City{i}", + Country = $"Country{i}", + Continent = GenerateContinent(i) + }, + Image = new byte[i], + BestFriend = bestFriend + }; + } + + private static Continent GenerateContinent(int i) + { + return new Continent + { + Id = i, + Name = $"Continent{i}" + }; + } + + private static Cat GenerateCat(int i, Person owner) + { + return new Cat + { + Id = i, + Address = new Address + { + City = owner.Address.City, + Country = owner.Address.Country + }, + Image = new byte[i], + Name = $"Cat{i}", + SecondImage = new byte[i * 2], + Owner = owner + }; + } + + private static Dog GenerateDog(int i, Person owner) + { + return new Dog + { + Id = i, + Address = new Address + { + City = owner.Address.City, + Country = owner.Address.Country + }, + Image = new byte[i * 3], + Name = $"Dog{i}", + Owner = owner + }; + } + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/Address.cs b/src/NHibernate.Test/FetchLazyProperties/Address.cs new file mode 100644 index 00000000000..4568945fdec --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/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.FetchLazyProperties +{ + public class Address + { + public string City { get; set; } + + public string Country { get; set; } + + public Continent Continent { get; set; } + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/Animal.cs b/src/NHibernate.Test/FetchLazyProperties/Animal.cs new file mode 100644 index 00000000000..f18a8b76ffe --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/Animal.cs @@ -0,0 +1,18 @@ + +namespace NHibernate.Test.FetchLazyProperties +{ + public abstract class Animal + { + public virtual int Id { get; set; } + + public virtual int Formula { get; set; } + + public virtual string Name { get; set; } + + public virtual Address Address { get; set; } + + public virtual byte[] Image { get; set; } + + public virtual Person Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/Cat.cs b/src/NHibernate.Test/FetchLazyProperties/Cat.cs new file mode 100644 index 00000000000..790b230983d --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/Cat.cs @@ -0,0 +1,10 @@ + +namespace NHibernate.Test.FetchLazyProperties +{ + public class Cat : Animal + { + public virtual string SecondFormula { get; set; } + + public virtual byte[] SecondImage { get; set; } + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/Continent.cs b/src/NHibernate.Test/FetchLazyProperties/Continent.cs new file mode 100644 index 00000000000..bf9d0af0b24 --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/Continent.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.FetchLazyProperties +{ + public class Continent + { + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/Dog.cs b/src/NHibernate.Test/FetchLazyProperties/Dog.cs new file mode 100644 index 00000000000..1019be4df3b --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/Dog.cs @@ -0,0 +1,7 @@ + +namespace NHibernate.Test.FetchLazyProperties +{ + public class Dog : Animal + { + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs new file mode 100644 index 00000000000..2f5bf5442d3 --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -0,0 +1,996 @@ +using System; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Cfg; +using NHibernate.Hql.Ast.ANTLR; +using NHibernate.Linq; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.FetchLazyProperties +{ + [TestFixture] + public class FetchLazyPropertiesFixture : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override string[] Mappings + { + get { return new[] { "FetchLazyProperties.Mappings.hbm.xml" }; } + } + + protected override bool AppliesTo(Dialect.Dialect dialect) + { + return dialect.SupportsTemporaryTables; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + configuration.Properties[Environment.CacheProvider] = typeof(HashtableCacheProvider).AssemblyQualifiedName; + configuration.Properties[Environment.UseSecondLevelCache] = "true"; + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var currAnimalId = 1; + Person lastPerson = null; + for (var i = 2; i > 0; i--) + { + var person = lastPerson = GeneratePerson(i, lastPerson); + person.Pets.Add(GenerateCat(currAnimalId++, person)); + person.Pets.Add(GenerateDog(currAnimalId++, person)); + s.Save(person); + } + + tx.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from Animal").ExecuteUpdate(); + s.CreateQuery("update Person set BestFriend = null").ExecuteUpdate(); + s.CreateQuery("delete from Person").ExecuteUpdate(); + s.CreateQuery("delete from Continent").ExecuteUpdate(); + tx.Commit(); + } + } + + #region FetchComponent + + [Test] + public void TestHqlFetchComponent() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person fetch Address where Id = 1").UniqueResult(); + } + + AssertFetchComponent(person); + } + + [Test] + public void TestLinqFetchComponent() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query().Fetch(o => o.Address).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchComponent(person); + } + + private static void AssertFetchComponent(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + } + + #endregion + + #region FetchFormula + + [Test] + public void TestHqlFetchFormula() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person fetch Formula where Id = 1").UniqueResult(); + } + + AssertFetchFormula(person); + } + + [Test] + public void TestLinqFetchFormula() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query().Fetch(o => o.Formula).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchFormula(person); + } + + private static void AssertFetchFormula(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(person.Formula, Is.EqualTo(1)); + } + + #endregion + + #region FetchProperty + + [Test] + public void TestHqlFetchProperty() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person fetch Image where Id = 1").UniqueResult(); + } + + AssertFetchProperty(person); + } + + [Test] + public void TestLinqFetchProperty() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query().Fetch(o => o.Image).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchProperty(person); + } + + private static void AssertFetchProperty(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(person.Image, Has.Length.EqualTo(1)); + } + + #endregion + + + #region FetchComponentAndFormulaTwoQueryReadOnly + + [TestCase(true)] + [TestCase(false)] + public void TestHqlFetchComponentAndFormulaTwoQueryReadOnly(bool readOnly) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.CreateQuery("from Person fetch Address where Id = 1").SetReadOnly(readOnly).UniqueResult(); + person = s.CreateQuery("from Person fetch Formula where Id = 1").SetReadOnly(readOnly).UniqueResult(); + + tx.Commit(); + } + + AssertFetchComponentAndFormulaTwoQuery(person); + } + + [TestCase(true)] + [TestCase(false)] + public void TestLinqFetchComponentAndFormulaTwoQuery(bool readOnly) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.Query().Fetch(o => o.Address).WithOptions(o => o.SetReadOnly(readOnly)).FirstOrDefault(o => o.Id == 1); + person = s.Query().Fetch(o => o.Formula).WithOptions(o => o.SetReadOnly(readOnly)).FirstOrDefault(o => o.Id == 1); + + tx.Commit(); + } + + AssertFetchComponentAndFormulaTwoQuery(person); + } + + private static void AssertFetchComponentAndFormulaTwoQuery(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + Assert.That(person.Formula, Is.EqualTo(1)); + } + + #endregion + + #region FetchAllProperties + + [Test] + public void TestHqlFetchAllProperties() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person fetch all properties where Id = 1").UniqueResult(); + } + + AssertFetchAllProperties(person); + } + + [Test] + public void TestLinqFetchAllProperties() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query().FetchLazyProperties().FirstOrDefault(o => o.Id == 1); + } + + AssertFetchAllProperties(person); + } + + private static void AssertFetchAllProperties(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(person.Image, Has.Length.EqualTo(1)); + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + Assert.That(person.Formula, Is.EqualTo(1)); + } + + #endregion + + #region FetchAllPropertiesIndividually + + [Test] + public void TestHqlFetchAllPropertiesIndividually() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person fetch Image fetch Address fetch Formula fetch Image where Id = 1").UniqueResult(); + } + + AssertFetchAllProperties(person); + } + + [Test] + public void TestLinqFetchAllPropertiesIndividually() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query().Fetch(o => o.Image).Fetch(o => o.Address).Fetch(o => o.Formula).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchAllProperties(person); + } + + #endregion + + #region FetchFormulaAndManyToOneComponent + + [Test] + public void TestHqlFetchFormulaAndManyToOneComponent() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address where p.Id = 1") + .UniqueResult(); + } + + AssertFetchFormulaAndManyToOneComponent(person); + } + + [Test] + public void TestLinqFetchFormulaAndManyToOneComponent() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query() + .Fetch(o => o.Formula) + .Fetch(o => o.BestFriend).ThenFetch(o => o.Address) + .FirstOrDefault(o => o.Id == 1); + + } + + AssertFetchFormulaAndManyToOneComponent(person); + } + + private static void AssertFetchFormulaAndManyToOneComponent(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.False); + + Assert.That(person.Formula, Is.EqualTo(1)); + Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2")); + Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2")); + } + + #endregion + + #region FetchFormulaAndManyToOneComponentList + + [TestCase(true)] + [TestCase(false)] + public void TestHqlFetchFormulaAndManyToOneComponentList(bool descending) + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address order by p.Id" + (descending ? " desc" : "")) + .List().FirstOrDefault(o => o.Id == 1); + } + + AssertFetchFormulaAndManyToOneComponentList(person); + } + + [TestCase(true)] + [TestCase(false)] + public void TestLinqFetchFormulaAndManyToOneComponentList(bool descending) + { + Person person; + using (var s = OpenSession()) + { + IQueryable query = s.Query() + .Fetch(o => o.Formula) + .Fetch(o => o.BestFriend).ThenFetch(o => o.Address); + query = descending ? query.OrderByDescending(o => o.Id) : query.OrderBy(o => o.Id); + person = query + .ToList() + .FirstOrDefault(o => o.Id == 1); + } + + AssertFetchFormulaAndManyToOneComponentList(person); + } + + private static void AssertFetchFormulaAndManyToOneComponentList(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.True); + + Assert.That(person.Formula, Is.EqualTo(1)); + Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2")); + Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2")); + } + + #endregion + + #region FetchManyToOneAllProperties + + [Test] + public void TestHqlFetchManyToOneAllProperties() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p left join fetch p.BestFriend fetch all properties") + .List().FirstOrDefault(o => o.Id == 1); + } + + AssertFetchManyToOneAllProperties(person); + } + + [Test] + public void TestLinqFetchManyToOneAllProperties() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query() + .Fetch(o => o.BestFriend).FetchLazyProperties() + .ToList() + .FirstOrDefault(o => o.Id == 1); + } + + AssertFetchManyToOneAllProperties(person); + } + + private static void AssertFetchManyToOneAllProperties(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person.BestFriend, "Formula"), Is.True); + + Assert.That(person.BestFriend.Formula, Is.EqualTo(2)); + Assert.That(person.BestFriend.Address.City, Is.EqualTo("City2")); + Assert.That(person.BestFriend.Address.Country, Is.EqualTo("Country2")); + Assert.That(person.BestFriend.Image, Has.Length.EqualTo(2)); + } + + #endregion + + #region FetchFormulaAndOneToManyComponent + + [Test] + public void TestHqlFetchFormulaAndOneToManyComponent() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.Dogs dog fetch dog.Address where p.Id = 1") + .List() + .FirstOrDefault(); + } + + AssertFetchFormulaAndOneToManyComponent(person); + } + + [Test] + public void TestLinqFetchFormulaAndOneToManyComponent() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query() + .Fetch(o => o.Formula) + .FetchMany(o => o.Dogs).ThenFetch(o => o.Address) + .Where(o => o.Id == 1) + .ToList() + .FirstOrDefault(); + } + + AssertFetchFormulaAndOneToManyComponent(person); + } + + private static void AssertFetchFormulaAndOneToManyComponent(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + + Assert.That(NHibernateUtil.IsInitialized(person.Dogs), Is.True); + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.False); + Assert.That(NHibernateUtil.IsInitialized(person.Pets), Is.False); + + Assert.That(person.Formula, Is.EqualTo(1)); + Assert.That(person.Dogs, Has.Count.EqualTo(1)); + foreach (var dog in person.Dogs) + { + Assert.That(NHibernateUtil.IsPropertyInitialized(dog, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(dog, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(dog, "Address"), Is.True); + + Assert.That(dog.Address.City, Is.EqualTo("City1")); + Assert.That(dog.Address.Country, Is.EqualTo("Country1")); + } + } + + #endregion + + #region FetchOneToManyProperty + + [Test] + public void TestHqlFetchOneToManyProperty() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p left join fetch p.Cats cat fetch cat.SecondImage where p.Id = 1") + .List() + .FirstOrDefault(); + } + + AssertFetchOneToManyProperty(person); + } + + [Test] + public void TestLinqFetchOneToManyProperty() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query() + .FetchMany(o => o.Cats).ThenFetch(o => o.SecondImage) + .Where(o => o.Id == 1) + .ToList() + .FirstOrDefault(); + } + + AssertFetchOneToManyProperty(person); + } + + private static void AssertFetchOneToManyProperty(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(NHibernateUtil.IsInitialized(person.BestFriend), Is.False); + Assert.That(NHibernateUtil.IsInitialized(person.Pets), Is.False); + Assert.That(NHibernateUtil.IsInitialized(person.Cats), Is.True); + + Assert.That(person.Cats, Has.Count.EqualTo(1)); + foreach (var cat in person.Cats) + { + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.False); + + Assert.That(cat.SecondImage, Has.Length.EqualTo(6)); + } + } + + #endregion + + #region FetchNotMappedProperty + + [Test] + public void TestHqlFetchNotMappedProperty() + { + using (var s = OpenSession()) + { + Assert.Throws( + () => + { + var person = s.CreateQuery("from Person p fetch p.BirthYear where p.Id = 1").UniqueResult(); + }); + } + } + + [Test] + public void TestLinqFetchNotMappedProperty() + { + using (var s = OpenSession()) + { + Assert.Throws( + () => + { + var person = s.Query().Fetch(o => o.BirthYear).FirstOrDefault(o => o.Id == 1); + }); + } + } + + #endregion + + #region FetchComponentManyToOne + + [Test] + public void TestHqlFetchComponentManyToOne() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p fetch p.Address left join fetch p.Address.Continent where p.Id = 1").UniqueResult(); + } + + AssertFetchComponentManyToOne(person); + } + + [Test] + public void TestLinqFetchComponentManyToOne() + { + Person person; + using (var s = OpenSession()) + { + person = s.Query().Fetch(o => o.Address).ThenFetch(o => o.Continent).FirstOrDefault(o => o.Id == 1); + } + + AssertFetchComponentManyToOne(person); + } + + private static void AssertFetchComponentManyToOne(Person person) + { + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(NHibernateUtil.IsInitialized(person.Address.Continent), Is.True); + + Assert.That(person.Address.City, Is.EqualTo("City1")); + Assert.That(person.Address.Country, Is.EqualTo("Country1")); + Assert.That(person.Address.Continent.Name, Is.EqualTo("Continent1")); + } + + #endregion + + #region FetchSubClassFormula + + [Test] + public void TestHqlFetchSubClassFormula() + { + Animal animal; + using (var s = OpenSession()) + { + animal = s.CreateQuery("from Animal a fetch a.SecondFormula where a.Id = 1").UniqueResult(); + } + + AssertFetchSubClassFormula(animal); + } + + [Test] + public void TestLinqFetchSubClassFormula() + { + Animal animal; + using (var s = OpenSession()) + { + animal = s.Query().Fetch(o => ((Cat) o).SecondFormula).First(o => o.Id == 1); + } + + AssertFetchSubClassFormula(animal); + } + + private static void AssertFetchSubClassFormula(Animal animal) + { + Assert.That(animal, Is.AssignableTo(typeof(Cat))); + var cat = (Cat) animal; + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.False); + } + + #endregion + + #region FetchSubClassProperty + + [Test] + public void TestHqlFetchSubClassProperty() + { + Animal animal; + using (var s = OpenSession()) + { + animal = s.CreateQuery("from Animal a fetch a.SecondImage where a.Id = 1").UniqueResult(); + } + + AssertFetchSubClassProperty(animal); + } + + [Test] + public void TestLinqFetchSubClassProperty() + { + Animal animal; + using (var s = OpenSession()) + { + animal = s.Query().Fetch(o => ((Cat) o).SecondImage).First(o => o.Id == 1); + } + + AssertFetchSubClassProperty(animal); + } + + private static void AssertFetchSubClassProperty(Animal animal) + { + Assert.That(animal, Is.AssignableTo(typeof(Cat))); + var cat = (Cat) animal; + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.False); + } + + #endregion + + #region FetchSubClassAllProperty + + [Test] + public void TestHqlFetchSubClassAllProperties() + { + Animal animal; + using (var s = OpenSession()) + { + animal = s.CreateQuery("from Animal a fetch all properties where a.Id = 1").UniqueResult(); + } + + AssertFetchSubClassAllProperties(animal); + } + + [Test] + public void TestLinqFetchSubClassAllProperties() + { + Animal animal; + using (var s = OpenSession()) + { + animal = s.Query().FetchLazyProperties().First(o => o.Id == 1); + } + + AssertFetchSubClassAllProperties(animal); + } + + private static void AssertFetchSubClassAllProperties(Animal animal) + { + Assert.That(animal, Is.AssignableTo(typeof(Cat))); + var cat = (Cat) animal; + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondFormula"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "SecondImage"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Formula"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(cat, "Image"), Is.True); + } + + #endregion + + #region FetchAllPropertiesWithFetchProperty + + [Test] + public void TestHqlFetchAllPropertiesWithFetchProperty() + { + using (var s = OpenSession()) + { + Assert.Throws( + () => + { + var person = s.CreateQuery("from Person p fetch p.Address fetch all properties where p.Id = 1").UniqueResult(); + }); + Assert.Throws( + () => + { + var person = s.CreateQuery("from Person p fetch all properties fetch p.Address where p.Id = 1").UniqueResult(); + }); + } + } + + [Test] + public void TestLinqFetchAllPropertiesWithFetchProperty() + { + using (var s = OpenSession()) + { + Assert.Throws( + () => + { + var person = s.Query().Fetch(o => o.Address).FetchLazyProperties().FirstOrDefault(o => o.Id == 1); + }); + Assert.Throws( + () => + { + var person = s.Query().FetchLazyProperties().Fetch(o => o.Address).FirstOrDefault(o => o.Id == 1); + }); + } + } + + #endregion + + [Test] + public void TestHqlFetchComponentAlias() + { + Person person; + using (var s = OpenSession()) + { + person = s.CreateQuery("from Person p fetch p.Address where p.Id = 1").UniqueResult(); + } + + AssertFetchComponent(person); + } + + [TestCase(true)] + [TestCase(false)] + public void TestFetchComponentAndFormulaTwoQueryCache(bool readOnly) + { + TestLinqFetchComponentAndFormulaTwoQuery(readOnly); + + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.Get(1); + + tx.Commit(); + } + + AssertFetchComponentAndFormulaTwoQuery(person); + } + + [Test] + public void TestFetchComponentCache() + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.Query().Fetch(o => o.Address).FirstOrDefault(o => o.Id == 1); + AssertFetchComponent(person); + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.Get(1); + AssertFetchComponent(person); + // Will reset the cache item + person.Name = "Test"; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.Get(1); + Assert.That(person, Is.Not.Null); + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + tx.Commit(); + } + } + + [TestCase(true)] + [TestCase(false)] + public void TestFetchAfterPropertyIsInitialized(bool readOnly) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.CreateQuery("from Person fetch Address where Id = 1").SetReadOnly(readOnly).UniqueResult(); + person.Image = new byte[10]; + person = s.CreateQuery("from Person fetch Image where Id = 1").SetReadOnly(readOnly).UniqueResult(); + + tx.Commit(); + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + + Assert.That(person.Image, Has.Length.EqualTo(10)); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.CreateQuery("from Person where Id = 1").SetReadOnly(readOnly).UniqueResult(); + person.Image = new byte[1]; + + tx.Commit(); + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.False); + } + + [TestCase(true)] + [TestCase(false)] + public void TestFetchAfterEntityIsInitialized(bool readOnly) + { + Person person; + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + person = s.CreateQuery("from Person where Id = 1").SetReadOnly(readOnly).UniqueResult(); + var image = person.Image; + person = s.CreateQuery("from Person fetch Image where Id = 1").SetReadOnly(readOnly).UniqueResult(); + + tx.Commit(); + } + + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Formula"), Is.True); + } + + private static Person GeneratePerson(int i, Person bestFriend) + { + return new Person + { + Id = i, + Name = $"Person{i}", + Address = new Address + { + City = $"City{i}", + Country = $"Country{i}", + Continent = GenerateContinent(i) + }, + Image = new byte[i], + BestFriend = bestFriend + }; + } + + private static Continent GenerateContinent(int i) + { + return new Continent + { + Id = i, + Name = $"Continent{i}" + }; + } + + private static Cat GenerateCat(int i, Person owner) + { + return new Cat + { + Id = i, + Address = new Address + { + City = owner.Address.City, + Country = owner.Address.Country + }, + Image = new byte[i], + Name = $"Cat{i}", + SecondImage = new byte[i * 2], + Owner = owner + }; + } + + private static Dog GenerateDog(int i, Person owner) + { + return new Dog + { + Id = i, + Address = new Address + { + City = owner.Address.City, + Country = owner.Address.Country + }, + Image = new byte[i * 3], + Name = $"Dog{i}", + Owner = owner + }; + } + } +} diff --git a/src/NHibernate.Test/FetchLazyProperties/Mappings.hbm.xml b/src/NHibernate.Test/FetchLazyProperties/Mappings.hbm.xml new file mode 100644 index 00000000000..e832dbca5ae --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/Mappings.hbm.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/FetchLazyProperties/Person.cs b/src/NHibernate.Test/FetchLazyProperties/Person.cs new file mode 100644 index 00000000000..4e13bddff23 --- /dev/null +++ b/src/NHibernate.Test/FetchLazyProperties/Person.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.FetchLazyProperties +{ + public class Person + { + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual int Formula { get; set; } + + public virtual Address Address { get; set; } + + public virtual byte[] Image { get; set; } + + public virtual Person BestFriend { get; set; } + + // Not mapped property + public virtual int BirthYear { get; set; } + + public virtual ISet Pets { get; set; } = new HashSet(); + + public virtual ISet Cats { get; set; } = new HashSet(); + + public virtual ISet Dogs { get; set; } = new HashSet(); + } +} diff --git a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs index 66e293c3b08..3f063cf00e6 100644 --- a/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Async/Impl/MultiCriteriaImpl.cs @@ -152,6 +152,7 @@ private async Task GetResultsFromDatabaseAsync(IList results, CancellationToken stopWatch.Start(); } int rowCount = 0; + var cacheBatcher = new CacheBatcher(session); try { @@ -184,7 +185,8 @@ private async Task GetResultsFromDatabaseAsync(IList results, CancellationToken object o = await (loader.GetRowFromResultSetAsync(reader, session, queryParameters, loader.GetLockModes(queryParameters.LockModes), - null, hydratedObjects[i], keys, true, cancellationToken)).ConfigureAwait(false); + null, hydratedObjects[i], keys, true, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); if (createSubselects[i]) { subselectResultKeys[i].Add(keys); @@ -200,13 +202,15 @@ private async Task GetResultsFromDatabaseAsync(IList results, CancellationToken for (int i = 0; i < loaders.Count; i++) { CriteriaLoader loader = loaders[i]; - await (loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, session.DefaultReadOnly, cancellationToken: cancellationToken)).ConfigureAwait(false); + await (loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, session.DefaultReadOnly, cacheBatcher, cancellationToken)).ConfigureAwait(false); if (createSubselects[i]) { loader.CreateSubselects(subselectResultKeys[i], parameters[i], session); } } + + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); } } catch (OperationCanceledException) { throw; } diff --git a/src/NHibernate/Async/Impl/MultiQueryImpl.cs b/src/NHibernate/Async/Impl/MultiQueryImpl.cs index 93cf48d59ad..b65ead8cf26 100644 --- a/src/NHibernate/Async/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Async/Impl/MultiQueryImpl.cs @@ -89,6 +89,7 @@ protected async Task> DoListAsync(CancellationToken cancellationTok var hydratedObjects = new List[Translators.Count]; List[] subselectResultKeys = new List[Translators.Count]; bool[] createSubselects = new bool[Translators.Count]; + var cacheBatcher = new CacheBatcher(session); try { @@ -142,7 +143,8 @@ protected async Task> DoListAsync(CancellationToken cancellationTok rowCount++; object result = await (translator.Loader.GetRowFromResultSetAsync( - reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, cancellationToken)).ConfigureAwait(false); + reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); tempResults.Add(result); if (createSubselects[i]) @@ -172,13 +174,15 @@ protected async Task> DoListAsync(CancellationToken cancellationTok ITranslator translator = translators[i]; QueryParameters parameter = parameters[i]; - await (translator.Loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, false, cancellationToken: cancellationToken)).ConfigureAwait(false); + await (translator.Loader.InitializeEntitiesAndCollectionsAsync(hydratedObjects[i], reader, session, false, cacheBatcher, cancellationToken)).ConfigureAwait(false); if (createSubselects[i]) { translator.Loader.CreateSubselects(subselectResultKeys[i], parameter, session); } } + + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); } } catch (OperationCanceledException) { throw; } diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 537918795db..ca9136face0 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -19,6 +19,7 @@ using System.Runtime.CompilerServices; using NHibernate.AdoNet; using NHibernate.Cache; +using NHibernate.Cache.Entry; using NHibernate.Collection; using NHibernate.Driver; using NHibernate.Engine; @@ -26,6 +27,7 @@ using NHibernate.Exceptions; using NHibernate.Hql.Util; using NHibernate.Impl; +using NHibernate.Intercept; using NHibernate.Param; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; @@ -102,19 +104,22 @@ private async Task DoQueryAndInitializeNonLazyCollectionsAsync(ISessionIm /// A cancellation token that can be used to cancel the work /// The loaded "row". /// + // Since v5.3 + [Obsolete("This method has no more usages and will be removed in a future version")] protected async Task LoadSingleRowAsync(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, bool returnProxies, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); int entitySpan = EntityPersisters.Length; IList hydratedObjects = entitySpan == 0 ? null : new List(entitySpan); + var cacheBatcher = new CacheBatcher(session); object result; try { result = await (GetRowFromResultSetAsync(resultSet, session, queryParameters, GetLockModes(queryParameters.LockModes), null, - hydratedObjects, new EntityKey[entitySpan], returnProxies, cancellationToken)).ConfigureAwait(false); + hydratedObjects, new EntityKey[entitySpan], returnProxies, (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (HibernateException) @@ -128,7 +133,8 @@ protected async Task LoadSingleRowAsync(DbDataReader resultSet, ISession queryParameters.NamedParameters); } - await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, resultSet, session, queryParameters.IsReadOnly(session), cancellationToken: cancellationToken)).ConfigureAwait(false); + await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, resultSet, session, queryParameters.IsReadOnly(session), cacheBatcher, cancellationToken)).ConfigureAwait(false); + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); await (session.PersistenceContext.InitializeNonLazyCollectionsAsync(cancellationToken)).ConfigureAwait(false); return result; } @@ -136,20 +142,21 @@ protected async Task LoadSingleRowAsync(DbDataReader resultSet, ISession internal Task GetRowFromResultSetAsync(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, - bool returnProxies, CancellationToken cancellationToken) + bool returnProxies, Action cacheBatchingHandler, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } return GetRowFromResultSetAsync(resultSet, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, null, cancellationToken); + keys, returnProxies, null, cacheBatchingHandler, cancellationToken); } internal async Task GetRowFromResultSetAsync(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, - bool returnProxies, IResultTransformer forcedResultTransformer, CancellationToken cancellationToken) + bool returnProxies, IResultTransformer forcedResultTransformer, + Action cacheBatchingHandler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ILoadable[] persisters = EntityPersisters; @@ -167,7 +174,7 @@ internal async Task GetRowFromResultSetAsync(DbDataReader resultSet, ISe // this call is side-effecty object[] row = await (GetRowAsync(resultSet, persisters, keys, queryParameters.OptionalObject, optionalObjectKey, lockModeArray, - hydratedObjects, session, !returnProxies, cancellationToken)).ConfigureAwait(false); + hydratedObjects, session, !returnProxies, cacheBatchingHandler, cancellationToken)).ConfigureAwait(false); await (ReadCollectionElementsAsync(row, resultSet, session, cancellationToken)).ConfigureAwait(false); @@ -273,6 +280,7 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet bool createSubselects = IsSubselectLoadingEnabled; List subselectResultKeys = createSubselects ? new List() : null; IList results = new List(); + var cacheBatcher = new CacheBatcher(session); try { @@ -294,7 +302,8 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet object result = await (GetRowFromResultSetAsync(rs, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, forcedResultTransformer, cancellationToken)).ConfigureAwait(false); + keys, returnProxies, forcedResultTransformer, + (persister, data) => cacheBatcher.AddToBatch(persister, data), cancellationToken)).ConfigureAwait(false); results.Add(result); if (createSubselects) @@ -320,7 +329,8 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet session.Batcher.CloseCommand(st, rs); } - await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, rs, session, queryParameters.IsReadOnly(session), cancellationToken: cancellationToken)).ConfigureAwait(false); + await (InitializeEntitiesAndCollectionsAsync(hydratedObjects, rs, session, queryParameters.IsReadOnly(session), cacheBatcher, cancellationToken)).ConfigureAwait(false); + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); if (createSubselects) { @@ -333,7 +343,7 @@ private async Task DoQueryAsync(ISessionImplementor session, QueryParamet internal async Task InitializeEntitiesAndCollectionsAsync( IList hydratedObjects, DbDataReader reader, ISessionImplementor session, bool readOnly, - CacheBatcher cacheBatcher = null, CancellationToken cancellationToken = default(CancellationToken)) + CacheBatcher cacheBatcher, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ICollectionPersister[] collectionPersisters = CollectionPersisters; @@ -593,7 +603,7 @@ private async Task CheckVersionAsync(int i, IEntityPersister persister, object i /// private async Task GetRowAsync(DbDataReader rs, ILoadable[] persisters, EntityKey[] keys, object optionalObject, EntityKey optionalObjectKey, LockMode[] lockModes, IList hydratedObjects, - ISessionImplementor session, bool mustLoadMissingEntity, CancellationToken cancellationToken) + ISessionImplementor session, bool mustLoadMissingEntity, Action cacheBatchingHandler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); int cols = persisters.Length; @@ -638,7 +648,7 @@ private async Task GetRowAsync(DbDataReader rs, ILoadable[] persisters if (alreadyLoaded) { //its already loaded so dont need to hydrate it - await (InstanceAlreadyLoadedAsync(rs, i, persister, key, obj, lockModes[i], session, cancellationToken)).ConfigureAwait(false); + await (InstanceAlreadyLoadedAsync(rs, i, persister, key, obj, lockModes[i], session, cacheBatchingHandler, cancellationToken)).ConfigureAwait(false); } else { @@ -696,8 +706,8 @@ private async Task GetRowAsync(DbDataReader rs, ILoadable[] persisters /// /// The entity instance is already in the session cache /// - private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPersister persister, EntityKey key, object obj, - LockMode lockMode, ISessionImplementor session, CancellationToken cancellationToken) + private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, ILoadable persister, EntityKey key, object obj, + LockMode lockMode, ISessionImplementor session, Action cacheBatchingHandler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (!persister.IsInstance(obj)) @@ -706,9 +716,10 @@ private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPer throw new WrongClassException(errorMsg, key.Identifier, persister.EntityName); } + EntityEntry entry = null; if (LockMode.None != lockMode && UpgradeLocks()) { - EntityEntry entry = session.PersistenceContext.GetEntry(obj); + entry = session.PersistenceContext.GetEntry(obj); bool isVersionCheckNeeded = persister.IsVersioned && entry.LockMode.LessThan(lockMode); // we don't need to worry about existing version being uninitialized @@ -722,6 +733,15 @@ private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPer entry.LockMode = lockMode; } } + + if (!persister.HasLazyProperties) + { + return; + } + + var instanceClass = await (GetInstanceClassAsync(rs, i, persister, key.Identifier, session, cancellationToken)).ConfigureAwait(false); + entry = entry ?? session.PersistenceContext.GetEntry(obj); + await (UpdateLazyPropertiesFromResultSetAsync(rs, i, obj, instanceClass, key, entry, persister, session, cacheBatchingHandler, cancellationToken)).ConfigureAwait(false); } /// @@ -760,6 +780,99 @@ private async Task InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILo return obj; } + private async Task UpdateLazyPropertiesFromResultSetAsync(DbDataReader rs, int i, object obj, string instanceClass, EntityKey key, + EntityEntry entry, ILoadable rootPersister, ISessionImplementor session, + Action cacheBatchingHandler, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!entry.LoadedWithLazyPropertiesUnfetched) + { + return; // All lazy properties were already loaded + } + + var eagerPropertyFetch = IsEagerPropertyFetchEnabled(i); + var fetchLazyProperties = GetFetchLazyProperties(i); + + if (!eagerPropertyFetch && fetchLazyProperties == null) + { + return; // No lazy properties were loaded + } + + // Get the persister for the _subclass_ + var persister = instanceClass == rootPersister.EntityName + ? rootPersister + : (ILoadable) Factory.GetEntityPersister(instanceClass); + + // The property values will not be set when the entry status is Loading so in that case we have to get + // the uninitialized lazy properties from the loaded state + var uninitializedProperties = entry.Status == Status.Loading + ? persister.EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(entry.LoadedState) + : persister.EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(obj); + + var updateLazyProperties = fetchLazyProperties?.Intersect(uninitializedProperties).ToArray(); + if (updateLazyProperties?.Length == 0) + { + return; // No new lazy properites were loaded + } + + var id = key.Identifier; + + if (Log.IsDebugEnabled()) + { + Log.Debug("Updating lazy properites from DataReader: {0}", MessageHelper.InfoString(persister, id)); + } + + var cols = persister == rootPersister + ? EntityAliases[i].SuffixedPropertyAliases + : GetSubclassEntityAliases(i, persister); + + if (!await (persister.InitializeLazyPropertiesAsync(rs, id, obj, rootPersister, cols, updateLazyProperties, eagerPropertyFetch, session, cancellationToken)).ConfigureAwait(false)) + { + return; + } + + if (entry.Status == Status.Loading || !persister.HasCache || + !session.CacheMode.HasFlag(CacheMode.Put) || !persister.IsLazyPropertiesCacheable) + { + return; + } + + if (Log.IsDebugEnabled()) + { + Log.Debug("Updating entity to second-level cache: {0}", MessageHelper.InfoString(persister, id, session.Factory)); + } + + 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 cacheKey = session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); + + if (cacheBatchingHandler != null && persister.IsBatchLoadable) + { + cacheBatchingHandler( + persister, + new CachePutData( + cacheKey, + persister.CacheEntryStructure.Structure(cacheEntry), + version, + persister.IsVersioned ? persister.VersionType.Comparator : null, + false)); + } + else + { + var put = + await (persister.Cache.PutAsync(cacheKey, persister.CacheEntryStructure.Structure(cacheEntry), session.Timestamp, version, + persister.IsVersioned ? persister.VersionType.Comparator : null, + false, cancellationToken)).ConfigureAwait(false); + + if (put && factory.Statistics.IsStatisticsEnabled) + { + factory.StatisticsImplementor.SecondLevelCachePut(persister.Cache.RegionName); + } + } + } + /// /// Hydrate the state of an object from the SQL DbDataReader, into /// an array of "hydrated" values (do not resolve associations yet), @@ -783,6 +896,7 @@ private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, st } bool eagerPropertyFetch = 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 @@ -793,7 +907,7 @@ private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, st ? EntityAliases[i].SuffixedPropertyAliases : GetSubclassEntityAliases(i, persister); - object[] values = await (persister.HydrateAsync(rs, id, obj, rootPersister, cols, eagerPropertyFetch, session, cancellationToken)).ConfigureAwait(false); + object[] values = await (persister.HydrateAsync(rs, id, obj, rootPersister, cols, eagerFetchProperties, eagerPropertyFetch, session, cancellationToken)).ConfigureAwait(false); object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; diff --git a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs index 47e7bf0a952..f262161b342 100644 --- a/src/NHibernate/Async/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Async/Multi/QueryBatchItemBase.cs @@ -75,6 +75,10 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT var lockModeArray = loader.GetLockModes(queryParameters.LockModes); var optionalObjectKey = Loader.Loader.GetOptionalObjectKey(queryParameters, Session); var tmpResults = new List(); + var cacheBatcher = queryInfo.CacheBatcher; + var ownCacheBatcher = cacheBatcher == null; + if (ownCacheBatcher) + cacheBatcher = new CacheBatcher(Session); for (var count = 0; count < maxRows && await (reader.ReadAsync(cancellationToken)).ConfigureAwait(false); count++) { @@ -90,7 +94,8 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT hydratedObjects[i], keys, true, - forcedResultTransformer + forcedResultTransformer, + (persister, data) => cacheBatcher.AddToBatch(persister, data) , cancellationToken )).ConfigureAwait(false); if (loader.IsSubselectLoadingEnabled) { @@ -105,6 +110,9 @@ public async Task ProcessResultsSetAsync(DbDataReader reader, CancellationT if (queryInfo.CanPutToCache) queryInfo.ResultToCache = tmpResults; + if (ownCacheBatcher) + await (cacheBatcher.ExecuteBatchAsync(cancellationToken)).ConfigureAwait(false); + await (reader.NextResultAsync(cancellationToken)).ConfigureAwait(false); } diff --git a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs index ae54caf3fcb..85c93b0a706 100644 --- a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs @@ -60,6 +60,67 @@ public virtual Task BindValuesAsync(DbCommand ps, CancellationToken cancellation } } + public Task InitializeLazyPropertiesAsync( + DbDataReader rs, object id, object entity, ILoadable rootPersister, string[][] suffixedPropertyColumns, + string[] uninitializedLazyProperties, bool allLazyProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + if (!HasLazyProperties) + { + throw new AssertionFailure("No lazy properties"); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalInitializeLazyPropertiesAsync(); + async Task InternalInitializeLazyPropertiesAsync() + { + + var entry = session.PersistenceContext.GetEntry(entity); + if (entry == null) + { + throw new HibernateException($"Entity is not associated with the session: {id}"); + } + + int[] indexes; + int[] lazyIndexes; + if (allLazyProperties) + { + lazyIndexes = indexes = lazyPropertyNumbers; + } + else + { + var metadata = InstrumentationMetadata.LazyPropertiesMetadata; + indexes = new int[uninitializedLazyProperties.Length]; + lazyIndexes = new int[uninitializedLazyProperties.Length]; + for (var i = 0; i < uninitializedLazyProperties.Length; i++) + { + var descriptor = metadata.GetLazyPropertyDescriptor(uninitializedLazyProperties[i]); + indexes[i] = descriptor.PropertyIndex; + lazyIndexes[i] = descriptor.LazyIndex; + } + } + + var values = await (HydrateAsync(rs, id, entity, rootPersister, suffixedPropertyColumns, null, true, indexes, session, cancellationToken)).ConfigureAwait(false); + for (var i = 0; i < lazyIndexes.Length; i++) + { + var value = values[i]; + if (entry.Status == Status.Loading) + { + // Here loaded state contains hydrated values and is always not null even in ReadOnly mode. + // We don't need to set the property value here as it will be set in TwoPhaseLoad.InitializeEntity + entry.LoadedState[indexes[i]] = value; + } + else + { + var lazyIndex = lazyIndexes[i]; + var propValue = await (lazyPropertyTypes[lazyIndex].AssembleAsync(value, session, entity, cancellationToken)).ConfigureAwait(false); + InitializeLazyProperty(entity, entry.LoadedState, lazyIndex, propValue, null); + } + } + } + } + public async Task GetDatabaseSnapshotAsync(object id, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -339,8 +400,38 @@ protected async Task DehydrateAsync(object id, object[] fields, object rowI /// Unmarshall the fields of a persistent instance from a result set, /// without resolving associations or collections /// - public async Task HydrateAsync(DbDataReader rs, object id, object obj, ILoadable rootLoadable, - string[][] suffixedPropertyColumns, bool allProperties, ISessionImplementor session, CancellationToken cancellationToken) + public Task HydrateAsync(DbDataReader rs, object id, object obj, ILoadable rootLoadable, + string[][] suffixedPropertyColumns, ISet fetchedLazyProperties, bool allProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return HydrateAsync(rs, id, obj, rootLoadable, suffixedPropertyColumns, fetchedLazyProperties, allProperties, null, session, cancellationToken); + } + + /// + /// Unmarshall the fields of a persistent instance from a result set, + /// without resolving associations or collections + /// + // Since v5.3 + [Obsolete("Use the overload with fetchedLazyProperties parameter instead")] + public Task HydrateAsync(DbDataReader rs, object id, object obj, ILoadable rootLoadable, + string[][] suffixedPropertyColumns, bool allProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return HydrateAsync(rs, id, obj, rootLoadable, suffixedPropertyColumns, null, allProperties, null, session, cancellationToken); + } + + /// + /// Unmarshall the fields of a persistent instance from a result set, + /// without resolving associations or collections + /// + private async Task HydrateAsync(DbDataReader rs, object id, object obj, ILoadable rootLoadable, string[][] suffixedPropertyColumns, + ISet fetchedLazyProperties, bool allProperties, int[] indexes, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (log.IsDebugEnabled()) @@ -394,37 +485,40 @@ public async Task HydrateAsync(DbDataReader rs, object id, object obj, } } + int i; + int length = indexes?.Length ?? PropertyTypes.Length; string[] propNames = PropertyNames; IType[] types = PropertyTypes; - object[] values = new object[types.Length]; + object[] values = new object[length]; bool[] laziness = PropertyLaziness; string[] propSubclassNames = SubclassPropertySubclassNameClosure; - for (int i = 0; i < types.Length; i++) + for (int j = 0; j < length; j++) { + i = indexes?[j] ?? j; if (!propertySelectable[i]) { - values[i] = BackrefPropertyAccessor.Unknown; + values[j] = BackrefPropertyAccessor.Unknown; } - else if (allProperties || !laziness[i]) + else if (allProperties || !laziness[i] || fetchedLazyProperties?.Contains(propNames[i]) == true) { //decide which ResultSet to get the property value from: bool propertyIsDeferred = hasDeferred && rootPersister.IsSubclassPropertyDeferred(propNames[i], propSubclassNames[i]); if (propertyIsDeferred && sequentialSelectEmpty) { - values[i] = null; + values[j] = null; } else { var propertyResultSet = propertyIsDeferred ? sequentialResultSet : rs; string[] cols = propertyIsDeferred ? propertyColumnAliases[i] : suffixedPropertyColumns[i]; - values[i] = await (types[i].HydrateAsync(propertyResultSet, cols, session, obj, cancellationToken)).ConfigureAwait(false); + values[j] = await (types[i].HydrateAsync(propertyResultSet, cols, session, obj, cancellationToken)).ConfigureAwait(false); } } else { - values[i] = LazyPropertyInitializer.UnfetchedProperty; + values[j] = LazyPropertyInitializer.UnfetchedProperty; } } diff --git a/src/NHibernate/Async/Persister/Entity/ILoadable.cs b/src/NHibernate/Async/Persister/Entity/ILoadable.cs index 2d74dae80a4..ac547d0eabc 100644 --- a/src/NHibernate/Async/Persister/Entity/ILoadable.cs +++ b/src/NHibernate/Async/Persister/Entity/ILoadable.cs @@ -8,6 +8,8 @@ //------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; using NHibernate.Type; using NHibernate.Engine; using System.Data.Common; @@ -23,7 +25,70 @@ public partial interface ILoadable : IEntityPersister /// /// Retrieve property values from one row of a result set /// + // Since v5.3 + [Obsolete("Use the extension method with fetchedLazyProperties parameter instead")] Task HydrateAsync(DbDataReader rs, object id, object obj, ILoadable rootLoadable, string[][] suffixedPropertyColumns, bool allProperties, ISessionImplementor session, CancellationToken cancellationToken); } + + public static partial class LoadableExtensions + { + /// + /// Retrieve property values from one row of a result set + /// + //6.0 TODO: Merge into ILoadable + public static Task HydrateAsync( + this ILoadable loadable, DbDataReader rs, object id, object obj, ILoadable rootLoadable, + string[][] suffixedPropertyColumns, ISet fetchedLazyProperties, bool allProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + if (loadable is AbstractEntityPersister abstractEntityPersister) + { + return abstractEntityPersister.HydrateAsync( + rs, id, obj, rootLoadable, suffixedPropertyColumns, fetchedLazyProperties, allProperties, session, cancellationToken); + } + +#pragma warning disable 618 + // Fallback to the old behavior + return loadable.HydrateAsync(rs, id, obj, rootLoadable, suffixedPropertyColumns, allProperties, session, cancellationToken); +#pragma warning restore 618 + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + /// Set lazy properties from one row of a result set + /// + //6.0 TODO: Change to void and merge into ILoadable + internal static async Task InitializeLazyPropertiesAsync( + this ILoadable loadable, DbDataReader rs, object id, object entity, ILoadable rootPersister, string[][] suffixedPropertyColumns, + string[] uninitializedLazyProperties, bool allLazyProperties, ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (loadable is AbstractEntityPersister abstractEntityPersister) + { + await (abstractEntityPersister.InitializeLazyPropertiesAsync( + rs, + id, + entity, + rootPersister, + suffixedPropertyColumns, + uninitializedLazyProperties, + allLazyProperties, + session, cancellationToken)).ConfigureAwait(false); + + return true; + } + + return false; + } + } } diff --git a/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs b/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs index ee70714e9b1..d27ec518b3b 100644 --- a/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs +++ b/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs @@ -54,5 +54,21 @@ public LazyPropertiesMetadata( public IEnumerable LazyPropertyDescriptors => _lazyPropertyDescriptors?.Values ?? Enumerable.Empty(); + + /// + /// Get the descriptor for the lazy property. + /// + /// The propery name. + /// The lazy property descriptor. + public LazyPropertyDescriptor GetLazyPropertyDescriptor(string propertyName) + { + if (!_lazyPropertyDescriptors.TryGetValue(propertyName, out var descriptor)) + { + throw new InvalidOperationException( + $"Property {propertyName} is not mapped as lazy on entity {EntityName}"); + } + + return descriptor; + } } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Hql.g b/src/NHibernate/Hql/Ast/ANTLR/Hql.g index bb194bf0f91..288d662cec3 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Hql.g +++ b/src/NHibernate/Hql/Ast/ANTLR/Hql.g @@ -301,7 +301,7 @@ alias propertyFetch : FETCH ALL! PROPERTIES! - ; + | (FETCH path)+; groupByClause : GROUP^ diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index 85676bb5757..c34be9f1ebd 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -706,7 +706,7 @@ void CreateFromJoinElement( { throw new InvalidPathException("Invalid join: " + dot.Path); } - fromElement.SetAllPropertyFetch(propertyFetch != null); + SetPropertyFetch(fromElement, propertyFetch, alias); if (with != null) { @@ -725,13 +725,75 @@ void CreateFromJoinElement( } } + private static string GetPropertyPath(DotNode dotNode, IASTNode alias) + { + var lhs = dotNode.GetLhs(); + var rhs = (SqlNode) lhs.NextSibling; + + if (lhs is DotNode nextDotNode) + { + return GetPropertyPath(nextDotNode, alias) + "." + rhs.OriginalText; + } + + var path = rhs.OriginalText; + + if (alias != null && alias.Text == lhs.Text) + { + return path; + } + + return lhs.Path + "." + path; + } + IASTNode CreateFromElement(string path, IASTNode pathNode, IASTNode alias, IASTNode propertyFetch) { FromElement fromElement = _currentFromClause.AddFromElement(path, alias); - fromElement.SetAllPropertyFetch(propertyFetch != null); + SetPropertyFetch(fromElement, propertyFetch, alias); return fromElement; } + static void SetPropertyFetch(FromElement fromElement, IASTNode propertyFetch, IASTNode alias) + { + if (propertyFetch == null) + { + return; + } + + if (propertyFetch.ChildCount == 0) + { + fromElement.SetAllPropertyFetch(true); + } + else + { + var propertyPaths = new string[propertyFetch.ChildCount / 2]; + for (var i = 1; i < propertyFetch.ChildCount; i = i + 2) + { + string propertyPath; + var child = propertyFetch.GetChild(i); + + // o.PropName + if (child is DotNode dotNode) + { + dotNode.JoinType = JoinType.None; + dotNode.PropertyPath = GetPropertyPath(dotNode, alias); + propertyPath = dotNode.PropertyPath; + } + else if (child is IdentNode identNode) + { + propertyPath = identNode.OriginalText; + } + else + { + throw new InvalidOperationException($"Unable to determine property path for AST node: {child.ToStringTree()}"); + } + + propertyPaths[(i - 1) / 2] = propertyPath; + } + + fromElement.FetchLazyProperties = propertyPaths; + } + } + IASTNode CreateFromFilterElement(IASTNode filterEntity, IASTNode alias) { var fromElementFound = true; diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index 404001981a5..d195d02348e 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -241,6 +241,10 @@ aggregateExpr | collectionFunction ; +propertyFetch + : FETCH + | (FETCH path)+; + // Establishes the list of aliases being used by this query. fromClause : ^(f=FROM { PushFromClause($f.tree); HandleClauseStart( FROM ); } fromElementList ) @@ -261,7 +265,7 @@ fromElement! IASTNode fromElement = null; } // A simple class name, alias element. - : ^(RANGE p=path (a=ALIAS)? (pf=FETCH)? ) { fromElement = CreateFromElement($p.p, $p.tree, $a, $pf); } + : ^(RANGE p=path (a=ALIAS)? (pf=propertyFetch)? ) { fromElement = CreateFromElement($p.p, $p.tree, $a, $pf.tree); } -> {fromElement != null}? ^({fromElement}) -> | je=joinElement @@ -275,9 +279,9 @@ joinElement! // A from element with a join. This time, the 'path' should be treated as an AST // and resolved (like any path in a WHERE clause). Make sure all implied joins // generated by the property ref use the join type, if it was specified. - : ^(JOIN (j=joinType { SetImpliedJoinType($j.j); } )? (f=FETCH)? pRef=propertyRef (a=ALIAS)? (pf=FETCH)? (^((with=WITH) .*))? ) + : ^(JOIN (j=joinType { SetImpliedJoinType($j.j); } )? (f=FETCH)? pRef=propertyRef (a=ALIAS)? (pf=propertyFetch)? (^((with=WITH) .*))? ) { - CreateFromJoinElement($pRef.tree,$a,$j.j,$f, $pf, $with); + CreateFromJoinElement($pRef.tree,$a,$j.j,$f, $pf.tree, $with); SetImpliedJoinType(INNER); // Reset the implied join type. } ; diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs index d352ba4532b..6c3a8215670 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs @@ -28,6 +28,7 @@ public class FromElement : HqlSqlWalkerNode, IDisplayableNode, IParameterContain private string _collectionTableAlias; private FromClause _fromClause; private string[] _columns; + private string[] _fetchLazyProperties; private FromElement _origin; private bool _useFromFragment; private bool _useWhereFragment = true; @@ -109,6 +110,15 @@ public bool IsAllPropertyFetch set { _isAllPropertyFetch = value; } } + /// + /// Names of lazy properties to be fetched. + /// + public string[] FetchLazyProperties + { + get { return _fetchLazyProperties; } + set { _fetchLazyProperties = value; } + } + public virtual bool IsImpliedInFromClause { get { return false; } // Since this is an explicit FROM element, it can't be implied in the FROM clause. @@ -327,7 +337,9 @@ public string RenderIdentifierSelect(int size, int k) /// the property select SQL fragment. public string RenderPropertySelect(int size, int k) { - return _elementType.RenderPropertySelect(size, k, IsAllPropertyFetch); + return IsAllPropertyFetch + ? _elementType.RenderPropertySelect(size, k, IsAllPropertyFetch) + : _elementType.RenderPropertySelect(size, k, _fetchLazyProperties); } public override SqlString RenderText(Engine.ISessionFactoryImplementor sessionFactory) diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs index 8be86e92808..d8328bfdbf8 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs @@ -198,6 +198,16 @@ public virtual string RenderScalarIdentifierSelect(int i) /// /// the property select SQL fragment. public string RenderPropertySelect(int size, int k, bool allProperties) + { + return RenderPropertySelect(size, k, null, allProperties); + } + + public string RenderPropertySelect(int size, int k, string[] fetchLazyProperties) + { + return RenderPropertySelect(size, k, fetchLazyProperties, false); + } + + private string RenderPropertySelect(int size, int k, string[] fetchLazyProperties, bool allProperties) { CheckInitialized(); @@ -205,7 +215,11 @@ public string RenderPropertySelect(int size, int k, bool allProperties) if (queryable == null) return ""; - string fragment = queryable.PropertySelectFragment(TableAlias, GetSuffix(size, k), allProperties); + // Use the old method when fetchProperties is null to prevent any breaking changes + // 6.0 TODO: simplify condition by removing the fetchProperties part + string fragment = fetchLazyProperties == null || allProperties + ? queryable.PropertySelectFragment(TableAlias, GetSuffix(size, k), allProperties) + : queryable.PropertySelectFragment(TableAlias, GetSuffix(size, k), fetchLazyProperties); return TrimLeadingCommaAndSpaces(fragment); } diff --git a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs index 4e786aa541e..49cad7607e1 100755 --- a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs +++ b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs @@ -493,6 +493,11 @@ public HqlLeftFetchJoin LeftFetchJoin(HqlExpression expression, HqlAlias @alias) return new HqlLeftFetchJoin(_factory, expression, @alias); } + public HqlFetch Fetch() + { + return new HqlFetch(_factory); + } + public HqlClass Class() { return new HqlClass(_factory); diff --git a/src/NHibernate/Impl/MultiCriteriaImpl.cs b/src/NHibernate/Impl/MultiCriteriaImpl.cs index 98f87bd9bb5..4920acc5306 100644 --- a/src/NHibernate/Impl/MultiCriteriaImpl.cs +++ b/src/NHibernate/Impl/MultiCriteriaImpl.cs @@ -225,6 +225,7 @@ private void GetResultsFromDatabase(IList results) stopWatch.Start(); } int rowCount = 0; + var cacheBatcher = new CacheBatcher(session); try { @@ -257,7 +258,8 @@ private void GetResultsFromDatabase(IList results) object o = loader.GetRowFromResultSet(reader, session, queryParameters, loader.GetLockModes(queryParameters.LockModes), - null, hydratedObjects[i], keys, true); + null, hydratedObjects[i], keys, true, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); if (createSubselects[i]) { subselectResultKeys[i].Add(keys); @@ -273,13 +275,15 @@ private void GetResultsFromDatabase(IList results) for (int i = 0; i < loaders.Count; i++) { CriteriaLoader loader = loaders[i]; - loader.InitializeEntitiesAndCollections(hydratedObjects[i], reader, session, session.DefaultReadOnly); + loader.InitializeEntitiesAndCollections(hydratedObjects[i], reader, session, session.DefaultReadOnly, cacheBatcher); if (createSubselects[i]) { loader.CreateSubselects(subselectResultKeys[i], parameters[i], session); } } + + cacheBatcher.ExecuteBatch(); } } catch (Exception sqle) diff --git a/src/NHibernate/Impl/MultiQueryImpl.cs b/src/NHibernate/Impl/MultiQueryImpl.cs index 278c2822ff0..310e45125b4 100644 --- a/src/NHibernate/Impl/MultiQueryImpl.cs +++ b/src/NHibernate/Impl/MultiQueryImpl.cs @@ -533,6 +533,7 @@ protected List DoList() var hydratedObjects = new List[Translators.Count]; List[] subselectResultKeys = new List[Translators.Count]; bool[] createSubselects = new bool[Translators.Count]; + var cacheBatcher = new CacheBatcher(session); try { @@ -586,7 +587,8 @@ protected List DoList() rowCount++; object result = translator.Loader.GetRowFromResultSet( - reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true); + reader, session, parameter, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); tempResults.Add(result); if (createSubselects[i]) @@ -616,13 +618,15 @@ protected List DoList() ITranslator translator = translators[i]; QueryParameters parameter = parameters[i]; - translator.Loader.InitializeEntitiesAndCollections(hydratedObjects[i], reader, session, false); + translator.Loader.InitializeEntitiesAndCollections(hydratedObjects[i], reader, session, false, cacheBatcher); if (createSubselects[i]) { translator.Loader.CreateSubselects(subselectResultKeys[i], parameter, session); } } + + cacheBatcher.ExecuteBatch(); } } catch (Exception sqle) diff --git a/src/NHibernate/Linq/EagerFetchingExtensionMethods.cs b/src/NHibernate/Linq/EagerFetchingExtensionMethods.cs index b879dff8000..797cf6eca94 100644 --- a/src/NHibernate/Linq/EagerFetchingExtensionMethods.cs +++ b/src/NHibernate/Linq/EagerFetchingExtensionMethods.cs @@ -19,6 +19,21 @@ public static INhFetchRequest Fetch(methodInfo, query, relatedObjectSelector); } + /// + /// Fetch all lazy properties. Note that this method cannot be mixed with method that + /// is used for fetching an individual lazy property. + /// + /// The type on where all lazy properties will be fetched. + /// The NHibernate query. + public static INhFetchRequest FetchLazyProperties( + this IQueryable query) + { + if (query == null) throw new ArgumentNullException(nameof(query)); + + var methodInfo = ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(typeof(TOriginating)); + return CreateFluentFetchRequest(methodInfo, query, null); + } + public static INhFetchRequest FetchMany( this IQueryable query, Expression>> relatedObjectSelector) { @@ -55,7 +70,10 @@ private static INhFetchRequest CreateFluentFetchRequest< LambdaExpression relatedObjectSelector) { var queryProvider = query.Provider; // ArgumentUtility.CheckNotNullAndType("query.Provider", query.Provider); - var callExpression = Expression.Call(currentFetchMethod, query.Expression, relatedObjectSelector); + var callExpression = relatedObjectSelector != null + ? Expression.Call(currentFetchMethod, query.Expression, relatedObjectSelector) + : Expression.Call(currentFetchMethod, query.Expression); + return new NhFetchRequest(queryProvider, callExpression); } } @@ -71,4 +89,4 @@ public NhFetchRequest(IQueryProvider provider, Expression expression) { } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Linq/FetchLazyPropertiesExpressionNode.cs b/src/NHibernate/Linq/FetchLazyPropertiesExpressionNode.cs new file mode 100644 index 00000000000..561c6a02de8 --- /dev/null +++ b/src/NHibernate/Linq/FetchLazyPropertiesExpressionNode.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; +using Remotion.Linq.Clauses; +using Remotion.Linq.Parsing.Structure.IntermediateModel; + +namespace NHibernate.Linq +{ + internal sealed class FetchLazyPropertiesExpressionNode : ResultOperatorExpressionNodeBase + { + public FetchLazyPropertiesExpressionNode(MethodCallExpressionParseInfo parseInfo) + : base(parseInfo, null, null) + { + } + + public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, ClauseGenerationContext clauseGenerationContext) + { + return Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext); + } + + protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext) + { + return new FetchLazyPropertiesResultOperator(); + } + } +} diff --git a/src/NHibernate/Linq/FetchLazyPropertiesResultOperator.cs b/src/NHibernate/Linq/FetchLazyPropertiesResultOperator.cs new file mode 100644 index 00000000000..a748a35623d --- /dev/null +++ b/src/NHibernate/Linq/FetchLazyPropertiesResultOperator.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.StreamedData; + +namespace NHibernate.Linq +{ + internal class FetchLazyPropertiesResultOperator : ResultOperatorBase + { + public override IStreamedData ExecuteInMemory(IStreamedData input) + { + throw new NotImplementedException(); + } + + public override IStreamedDataInfo GetOutputDataInfo(IStreamedDataInfo inputInfo) + { + return inputInfo; + } + + public override ResultOperatorBase Clone(CloneContext cloneContext) + { + throw new NotImplementedException(); + } + + public override void TransformExpressions(Func transformation) + { + } + } +} diff --git a/src/NHibernate/Linq/IntermediateHqlTree.cs b/src/NHibernate/Linq/IntermediateHqlTree.cs index fe433964b25..1cc0e18407a 100644 --- a/src/NHibernate/Linq/IntermediateHqlTree.cs +++ b/src/NHibernate/Linq/IntermediateHqlTree.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Hql.Ast; +using NHibernate.Hql.Ast.ANTLR; using NHibernate.Transform; using NHibernate.Type; @@ -131,6 +132,15 @@ public void AddSelectClause(HqlTreeNode select) _root.NodesPreOrder.OfType().First().AddChild(select); } + public void AddFromLastChildClause(params HqlTreeNode[] nodes) + { + var fromChild = _root.NodesPreOrder.OfType().First().Children.Last(); + foreach (var node in nodes) + { + fromChild.AddChild(node); + } + } + public void AddInsertClause(HqlIdent target, HqlRange columnSpec) { var into = _insertRoot.NodesPreOrder.OfType().Single(); @@ -281,4 +291,4 @@ public void SetRoot(HqlTreeNode newRoot) _root = newRoot; } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs index af920331057..bafd050280b 100644 --- a/src/NHibernate/Linq/NhRelinqQueryParser.cs +++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs @@ -73,6 +73,9 @@ public NHibernateNodeTypeProvider() methodInfoRegistry.Register( new[] { ReflectHelper.GetMethodDefinition(() => EagerFetchingExtensionMethods.Fetch(null, null)) }, typeof(FetchOneExpressionNode)); + methodInfoRegistry.Register( + new[] { ReflectHelper.GetMethodDefinition(() => EagerFetchingExtensionMethods.FetchLazyProperties(null)) }, + typeof(FetchLazyPropertiesExpressionNode)); methodInfoRegistry.Register( new[] { ReflectHelper.GetMethodDefinition(() => EagerFetchingExtensionMethods.FetchMany(null, null)) }, typeof(FetchManyExpressionNode)); diff --git a/src/NHibernate/Linq/ReWriters/QueryReferenceExpressionFlattener.cs b/src/NHibernate/Linq/ReWriters/QueryReferenceExpressionFlattener.cs index be66bfa727f..c9ce9fa4578 100644 --- a/src/NHibernate/Linq/ReWriters/QueryReferenceExpressionFlattener.cs +++ b/src/NHibernate/Linq/ReWriters/QueryReferenceExpressionFlattener.cs @@ -16,6 +16,7 @@ public class QueryReferenceExpressionFlattener : RelinqExpressionVisitor internal static readonly System.Type[] FlattenableResultOperators = { typeof(LockResultOperator), + typeof(FetchLazyPropertiesResultOperator), typeof(FetchOneRequest), typeof(FetchManyRequest) }; diff --git a/src/NHibernate/Linq/ReWriters/ResultOperatorRewriter.cs b/src/NHibernate/Linq/ReWriters/ResultOperatorRewriter.cs index b05dddca40c..f81f0afff70 100644 --- a/src/NHibernate/Linq/ReWriters/ResultOperatorRewriter.cs +++ b/src/NHibernate/Linq/ReWriters/ResultOperatorRewriter.cs @@ -69,6 +69,7 @@ private class ResultOperatorExpressionRewriter : RelinqExpressionVisitor typeof(CastResultOperator), typeof(AsQueryableResultOperator), typeof(LockResultOperator), + typeof(FetchLazyPropertiesResultOperator) }; private readonly List resultOperators = new List(); diff --git a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index 3567ad7c970..1a87c9705a1 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -142,6 +142,7 @@ static QueryModelVisitor() ResultOperatorMap.Add(); ResultOperatorMap.Add(); ResultOperatorMap.Add(); + ResultOperatorMap.Add(); } private QueryModelVisitor(VisitorParameters visitorParameters, bool root, QueryModel queryModel, diff --git a/src/NHibernate/Linq/Visitors/QuerySourceLocator.cs b/src/NHibernate/Linq/Visitors/QuerySourceLocator.cs index 54ad4e4c24a..337aa6a05a1 100644 --- a/src/NHibernate/Linq/Visitors/QuerySourceLocator.cs +++ b/src/NHibernate/Linq/Visitors/QuerySourceLocator.cs @@ -53,7 +53,7 @@ public override void VisitNhJoinClause(NhJoinClause joinClause, QueryModel query public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) { - if (_type.IsAssignableFrom(fromClause.ItemType)) + if (_type.IsAssignableFrom(fromClause.ItemType) || fromClause.ItemType.IsAssignableFrom(_type)) { _querySource = fromClause; } diff --git a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs index 0fd3faa74f3..102d1cb6dc2 100644 --- a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs +++ b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs @@ -1,32 +1,80 @@ -using Remotion.Linq.EagerFetching; +using System; +using System.Linq; +using NHibernate.Hql.Ast; +using NHibernate.Type; +using Remotion.Linq.EagerFetching; namespace NHibernate.Linq.Visitors.ResultOperatorProcessors { - public class ProcessFetch - { - public void Process(FetchRequestBase resultOperator, QueryModelVisitor queryModelVisitor, IntermediateHqlTree tree) - { - var querySource = QuerySourceLocator.FindQuerySource(queryModelVisitor.Model, resultOperator.RelationMember.DeclaringType); - - Process(resultOperator, queryModelVisitor, tree, querySource.ItemName); - } - - public void Process(FetchRequestBase resultOperator, QueryModelVisitor queryModelVisitor, IntermediateHqlTree tree, string sourceAlias) - { - var join = tree.TreeBuilder.Dot( - tree.TreeBuilder.Ident(sourceAlias), - tree.TreeBuilder.Ident(resultOperator.RelationMember.Name)); - - string alias = queryModelVisitor.Model.GetNewName("_"); - - tree.AddFromClause(tree.TreeBuilder.LeftFetchJoin(join, tree.TreeBuilder.Alias(alias))); - tree.AddDistinctRootOperator(); - - foreach (var innerFetch in resultOperator.InnerFetchRequests) - { - Process(innerFetch, queryModelVisitor, tree, alias); - } - } - - } -} \ No newline at end of file + public class ProcessFetch + { + public void Process(FetchRequestBase resultOperator, QueryModelVisitor queryModelVisitor, IntermediateHqlTree tree) + { + var querySource = QuerySourceLocator.FindQuerySource( + queryModelVisitor.Model, + resultOperator.RelationMember.DeclaringType); + + Process(resultOperator, queryModelVisitor, tree, querySource.ItemName); + } + + public void Process(FetchRequestBase resultOperator, QueryModelVisitor queryModelVisitor, IntermediateHqlTree tree, string sourceAlias) + { + var memberPath = tree.TreeBuilder.Dot( + tree.TreeBuilder.Ident(sourceAlias), + tree.TreeBuilder.Ident(resultOperator.RelationMember.Name)); + + Process(resultOperator, queryModelVisitor, tree, memberPath, null); + } + + private void Process(FetchRequestBase resultOperator, QueryModelVisitor queryModelVisitor, IntermediateHqlTree tree, HqlDot memberPath, IType propType) + { + if (resultOperator is FetchOneRequest) + { + if (propType == null) + { + var metadata = queryModelVisitor.VisitorParameters.SessionFactory + .GetClassMetadata(resultOperator.RelationMember.ReflectedType); + propType = metadata?.GetPropertyType(resultOperator.RelationMember.Name); + } + + if (propType != null && !propType.IsAssociationType) + { + tree.AddFromLastChildClause(tree.TreeBuilder.Fetch()); + tree.AddFromLastChildClause(memberPath); + + ComponentType componentType = null; + foreach (var innerFetch in resultOperator.InnerFetchRequests) + { + if (componentType == null) + { + componentType = propType as ComponentType; + if (componentType == null) + { + throw new InvalidOperationException( + $"Property {innerFetch.RelationMember.Name} cannot be fetched from a non component type property {resultOperator.RelationMember.Name}."); + } + } + + var subTypeIndex = componentType.GetPropertyIndex(innerFetch.RelationMember.Name); + memberPath = tree.TreeBuilder.Dot( + memberPath, + tree.TreeBuilder.Ident(innerFetch.RelationMember.Name)); + + Process(innerFetch, queryModelVisitor, tree, memberPath, componentType.Subtypes[subTypeIndex]); + } + + return; + } + } + + var alias = queryModelVisitor.Model.GetNewName("_"); + tree.AddFromClause(tree.TreeBuilder.LeftFetchJoin(memberPath, tree.TreeBuilder.Alias(alias))); + tree.AddDistinctRootOperator(); + + foreach (var innerFetch in resultOperator.InnerFetchRequests) + { + Process(innerFetch, queryModelVisitor, tree, alias); + } + } + } +} diff --git a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetchLazyProperties.cs b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetchLazyProperties.cs new file mode 100644 index 00000000000..ee0a7969d4c --- /dev/null +++ b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetchLazyProperties.cs @@ -0,0 +1,11 @@ + +namespace NHibernate.Linq.Visitors.ResultOperatorProcessors +{ + internal class ProcessFetchLazyProperties : IResultOperatorProcessor + { + public void Process(FetchLazyPropertiesResultOperator resultOperator, QueryModelVisitor queryModelVisitor, IntermediateHqlTree tree) + { + tree.AddFromLastChildClause(tree.TreeBuilder.Fetch()); + } + } +} diff --git a/src/NHibernate/Linq/Visitors/SubQueryFromClauseFlattener.cs b/src/NHibernate/Linq/Visitors/SubQueryFromClauseFlattener.cs index d47b7cb47d1..82b33c27b1d 100644 --- a/src/NHibernate/Linq/Visitors/SubQueryFromClauseFlattener.cs +++ b/src/NHibernate/Linq/Visitors/SubQueryFromClauseFlattener.cs @@ -13,6 +13,7 @@ public class SubQueryFromClauseFlattener : NhQueryModelVisitorBase private static readonly System.Type[] FlattenableResultOperators = { typeof(LockResultOperator), + typeof(FetchLazyPropertiesResultOperator), typeof(FetchOneRequest), typeof(FetchManyRequest) }; @@ -98,4 +99,4 @@ private static void InsertBodyClauses(IEnumerable bodyClauses, Quer } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Loader/Hql/QueryLoader.cs b/src/NHibernate/Loader/Hql/QueryLoader.cs index f85b2ec4512..236ea7dea45 100644 --- a/src/NHibernate/Loader/Hql/QueryLoader.cs +++ b/src/NHibernate/Loader/Hql/QueryLoader.cs @@ -33,6 +33,7 @@ public partial class QueryLoader : BasicLoader private string[] _collectionSuffixes; private IQueryable[] _entityPersisters; private bool[] _entityEagerPropertyFetches; + private HashSet[] _entityFetchLazyProperties; private string[] _entityAliases; private string[] _sqlAliases; private string[] _sqlAliasSuffixes; @@ -115,6 +116,11 @@ protected override bool[] EntityEagerPropertyFetches get { return _entityEagerPropertyFetches; } } + protected override HashSet[] EntityFetchLazyProperties + { + get { return _entityFetchLazyProperties; } + } + protected override EntityType[] OwnerAssociationTypes { get { return _ownerAssociationTypes; } @@ -228,6 +234,7 @@ private void Initialize(SelectClause selectClause) int size = fromElementList.Count; _entityPersisters = new IQueryable[size]; _entityEagerPropertyFetches = new bool[size]; + _entityFetchLazyProperties = new HashSet[size]; _entityAliases = new String[size]; _sqlAliases = new String[size]; _sqlAliasSuffixes = new String[size]; @@ -246,6 +253,9 @@ private void Initialize(SelectClause selectClause) } _entityEagerPropertyFetches[i] = element.IsAllPropertyFetch; + _entityFetchLazyProperties[i] = element.FetchLazyProperties != null + ? new HashSet(element.FetchLazyProperties) + : null; _sqlAliases[i] = element.TableAlias; _entityAliases[i] = element.ClassAlias; _sqlAliasByEntityAlias.Add(_entityAliases[i], _sqlAliases[i]); diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 0615518a3aa..8849605899c 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using NHibernate.AdoNet; using NHibernate.Cache; +using NHibernate.Cache.Entry; using NHibernate.Collection; using NHibernate.Driver; using NHibernate.Engine; @@ -16,6 +17,7 @@ using NHibernate.Exceptions; using NHibernate.Hql.Util; using NHibernate.Impl; +using NHibernate.Intercept; using NHibernate.Param; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; @@ -90,6 +92,14 @@ protected virtual bool[] EntityEagerPropertyFetches get { return null; } } + /// + /// An array of hash sets indicating which lazy properties will be fetched for an entity persister. + /// + protected virtual HashSet[] EntityFetchLazyProperties + { + get { return null; } + } + /// /// An array of indexes of the entity that owns a one-to-one association /// to the entity at the given index (-1 if there is no "owner") @@ -290,18 +300,21 @@ private IList DoQueryAndInitializeNonLazyCollections(ISessionImplementor session /// Should proxies be generated /// The loaded "row". /// + // Since v5.3 + [Obsolete("This method has no more usages and will be removed in a future version")] protected object LoadSingleRow(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, bool returnProxies) { int entitySpan = EntityPersisters.Length; IList hydratedObjects = entitySpan == 0 ? null : new List(entitySpan); + var cacheBatcher = new CacheBatcher(session); object result; try { result = GetRowFromResultSet(resultSet, session, queryParameters, GetLockModes(queryParameters.LockModes), null, - hydratedObjects, new EntityKey[entitySpan], returnProxies); + hydratedObjects, new EntityKey[entitySpan], returnProxies, (persister, data) => cacheBatcher.AddToBatch(persister, data)); } catch (HibernateException) { @@ -314,7 +327,8 @@ protected object LoadSingleRow(DbDataReader resultSet, ISessionImplementor sessi queryParameters.NamedParameters); } - InitializeEntitiesAndCollections(hydratedObjects, resultSet, session, queryParameters.IsReadOnly(session)); + InitializeEntitiesAndCollections(hydratedObjects, resultSet, session, queryParameters.IsReadOnly(session), cacheBatcher); + cacheBatcher.ExecuteBatch(); session.PersistenceContext.InitializeNonLazyCollections(); return result; } @@ -340,16 +354,17 @@ internal static EntityKey GetOptionalObjectKey(QueryParameters queryParameters, internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, - bool returnProxies) + bool returnProxies, Action cacheBatchingHandler) { return GetRowFromResultSet(resultSet, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, null); + keys, returnProxies, null, cacheBatchingHandler); } internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, - bool returnProxies, IResultTransformer forcedResultTransformer) + bool returnProxies, IResultTransformer forcedResultTransformer, + Action cacheBatchingHandler) { ILoadable[] persisters = EntityPersisters; int entitySpan = persisters.Length; @@ -366,7 +381,7 @@ internal object GetRowFromResultSet(DbDataReader resultSet, ISessionImplementor // this call is side-effecty object[] row = GetRow(resultSet, persisters, keys, queryParameters.OptionalObject, optionalObjectKey, lockModeArray, - hydratedObjects, session, !returnProxies); + hydratedObjects, session, !returnProxies, cacheBatchingHandler); ReadCollectionElements(row, resultSet, session); @@ -470,6 +485,7 @@ private IList DoQuery(ISessionImplementor session, QueryParameters queryParamete bool createSubselects = IsSubselectLoadingEnabled; List subselectResultKeys = createSubselects ? new List() : null; IList results = new List(); + var cacheBatcher = new CacheBatcher(session); try { @@ -491,7 +507,8 @@ private IList DoQuery(ISessionImplementor session, QueryParameters queryParamete object result = GetRowFromResultSet(rs, session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects, - keys, returnProxies, forcedResultTransformer); + keys, returnProxies, forcedResultTransformer, + (persister, data) => cacheBatcher.AddToBatch(persister, data)); results.Add(result); if (createSubselects) @@ -516,7 +533,8 @@ private IList DoQuery(ISessionImplementor session, QueryParameters queryParamete session.Batcher.CloseCommand(st, rs); } - InitializeEntitiesAndCollections(hydratedObjects, rs, session, queryParameters.IsReadOnly(session)); + InitializeEntitiesAndCollections(hydratedObjects, rs, session, queryParameters.IsReadOnly(session), cacheBatcher); + cacheBatcher.ExecuteBatch(); if (createSubselects) { @@ -600,7 +618,7 @@ private IEnumerable CreateSubselects(IList keys, Qu internal void InitializeEntitiesAndCollections( IList hydratedObjects, DbDataReader reader, ISessionImplementor session, bool readOnly, - CacheBatcher cacheBatcher = null) + CacheBatcher cacheBatcher) { ICollectionPersister[] collectionPersisters = CollectionPersisters; if (collectionPersisters != null) @@ -933,7 +951,7 @@ private void CheckVersion(int i, IEntityPersister persister, object id, object e /// private object[] GetRow(DbDataReader rs, ILoadable[] persisters, EntityKey[] keys, object optionalObject, EntityKey optionalObjectKey, LockMode[] lockModes, IList hydratedObjects, - ISessionImplementor session, bool mustLoadMissingEntity) + ISessionImplementor session, bool mustLoadMissingEntity, Action cacheBatchingHandler) { int cols = persisters.Length; @@ -977,7 +995,7 @@ private object[] GetRow(DbDataReader rs, ILoadable[] persisters, EntityKey[] key if (alreadyLoaded) { //its already loaded so dont need to hydrate it - InstanceAlreadyLoaded(rs, i, persister, key, obj, lockModes[i], session); + InstanceAlreadyLoaded(rs, i, persister, key, obj, lockModes[i], session, cacheBatchingHandler); } else { @@ -1007,8 +1025,8 @@ private object[] GetRow(DbDataReader rs, ILoadable[] persisters, EntityKey[] key /// /// The entity instance is already in the session cache /// - private void InstanceAlreadyLoaded(DbDataReader rs, int i, IEntityPersister persister, EntityKey key, object obj, - LockMode lockMode, ISessionImplementor session) + private void InstanceAlreadyLoaded(DbDataReader rs, int i, ILoadable persister, EntityKey key, object obj, + LockMode lockMode, ISessionImplementor session, Action cacheBatchingHandler) { if (!persister.IsInstance(obj)) { @@ -1016,9 +1034,10 @@ private void InstanceAlreadyLoaded(DbDataReader rs, int i, IEntityPersister pers throw new WrongClassException(errorMsg, key.Identifier, persister.EntityName); } + EntityEntry entry = null; if (LockMode.None != lockMode && UpgradeLocks()) { - EntityEntry entry = session.PersistenceContext.GetEntry(obj); + entry = session.PersistenceContext.GetEntry(obj); bool isVersionCheckNeeded = persister.IsVersioned && entry.LockMode.LessThan(lockMode); // we don't need to worry about existing version being uninitialized @@ -1032,6 +1051,15 @@ private void InstanceAlreadyLoaded(DbDataReader rs, int i, IEntityPersister pers entry.LockMode = lockMode; } } + + if (!persister.HasLazyProperties) + { + return; + } + + var instanceClass = GetInstanceClass(rs, i, persister, key.Identifier, session); + entry = entry ?? session.PersistenceContext.GetEntry(obj); + UpdateLazyPropertiesFromResultSet(rs, i, obj, instanceClass, key, entry, persister, session, cacheBatchingHandler); } private void CacheByUniqueKey(int i, IEntityPersister persister, object obj, ISessionImplementor session, bool alreadyLoaded) @@ -1112,6 +1140,104 @@ private bool IsEagerPropertyFetchEnabled(int i) return array != null && array[i]; } + private HashSet GetFetchLazyProperties(int i) + { + var array = EntityFetchLazyProperties; + return array?[i]; + } + + private void UpdateLazyPropertiesFromResultSet(DbDataReader rs, int i, object obj, string instanceClass, EntityKey key, + EntityEntry entry, ILoadable rootPersister, ISessionImplementor session, + Action cacheBatchingHandler) + { + if (!entry.LoadedWithLazyPropertiesUnfetched) + { + return; // All lazy properties were already loaded + } + + var eagerPropertyFetch = IsEagerPropertyFetchEnabled(i); + var fetchLazyProperties = GetFetchLazyProperties(i); + + if (!eagerPropertyFetch && fetchLazyProperties == null) + { + return; // No lazy properties were loaded + } + + // Get the persister for the _subclass_ + var persister = instanceClass == rootPersister.EntityName + ? rootPersister + : (ILoadable) Factory.GetEntityPersister(instanceClass); + + // The property values will not be set when the entry status is Loading so in that case we have to get + // the uninitialized lazy properties from the loaded state + var uninitializedProperties = entry.Status == Status.Loading + ? persister.EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(entry.LoadedState) + : persister.EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(obj); + + var updateLazyProperties = fetchLazyProperties?.Intersect(uninitializedProperties).ToArray(); + if (updateLazyProperties?.Length == 0) + { + return; // No new lazy properites were loaded + } + + var id = key.Identifier; + + if (Log.IsDebugEnabled()) + { + Log.Debug("Updating lazy properites from DataReader: {0}", MessageHelper.InfoString(persister, id)); + } + + var cols = persister == rootPersister + ? EntityAliases[i].SuffixedPropertyAliases + : GetSubclassEntityAliases(i, persister); + + if (!persister.InitializeLazyProperties(rs, id, obj, rootPersister, cols, updateLazyProperties, eagerPropertyFetch, session)) + { + return; + } + + if (entry.Status == Status.Loading || !persister.HasCache || + !session.CacheMode.HasFlag(CacheMode.Put) || !persister.IsLazyPropertiesCacheable) + { + return; + } + + if (Log.IsDebugEnabled()) + { + Log.Debug("Updating entity to second-level cache: {0}", MessageHelper.InfoString(persister, id, session.Factory)); + } + + 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 cacheKey = session.GenerateCacheKey(id, persister.IdentifierType, persister.RootEntityName); + + if (cacheBatchingHandler != null && persister.IsBatchLoadable) + { + cacheBatchingHandler( + persister, + new CachePutData( + cacheKey, + persister.CacheEntryStructure.Structure(cacheEntry), + version, + persister.IsVersioned ? persister.VersionType.Comparator : null, + false)); + } + else + { + var put = + persister.Cache.Put(cacheKey, persister.CacheEntryStructure.Structure(cacheEntry), session.Timestamp, version, + persister.IsVersioned ? persister.VersionType.Comparator : null, + false); + + if (put && factory.Statistics.IsStatisticsEnabled) + { + factory.StatisticsImplementor.SecondLevelCachePut(persister.Cache.RegionName); + } + } + } + /// /// Hydrate the state of an object from the SQL DbDataReader, into /// an array of "hydrated" values (do not resolve associations yet), @@ -1134,6 +1260,7 @@ private void LoadFromResultSet(DbDataReader rs, int i, object obj, string instan } bool eagerPropertyFetch = 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 @@ -1144,7 +1271,7 @@ private void LoadFromResultSet(DbDataReader rs, int i, object obj, string instan ? EntityAliases[i].SuffixedPropertyAliases : GetSubclassEntityAliases(i, persister); - object[] values = persister.Hydrate(rs, id, obj, rootPersister, cols, eagerPropertyFetch, session); + object[] values = persister.Hydrate(rs, id, obj, rootPersister, cols, eagerFetchProperties, eagerPropertyFetch, session); object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; diff --git a/src/NHibernate/Multi/QueryBatchItemBase.cs b/src/NHibernate/Multi/QueryBatchItemBase.cs index 273070d3f15..73d9cafd18e 100644 --- a/src/NHibernate/Multi/QueryBatchItemBase.cs +++ b/src/NHibernate/Multi/QueryBatchItemBase.cs @@ -214,6 +214,10 @@ public int ProcessResultsSet(DbDataReader reader) var lockModeArray = loader.GetLockModes(queryParameters.LockModes); var optionalObjectKey = Loader.Loader.GetOptionalObjectKey(queryParameters, Session); var tmpResults = new List(); + var cacheBatcher = queryInfo.CacheBatcher; + var ownCacheBatcher = cacheBatcher == null; + if (ownCacheBatcher) + cacheBatcher = new CacheBatcher(Session); for (var count = 0; count < maxRows && reader.Read(); count++) { @@ -229,7 +233,8 @@ public int ProcessResultsSet(DbDataReader reader) hydratedObjects[i], keys, true, - forcedResultTransformer + forcedResultTransformer, + (persister, data) => cacheBatcher.AddToBatch(persister, data) ); if (loader.IsSubselectLoadingEnabled) { @@ -244,6 +249,9 @@ public int ProcessResultsSet(DbDataReader reader) if (queryInfo.CanPutToCache) queryInfo.ResultToCache = tmpResults; + if (ownCacheBatcher) + cacheBatcher.ExecuteBatch(); + reader.NextResult(); } diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index 85d98154acd..984494ab9c4 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -1304,8 +1304,62 @@ public virtual object InitializeLazyProperty(string fieldName, object entity, IS return InitializeLazyPropertiesFromDatastore(fieldName, entity, session, id, entry, uninitializedLazyProperties); } - private object InitializeLazyPropertiesFromDatastore(string fieldName, object entity, ISessionImplementor session, object id, EntityEntry entry, - ISet uninitializedLazyProperties) + public void InitializeLazyProperties( + DbDataReader rs, object id, object entity, ILoadable rootPersister, string[][] suffixedPropertyColumns, + string[] uninitializedLazyProperties, bool allLazyProperties, ISessionImplementor session) + { + if (!HasLazyProperties) + { + throw new AssertionFailure("No lazy properties"); + } + + var entry = session.PersistenceContext.GetEntry(entity); + if (entry == null) + { + throw new HibernateException($"Entity is not associated with the session: {id}"); + } + + int[] indexes; + int[] lazyIndexes; + if (allLazyProperties) + { + lazyIndexes = indexes = lazyPropertyNumbers; + } + else + { + var metadata = InstrumentationMetadata.LazyPropertiesMetadata; + indexes = new int[uninitializedLazyProperties.Length]; + lazyIndexes = new int[uninitializedLazyProperties.Length]; + for (var i = 0; i < uninitializedLazyProperties.Length; i++) + { + var descriptor = metadata.GetLazyPropertyDescriptor(uninitializedLazyProperties[i]); + indexes[i] = descriptor.PropertyIndex; + lazyIndexes[i] = descriptor.LazyIndex; + } + } + + var values = Hydrate(rs, id, entity, rootPersister, suffixedPropertyColumns, null, true, indexes, session); + for (var i = 0; i < lazyIndexes.Length; i++) + { + var value = values[i]; + if (entry.Status == Status.Loading) + { + // Here loaded state contains hydrated values and is always not null even in ReadOnly mode. + // We don't need to set the property value here as it will be set in TwoPhaseLoad.InitializeEntity + entry.LoadedState[indexes[i]] = value; + } + else + { + var lazyIndex = lazyIndexes[i]; + var propValue = lazyPropertyTypes[lazyIndex].Assemble(value, session, entity); + InitializeLazyProperty(entity, entry.LoadedState, lazyIndex, propValue, null); + } + } + } + + private object InitializeLazyPropertiesFromDatastore( + string fieldName, object entity, ISessionImplementor session, object id, EntityEntry entry, + ISet uninitializedLazyProperties) { if (!HasLazyProperties) throw new AssertionFailure("no lazy properties"); @@ -1335,7 +1389,7 @@ private object InitializeLazyPropertiesFromDatastore(string fieldName, object en for (int j = 0; j < lazyPropertyNames.Length; j++) { object propValue = lazyPropertyTypes[j].NullSafeGet(rs, lazyPropertyColumnAliases[j], session, entity); - if (InitializeLazyProperty(fieldName, entity, session, snapshot, j, propValue, uninitializedLazyProperties)) + if (InitializeLazyProperty(fieldName, entity, snapshot, j, propValue, uninitializedLazyProperties)) { result = propValue; } @@ -1365,8 +1419,9 @@ private object InitializeLazyPropertiesFromDatastore(string fieldName, object en } } - private object InitializeLazyPropertiesFromCache(string fieldName, object entity, ISessionImplementor session, EntityEntry entry, CacheEntry cacheEntry, - ISet uninitializedLazyProperties) + private object InitializeLazyPropertiesFromCache( + string fieldName, object entity, ISessionImplementor session, EntityEntry entry, CacheEntry cacheEntry, + ISet uninitializedLazyProperties) { log.Debug("initializing lazy properties from second-level cache"); @@ -1376,7 +1431,7 @@ private object InitializeLazyPropertiesFromCache(string fieldName, object entity for (int j = 0; j < lazyPropertyNames.Length; j++) { object propValue = lazyPropertyTypes[j].Assemble(disassembledValues[lazyPropertyNumbers[j]], session, entity); - if (InitializeLazyProperty(fieldName, entity, session, snapshot, j, propValue, uninitializedLazyProperties)) + if (InitializeLazyProperty(fieldName, entity, snapshot, j, propValue, uninitializedLazyProperties)) { result = propValue; } @@ -1387,20 +1442,27 @@ private object InitializeLazyPropertiesFromCache(string fieldName, object entity return result; } - private bool InitializeLazyProperty(string fieldName, object entity, ISessionImplementor session, object[] snapshot, int j, object propValue, - ISet uninitializedLazyProperties) + private bool InitializeLazyProperty( + string fieldName, object entity, object[] snapshot, int lazyIndex, object propValue, + ISet uninitializedLazyProperties) { - if (uninitializedLazyProperties.Contains(lazyPropertyNames[j])) + InitializeLazyProperty(entity, snapshot, lazyIndex, propValue, uninitializedLazyProperties); + return fieldName.Equals(lazyPropertyNames[lazyIndex]); + } + + private void InitializeLazyProperty( + object entity, object[] snapshot, int lazyIndex, object propValue, ISet uninitializedLazyProperties) + { + if (uninitializedLazyProperties == null || uninitializedLazyProperties.Contains(lazyPropertyNames[lazyIndex])) { - SetPropertyValue(entity, lazyPropertyNumbers[j], propValue); + SetPropertyValue(entity, lazyPropertyNumbers[lazyIndex], propValue); } if (snapshot != null) { // object have been loaded with setReadOnly(true); HHH-2236 - snapshot[lazyPropertyNumbers[j]] = lazyPropertyTypes[j].DeepCopy(propValue, factory); + snapshot[lazyPropertyNumbers[lazyIndex]] = lazyPropertyTypes[lazyIndex].DeepCopy(propValue, factory); } - return fieldName.Equals(lazyPropertyNames[j]); } public string[] IdentifierAliases @@ -1449,6 +1511,16 @@ public virtual string IdentifierSelectFragment(string name, string suffix) } public string PropertySelectFragment(string name, string suffix, bool allProperties) + { + return PropertySelectFragment(name, suffix, null, allProperties); + } + + public string PropertySelectFragment(string name, string suffix, string[] fetchProperties) + { + return PropertySelectFragment(name, suffix, fetchProperties, false); + } + + private string PropertySelectFragment(string name, string suffix, string[] fetchProperties, bool allProperties) { SelectFragment select = new SelectFragment(Factory.Dialect) .SetSuffix(suffix) @@ -1457,10 +1529,35 @@ public string PropertySelectFragment(string name, string suffix, bool allPropert int[] columnTableNumbers = SubclassColumnTableNumberClosure; string[] columnAliases = SubclassColumnAliasClosure; string[] columns = SubclassColumnClosure; + HashSet fetchColumnsAndFormulas = null; + if (fetchProperties != null) + { + fetchColumnsAndFormulas = new HashSet(); + foreach (var fetchProperty in fetchProperties) + { + var index = GetSubclassPropertyIndex(fetchProperty); + if (index < 0) + { + throw new InvalidOperationException($"Property {fetchProperty} does not exist on entity {EntityName}"); + } + + var columnNames = SubclassPropertyColumnNameClosure[index]; + // Formulas will have all null values + if (columnNames.All(o => o == null)) + { + columnNames = SubclassPropertyFormulaTemplateClosure[index]; + } + + foreach (var columnName in columnNames) + { + fetchColumnsAndFormulas.Add(columnName); + } + } + } for (int i = 0; i < columns.Length; i++) { - bool selectable = (allProperties || !subclassColumnLazyClosure[i]) && + bool selectable = (allProperties || !subclassColumnLazyClosure[i] || fetchColumnsAndFormulas?.Contains(columns[i]) == true) && !IsSubclassTableSequentialSelect(columnTableNumbers[i]) && subclassColumnSelectableClosure[i]; if (selectable) @@ -1475,7 +1572,7 @@ public string PropertySelectFragment(string name, string suffix, bool allPropert string[] formulaAliases = SubclassFormulaAliasClosure; for (int i = 0; i < formulaTemplates.Length; i++) { - bool selectable = (allProperties || !subclassFormulaLazyClosure[i]) && + bool selectable = (allProperties || !subclassFormulaLazyClosure[i] || fetchColumnsAndFormulas?.Contains(formulaTemplates[i]) == true) && !IsSubclassTableSequentialSelect(formulaTableNumbers[i]); if (selectable) { @@ -2589,7 +2686,29 @@ protected int Dehydrate(object id, object[] fields, object rowId, bool[] include /// without resolving associations or collections /// public object[] Hydrate(DbDataReader rs, object id, object obj, ILoadable rootLoadable, - string[][] suffixedPropertyColumns, bool allProperties, ISessionImplementor session) + string[][] suffixedPropertyColumns, ISet fetchedLazyProperties, bool allProperties, ISessionImplementor session) + { + return Hydrate(rs, id, obj, rootLoadable, suffixedPropertyColumns, fetchedLazyProperties, allProperties, null, session); + } + + /// + /// Unmarshall the fields of a persistent instance from a result set, + /// without resolving associations or collections + /// + // Since v5.3 + [Obsolete("Use the overload with fetchedLazyProperties parameter instead")] + public object[] Hydrate(DbDataReader rs, object id, object obj, ILoadable rootLoadable, + string[][] suffixedPropertyColumns, bool allProperties, ISessionImplementor session) + { + return Hydrate(rs, id, obj, rootLoadable, suffixedPropertyColumns, null, allProperties, null, session); + } + + /// + /// Unmarshall the fields of a persistent instance from a result set, + /// without resolving associations or collections + /// + private object[] Hydrate(DbDataReader rs, object id, object obj, ILoadable rootLoadable, string[][] suffixedPropertyColumns, + ISet fetchedLazyProperties, bool allProperties, int[] indexes, ISessionImplementor session) { if (log.IsDebugEnabled()) { @@ -2642,37 +2761,40 @@ public object[] Hydrate(DbDataReader rs, object id, object obj, ILoadable rootLo } } + int i; + int length = indexes?.Length ?? PropertyTypes.Length; string[] propNames = PropertyNames; IType[] types = PropertyTypes; - object[] values = new object[types.Length]; + object[] values = new object[length]; bool[] laziness = PropertyLaziness; string[] propSubclassNames = SubclassPropertySubclassNameClosure; - for (int i = 0; i < types.Length; i++) + for (int j = 0; j < length; j++) { + i = indexes?[j] ?? j; if (!propertySelectable[i]) { - values[i] = BackrefPropertyAccessor.Unknown; + values[j] = BackrefPropertyAccessor.Unknown; } - else if (allProperties || !laziness[i]) + else if (allProperties || !laziness[i] || fetchedLazyProperties?.Contains(propNames[i]) == true) { //decide which ResultSet to get the property value from: bool propertyIsDeferred = hasDeferred && rootPersister.IsSubclassPropertyDeferred(propNames[i], propSubclassNames[i]); if (propertyIsDeferred && sequentialSelectEmpty) { - values[i] = null; + values[j] = null; } else { var propertyResultSet = propertyIsDeferred ? sequentialResultSet : rs; string[] cols = propertyIsDeferred ? propertyColumnAliases[i] : suffixedPropertyColumns[i]; - values[i] = types[i].Hydrate(propertyResultSet, cols, session, obj); + values[j] = types[i].Hydrate(propertyResultSet, cols, session, obj); } } else { - values[i] = LazyPropertyInitializer.UnfetchedProperty; + values[j] = LazyPropertyInitializer.UnfetchedProperty; } } diff --git a/src/NHibernate/Persister/Entity/ILoadable.cs b/src/NHibernate/Persister/Entity/ILoadable.cs index 22bea023e84..8991745ab38 100644 --- a/src/NHibernate/Persister/Entity/ILoadable.cs +++ b/src/NHibernate/Persister/Entity/ILoadable.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using NHibernate.Type; using NHibernate.Engine; using System.Data.Common; @@ -68,7 +70,58 @@ public partial interface ILoadable : IEntityPersister /// /// Retrieve property values from one row of a result set /// + // Since v5.3 + [Obsolete("Use the extension method with fetchedLazyProperties parameter instead")] object[] Hydrate(DbDataReader rs, object id, object obj, ILoadable rootLoadable, string[][] suffixedPropertyColumns, bool allProperties, ISessionImplementor session); } + + public static partial class LoadableExtensions + { + /// + /// Retrieve property values from one row of a result set + /// + //6.0 TODO: Merge into ILoadable + public static object[] Hydrate( + this ILoadable loadable, DbDataReader rs, object id, object obj, ILoadable rootLoadable, + string[][] suffixedPropertyColumns, ISet fetchedLazyProperties, bool allProperties, ISessionImplementor session) + { + if (loadable is AbstractEntityPersister abstractEntityPersister) + { + return abstractEntityPersister.Hydrate( + rs, id, obj, rootLoadable, suffixedPropertyColumns, fetchedLazyProperties, allProperties, session); + } + +#pragma warning disable 618 + // Fallback to the old behavior + return loadable.Hydrate(rs, id, obj, rootLoadable, suffixedPropertyColumns, allProperties, session); +#pragma warning restore 618 + } + + /// + /// Set lazy properties from one row of a result set + /// + //6.0 TODO: Change to void and merge into ILoadable + internal static bool InitializeLazyProperties( + this ILoadable loadable, DbDataReader rs, object id, object entity, ILoadable rootPersister, string[][] suffixedPropertyColumns, + string[] uninitializedLazyProperties, bool allLazyProperties, ISessionImplementor session) + { + if (loadable is AbstractEntityPersister abstractEntityPersister) + { + abstractEntityPersister.InitializeLazyProperties( + rs, + id, + entity, + rootPersister, + suffixedPropertyColumns, + uninitializedLazyProperties, + allLazyProperties, + session); + + return true; + } + + return false; + } + } } diff --git a/src/NHibernate/Persister/Entity/IQueryable.cs b/src/NHibernate/Persister/Entity/IQueryable.cs index 51b5665ac52..a0fe5934e8d 100644 --- a/src/NHibernate/Persister/Entity/IQueryable.cs +++ b/src/NHibernate/Persister/Entity/IQueryable.cs @@ -1,3 +1,6 @@ +using System; +using NHibernate.Util; + namespace NHibernate.Persister.Entity { public enum Declarer @@ -7,6 +10,19 @@ public enum Declarer SuperClass } + internal static class AbstractEntityPersisterExtensions + { + /// + /// Given a query alias and an identifying suffix, render the property select fragment. + /// + //6.0 TODO: Merge into IQueryable + public static string PropertySelectFragment(this IQueryable query, string alias, string suffix, string[] fetchProperties) + { + return ReflectHelper.CastOrThrow(query, "individual lazy property fetches") + .PropertySelectFragment(alias, suffix, fetchProperties); + } + } + /// /// Extends the generic ILoadable contract to add operations required by HQL ///