From 63e82b7f5558171420aad0a76eef42bed049633a Mon Sep 17 00:00:00 2001 From: maca88 Date: Thu, 17 Sep 2020 22:10:16 +0200 Subject: [PATCH 1/6] Fix parameter detection for expression arguments (#2542) Fixes #2537 --- .../Async/Linq/WhereSubqueryTests.cs | 25 +++++++++++++++++++ .../Linq/WhereSubqueryTests.cs | 25 +++++++++++++++++++ .../NhPartialEvaluatingExpressionVisitor.cs | 4 ++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/NHibernate.Test/Async/Linq/WhereSubqueryTests.cs b/src/NHibernate.Test/Async/Linq/WhereSubqueryTests.cs index 713b931e3b7..38303f0986d 100644 --- a/src/NHibernate.Test/Async/Linq/WhereSubqueryTests.cs +++ b/src/NHibernate.Test/Async/Linq/WhereSubqueryTests.cs @@ -707,5 +707,30 @@ public async Task NullComparedToMemberInitExpressionInWhereClauseAsync() Assert.That(result.Count, Is.EqualTo(45)); } + + public class Specification + { + private Expression> _expression; + + public Specification(Expression> expression) + { + _expression = expression; + } + + public static implicit operator Expression>(Specification specification) + { + return specification._expression; + } + } + + [Test] + public async Task ImplicitConversionInsideWhereSubqueryExpressionAsync() + { + if (!Dialect.SupportsScalarSubSelects) + Assert.Ignore(Dialect.GetType().Name + " does not support scalar sub-queries"); + + var spec = new Specification(x => x.Freight > 1000); + await (db.Orders.Where(o => db.Orders.Where(spec).Any(x => x.OrderId == o.OrderId)).ToListAsync()); + } } } diff --git a/src/NHibernate.Test/Linq/WhereSubqueryTests.cs b/src/NHibernate.Test/Linq/WhereSubqueryTests.cs index 99437428623..9531ccf7273 100644 --- a/src/NHibernate.Test/Linq/WhereSubqueryTests.cs +++ b/src/NHibernate.Test/Linq/WhereSubqueryTests.cs @@ -788,5 +788,30 @@ public void NullComparedToMemberInitExpressionInWhereClause() Assert.That(result.Count, Is.EqualTo(45)); } + + public class Specification + { + private Expression> _expression; + + public Specification(Expression> expression) + { + _expression = expression; + } + + public static implicit operator Expression>(Specification specification) + { + return specification._expression; + } + } + + [Test] + public void ImplicitConversionInsideWhereSubqueryExpression() + { + if (!Dialect.SupportsScalarSubSelects) + Assert.Ignore(Dialect.GetType().Name + " does not support scalar sub-queries"); + + var spec = new Specification(x => x.Freight > 1000); + db.Orders.Where(o => db.Orders.Where(spec).Any(x => x.OrderId == o.OrderId)).ToList(); + } } } diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 475dbcdad31..c92403cbc82 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -157,7 +157,9 @@ private Expression EvaluateSubtree(Expression subtree) private bool ContainsVariable(Expression expression) { - if (!(expression is UnaryExpression unaryExpression)) + if (!(expression is UnaryExpression unaryExpression) || + // Avoid detecting expression variables as parameters + typeof(Expression).IsAssignableFrom(expression.Type)) { return false; } From 3fab693687ecc76026416c53faabc53e2dcaf1ae Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Fri, 18 Sep 2020 14:49:08 +0300 Subject: [PATCH 2/6] Apply filters for entity joins in hql (#2550) Fixes #2549 --- .../Async/NHSpecificTest/GH2549/Fixture.cs | 82 +++++++++++++++++++ .../NHSpecificTest/GH2549/Fixture.cs | 70 ++++++++++++++++ .../NHSpecificTest/GH2549/Mappings.hbm.xml | 26 ++++++ .../NHSpecificTest/GH2549/Model.cs | 15 ++++ .../Ast/ANTLR/Tree/EntityJoinFromElement.cs | 13 ++- .../ANTLR/Tree/EntityJoinJoinSequenceImpl.cs | 51 ------------ 6 files changed, 199 insertions(+), 58 deletions(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH2549/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2549/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2549/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2549/Model.cs delete mode 100644 src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2549/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2549/Fixture.cs new file mode 100644 index 00000000000..82227867203 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2549/Fixture.cs @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +// +// 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.Linq; +using NUnit.Framework; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.GH2549 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Save(new Person {Id = 1, Name = "Name"}); + s.Save(new Customer {Deleted = false, Name = "Name", Id = 1}); + s.Save(new Customer {Deleted = true, Name = "Name", Id = 2}); + + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.CreateQuery("delete from System.Object").ExecuteUpdate(); + t.Commit(); + } + } + + [Test] + public async Task EntityJoinFilterLinqAsync() + { + using (var s = OpenSession()) + { + var list = await ((from p in s.Query() + join c in s.Query() on p.Name equals c.Name + select p).ToListAsync()); + + s.EnableFilter("DeletedCustomer").SetParameter("deleted", false); + + var filteredList = await ((from p in s.Query() + join c in s.Query() on p.Name equals c.Name + select p).ToListAsync()); + + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That(filteredList, Has.Count.EqualTo(1)); + } + } + + [Test] + public async Task EntityJoinFilterQueryOverAsync() + { + using (var s = OpenSession()) + { + Customer c = null; + Person p = null; + var list = await (s.QueryOver(() => p).JoinEntityAlias(() => c, () => c.Name == p.Name).ListAsync()); + + s.EnableFilter("DeletedCustomer").SetParameter("deleted", false); + + var filteredList = await (s.QueryOver(() => p).JoinEntityAlias(() => c, () => c.Name == p.Name).ListAsync()); + + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That(filteredList, Has.Count.EqualTo(1)); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2549/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2549/Fixture.cs new file mode 100644 index 00000000000..a01c1ec2572 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2549/Fixture.cs @@ -0,0 +1,70 @@ +using System.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2549 +{ + [TestFixture] + public class Fixture : BugTestCase + { + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Save(new Person {Id = 1, Name = "Name"}); + s.Save(new Customer {Deleted = false, Name = "Name", Id = 1}); + s.Save(new Customer {Deleted = true, Name = "Name", Id = 2}); + + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.CreateQuery("delete from System.Object").ExecuteUpdate(); + t.Commit(); + } + } + + [Test] + public void EntityJoinFilterLinq() + { + using (var s = OpenSession()) + { + var list = (from p in s.Query() + join c in s.Query() on p.Name equals c.Name + select p).ToList(); + + s.EnableFilter("DeletedCustomer").SetParameter("deleted", false); + + var filteredList = (from p in s.Query() + join c in s.Query() on p.Name equals c.Name + select p).ToList(); + + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That(filteredList, Has.Count.EqualTo(1)); + } + } + + [Test] + public void EntityJoinFilterQueryOver() + { + using (var s = OpenSession()) + { + Customer c = null; + Person p = null; + var list = s.QueryOver(() => p).JoinEntityAlias(() => c, () => c.Name == p.Name).List(); + + s.EnableFilter("DeletedCustomer").SetParameter("deleted", false); + + var filteredList = s.QueryOver(() => p).JoinEntityAlias(() => c, () => c.Name == p.Name).List(); + + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That(filteredList, Has.Count.EqualTo(1)); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2549/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH2549/Mappings.hbm.xml new file mode 100644 index 00000000000..65b540b9202 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2549/Mappings.hbm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/GH2549/Model.cs b/src/NHibernate.Test/NHSpecificTest/GH2549/Model.cs new file mode 100644 index 00000000000..b6d678c7b20 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2549/Model.cs @@ -0,0 +1,15 @@ +namespace NHibernate.Test.NHSpecificTest.GH2549 +{ + public class Customer + { + public virtual int Id { get; set; } + public virtual bool Deleted { get; set; } + public virtual string Name { get; set; } + } + + public class Person + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs index e2f4ce33365..4458d5cecce 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs @@ -1,4 +1,6 @@ -using Antlr.Runtime; +using System; +using Antlr.Runtime; +using NHibernate.Engine; using NHibernate.Persister.Entity; using NHibernate.SqlCommand; using NHibernate.Type; @@ -15,12 +17,9 @@ public EntityJoinFromElement(FromClause fromClause, IQueryable entityPersister, EntityType entityType = (EntityType) entityPersister.Type; InitializeEntity(fromClause, entityPersister.EntityName, entityPersister, entityType, alias, tableAlias); - JoinSequence = new EntityJoinJoinSequenceImpl( - SessionFactoryHelper.Factory, - entityType, - entityPersister.TableName, - tableAlias, - joinType); + //NH Specific: hibernate uses special class EntityJoinJoinSequenceImpl + JoinSequence = new JoinSequence(SessionFactoryHelper.Factory) + .AddJoin(entityType, tableAlias, joinType, Array.Empty()); fromClause.Walker.AddQuerySpaces(entityPersister.QuerySpaces); } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs deleted file mode 100644 index 430515223d0..00000000000 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using NHibernate.Engine; -using NHibernate.SqlCommand; -using NHibernate.Type; - -namespace NHibernate.Hql.Ast.ANTLR.Tree -{ - class EntityJoinJoinSequenceImpl : JoinSequence - { - private readonly EntityType _entityType; - private readonly string _tableName; - private readonly string _tableAlias; - private readonly JoinType _joinType; - - public EntityJoinJoinSequenceImpl(ISessionFactoryImplementor factory, EntityType entityType, string tableName, string tableAlias, JoinType joinType):base(factory) - { - _entityType = entityType; - _tableName = tableName; - _tableAlias = tableAlias; - _joinType = joinType; - } - - internal override JoinFragment ToJoinFragment( - IDictionary enabledFilters, - bool includeExtraJoins, - SqlString withClauseFragment, - string withClauseJoinAlias, - string withRootAlias) - { - var joinFragment = new ANSIJoinFragment(); - - var on = withClauseFragment ?? SqlString.Empty; - //Note: filters logic commented due to following issues - //1) Original code is non functional as SqlString is immutable and so all Append results are lost. Correct code would look like: on = on.Append(filters); - //2) Also it seems GetOnCondition always returns empty string for entity join (as IsReferenceToPrimaryKey is always true). - // So if filters for entity join really make sense we need to inline GetOnCondition part that retrieves filters -// var filters = _entityType.GetOnCondition(_tableAlias, Factory, enabledFilters); -// if (!string.IsNullOrEmpty(filters)) -// { -// on.Append(" and ").Append(filters); -// } - joinFragment.AddJoin(_tableName, _tableAlias, Array.Empty(), Array.Empty(), _joinType, on); - if (includeExtraJoins) - { - AddExtraJoins(joinFragment, _tableAlias, _entityType.GetAssociatedJoinable(Factory), _joinType == JoinType.InnerJoin); - } - return joinFragment; - } - } -} From 186a88b44efbf1e9647ea6e47cb73bcf7c07df37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Fri, 18 Sep 2020 22:13:14 +0200 Subject: [PATCH 3/6] Enable 5.3.4 dev versions --- build-common/NHibernate.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index 4d23e8d3d54..62ec177ced1 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -3,9 +3,9 @@ 5.3 - 3 + 4 - + dev $(NhVersion).$(VersionPatch) $(VersionSuffix).$(BuildNumber) From da95db47f2cb0423650bb3c2147119aa31ce3d37 Mon Sep 17 00:00:00 2001 From: illkostinf <71636371+illkostinf@users.noreply.github.com> Date: Wed, 23 Sep 2020 13:01:44 +0300 Subject: [PATCH 4/6] Fix caching linq query with ThenFetchMany (#2558) Fixes #2559 Co-authored-by: maca88 --- .../NHSpecificTest/GH2559/FixtureByCode.cs | 156 ++++++++++++++++++ .../NHSpecificTest/GH2559/Car.cs | 12 ++ .../NHSpecificTest/GH2559/Child.cs | 20 +++ .../NHSpecificTest/GH2559/FixtureByCode.cs | 145 ++++++++++++++++ .../NHSpecificTest/GH2559/Person.cs | 26 +++ .../NHSpecificTest/GH2559/Pet.cs | 12 ++ src/NHibernate/Async/Type/CollectionType.cs | 5 + src/NHibernate/Type/CollectionType.cs | 5 + 8 files changed, 381 insertions(+) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH2559/FixtureByCode.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2559/Car.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2559/Child.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2559/FixtureByCode.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2559/Person.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH2559/Pet.cs diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2559/FixtureByCode.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2559/FixtureByCode.cs new file mode 100644 index 00000000000..8dea23c6d76 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2559/FixtureByCode.cs @@ -0,0 +1,156 @@ +//------------------------------------------------------------------------------ +// +// 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.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2559 +{ + using System.Threading.Tasks; + [TestFixture] + public class ByCodeFixtureAsync : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Guid)); + rc.Property(x => x.Name); + rc.Property(x => x.Age); + rc.Set( + x => x.Children, + colMap => + { + colMap.Inverse(true); + colMap.Cascade(Mapping.ByCode.Cascade.DeleteOrphans); + colMap.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }, + rel => rel.OneToMany()); + rc.Set( + x => x.Cars, + colMap => + { + colMap.Inverse(true); + colMap.Cascade(Mapping.ByCode.Cascade.DeleteOrphans); + colMap.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }, + rel => rel.OneToMany()); + rc.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + mapper.Class(ch => + { + ch.Id(x => x.Id, m => m.Generator(Generators.Guid)); + ch.Property(x => x.Name); + ch.ManyToOne(c => c.Parent); + + ch.Set( + x => x.Pets, + colMap => + { + colMap.Inverse(true); + colMap.Cascade(Mapping.ByCode.Cascade.DeleteOrphans); + colMap.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }, + rel => rel.OneToMany()); + + ch.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + mapper.Class(ch => + { + ch.Id(x => x.Id, m => m.Generator(Generators.Guid)); + ch.Property(x => x.Name); + ch.ManyToOne(c => c.Owner); + + ch.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + mapper.Class(ch => + { + ch.Id(x => x.Id, m => m.Generator(Generators.Guid)); + ch.Property(x => x.Name); + ch.ManyToOne(c => c.Owner); + + ch.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var person = new Person { Name = "Person 1", Age = 18 }; + + var car1 = new Car { Name = "Car1", Owner = person }; + var car2 = new Car { Name = "Car2", Owner = person }; + session.Save(car1); + session.Save(car2); + + session.Save(person); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Pet").ExecuteUpdate(); + session.CreateQuery("delete from Child").ExecuteUpdate(); + session.CreateQuery("delete from Car").ExecuteUpdate(); + session.CreateQuery("delete from Person").ExecuteUpdate(); + transaction.Commit(); + } + } + + [Test] + public async Task TestQueryCachingWithThenFetchManyAsync() + { + Person dbPerson; + Person cachePerson; + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var query = + session + .Query() + .FetchMany(p => p.Children) + .ThenFetchMany(ch => ch.Pets) + .FetchMany(p => p.Cars) as IQueryable; + + query = query.WithOptions(opt => + opt.SetCacheable(true) + .SetCacheMode(CacheMode.Normal) + .SetCacheRegion("Long_Cache")); + + dbPerson = (await (query.ToListAsync())).First(); // First time the result will be cached + cachePerson = (await (query.ToListAsync())).First(); + + await (transaction.CommitAsync()); + } + + Assert.That(NHibernateUtil.IsInitialized(dbPerson.Cars), Is.True); + Assert.That(NHibernateUtil.IsInitialized(cachePerson.Cars), Is.True); + Assert.That(dbPerson.Cars, Has.Count.EqualTo(2)); + Assert.That(cachePerson.Cars, Has.Count.EqualTo(2)); + + Assert.That(NHibernateUtil.IsInitialized(dbPerson.Children), Is.True); + Assert.That(NHibernateUtil.IsInitialized(cachePerson.Children), Is.True); + Assert.That(dbPerson.Children, Has.Count.EqualTo(0)); + Assert.That(cachePerson.Children, Has.Count.EqualTo(0)); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2559/Car.cs b/src/NHibernate.Test/NHSpecificTest/GH2559/Car.cs new file mode 100644 index 00000000000..b93e028c8b6 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2559/Car.cs @@ -0,0 +1,12 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH2559 +{ + public class Car + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + + public virtual Person Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2559/Child.cs b/src/NHibernate.Test/NHSpecificTest/GH2559/Child.cs new file mode 100644 index 00000000000..43f56d8b701 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2559/Child.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH2559 +{ + public class Child + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + + public virtual Person Parent { get; set; } + + public virtual ISet Pets + { + get => _pets ?? (_pets = new HashSet()); + set => _pets = value; + } + private ISet _pets; + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2559/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH2559/FixtureByCode.cs new file mode 100644 index 00000000000..33348db8bd9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2559/FixtureByCode.cs @@ -0,0 +1,145 @@ +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2559 +{ + [TestFixture] + public class ByCodeFixture : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Guid)); + rc.Property(x => x.Name); + rc.Property(x => x.Age); + rc.Set( + x => x.Children, + colMap => + { + colMap.Inverse(true); + colMap.Cascade(Mapping.ByCode.Cascade.DeleteOrphans); + colMap.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }, + rel => rel.OneToMany()); + rc.Set( + x => x.Cars, + colMap => + { + colMap.Inverse(true); + colMap.Cascade(Mapping.ByCode.Cascade.DeleteOrphans); + colMap.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }, + rel => rel.OneToMany()); + rc.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + mapper.Class(ch => + { + ch.Id(x => x.Id, m => m.Generator(Generators.Guid)); + ch.Property(x => x.Name); + ch.ManyToOne(c => c.Parent); + + ch.Set( + x => x.Pets, + colMap => + { + colMap.Inverse(true); + colMap.Cascade(Mapping.ByCode.Cascade.DeleteOrphans); + colMap.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }, + rel => rel.OneToMany()); + + ch.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + mapper.Class(ch => + { + ch.Id(x => x.Id, m => m.Generator(Generators.Guid)); + ch.Property(x => x.Name); + ch.ManyToOne(c => c.Owner); + + ch.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + mapper.Class(ch => + { + ch.Id(x => x.Id, m => m.Generator(Generators.Guid)); + ch.Property(x => x.Name); + ch.ManyToOne(c => c.Owner); + + ch.Cache(c => c.Usage(CacheUsage.ReadWrite)); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var person = new Person { Name = "Person 1", Age = 18 }; + + var car1 = new Car { Name = "Car1", Owner = person }; + var car2 = new Car { Name = "Car2", Owner = person }; + session.Save(car1); + session.Save(car2); + + session.Save(person); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Pet").ExecuteUpdate(); + session.CreateQuery("delete from Child").ExecuteUpdate(); + session.CreateQuery("delete from Car").ExecuteUpdate(); + session.CreateQuery("delete from Person").ExecuteUpdate(); + transaction.Commit(); + } + } + + [Test] + public void TestQueryCachingWithThenFetchMany() + { + Person dbPerson; + Person cachePerson; + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var query = + session + .Query() + .FetchMany(p => p.Children) + .ThenFetchMany(ch => ch.Pets) + .FetchMany(p => p.Cars) as IQueryable; + + query = query.WithOptions(opt => + opt.SetCacheable(true) + .SetCacheMode(CacheMode.Normal) + .SetCacheRegion("Long_Cache")); + + dbPerson = query.ToList().First(); // First time the result will be cached + cachePerson = query.ToList().First(); + + transaction.Commit(); + } + + Assert.That(NHibernateUtil.IsInitialized(dbPerson.Cars), Is.True); + Assert.That(NHibernateUtil.IsInitialized(cachePerson.Cars), Is.True); + Assert.That(dbPerson.Cars, Has.Count.EqualTo(2)); + Assert.That(cachePerson.Cars, Has.Count.EqualTo(2)); + + Assert.That(NHibernateUtil.IsInitialized(dbPerson.Children), Is.True); + Assert.That(NHibernateUtil.IsInitialized(cachePerson.Children), Is.True); + Assert.That(dbPerson.Children, Has.Count.EqualTo(0)); + Assert.That(cachePerson.Children, Has.Count.EqualTo(0)); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2559/Person.cs b/src/NHibernate.Test/NHSpecificTest/GH2559/Person.cs new file mode 100644 index 00000000000..8d125021ec9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2559/Person.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH2559 +{ + public class Person + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual int Age { get; set; } + + public virtual ISet Cars + { + get => _cars ?? (_cars = new HashSet()); + set => _cars = value; + } + private ISet _cars; + + public virtual ISet Children + { + get => _children ?? (_children = new HashSet()); + set => _children = value; + } + private ISet _children; + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2559/Pet.cs b/src/NHibernate.Test/NHSpecificTest/GH2559/Pet.cs new file mode 100644 index 00000000000..7b580d24bd1 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2559/Pet.cs @@ -0,0 +1,12 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH2559 +{ + public class Pet + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + + public virtual Child Owner { get; set; } + } +} diff --git a/src/NHibernate/Async/Type/CollectionType.cs b/src/NHibernate/Async/Type/CollectionType.cs index b103b445c22..432c11957a8 100644 --- a/src/NHibernate/Async/Type/CollectionType.cs +++ b/src/NHibernate/Async/Type/CollectionType.cs @@ -104,6 +104,11 @@ public override async Task DisassembleAsync(object value, ISessionImplem public override async Task BeforeAssembleAsync(object oid, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + if (oid == null) + { + return; + } + var queryCacheQueue = session.PersistenceContext.BatchFetchQueue.QueryCacheQueue; if (queryCacheQueue == null) { diff --git a/src/NHibernate/Type/CollectionType.cs b/src/NHibernate/Type/CollectionType.cs index 4ac7c434d53..408e03d94ba 100644 --- a/src/NHibernate/Type/CollectionType.cs +++ b/src/NHibernate/Type/CollectionType.cs @@ -160,6 +160,11 @@ public override object Disassemble(object value, ISessionImplementor session, ob public override void BeforeAssemble(object oid, ISessionImplementor session) { + if (oid == null) + { + return; + } + var queryCacheQueue = session.PersistenceContext.BatchFetchQueue.QueryCacheQueue; if (queryCacheQueue == null) { From b27b7f7969d16c2f93ce060fc985a45fc94cb4c0 Mon Sep 17 00:00:00 2001 From: maca88 Date: Wed, 14 Oct 2020 23:15:24 +0200 Subject: [PATCH 5/6] Fix joining with an inherited property from another table for Linq provider (#2581) --- .../Async/Linq/ByMethod/JoinTests.cs | 8 +++ .../Linq/ByMethod/JoinTests.cs | 8 +++ .../Linq/Visitors/QueryModelVisitor.cs | 59 ++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/ByMethod/JoinTests.cs b/src/NHibernate.Test/Async/Linq/ByMethod/JoinTests.cs index 987ed002f60..9fda2e873c9 100644 --- a/src/NHibernate.Test/Async/Linq/ByMethod/JoinTests.cs +++ b/src/NHibernate.Test/Async/Linq/ByMethod/JoinTests.cs @@ -148,5 +148,13 @@ public async Task CanJoinOnEntityWithSubclassesAsync() from o2 in db.Animals.Where(x => x.BodyWeight > 50) select new {o, o2}).Take(1).ToListAsync()); } + + [Test(Description = "GH-2580")] + public async Task CanInnerJoinOnSubclassWithBaseTableReferenceInOnClauseAsync() + { + var result = await ((from o in db.Animals + join o2 in db.Mammals on o.BodyWeight equals o2.BodyWeight + select new { o, o2 }).Take(1).ToListAsync()); + } } } diff --git a/src/NHibernate.Test/Linq/ByMethod/JoinTests.cs b/src/NHibernate.Test/Linq/ByMethod/JoinTests.cs index 8dce4d39223..a7a4750b87a 100644 --- a/src/NHibernate.Test/Linq/ByMethod/JoinTests.cs +++ b/src/NHibernate.Test/Linq/ByMethod/JoinTests.cs @@ -137,5 +137,13 @@ public void CanJoinOnEntityWithSubclasses() from o2 in db.Animals.Where(x => x.BodyWeight > 50) select new {o, o2}).Take(1).ToList(); } + + [Test(Description = "GH-2580")] + public void CanInnerJoinOnSubclassWithBaseTableReferenceInOnClause() + { + var result = (from o in db.Animals + join o2 in db.Mammals on o.BodyWeight equals o2.BodyWeight + select new { o, o2 }).Take(1).ToList(); + } } } diff --git a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index 040e9b38932..62abaae77cb 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using NHibernate.Engine; using NHibernate.Hql.Ast; using NHibernate.Linq.Clauses; using NHibernate.Linq.Expressions; @@ -12,6 +13,7 @@ using NHibernate.Linq.ResultOperators; using NHibernate.Linq.ReWriters; using NHibernate.Linq.Visitors.ResultOperatorProcessors; +using NHibernate.Persister.Entity; using NHibernate.Util; using Remotion.Linq; using Remotion.Linq.Clauses; @@ -527,10 +529,13 @@ private void AddJoin(JoinClause joinClause, QueryModel queryModel, bool innerJoi var withClause = equalityVisitor.Visit(joinClause.InnerKeySelector, joinClause.OuterKeySelector); var alias = _hqlTree.TreeBuilder.Alias(VisitorParameters.QuerySourceNamer.GetName(joinClause)); var joinExpression = HqlGeneratorExpressionVisitor.Visit(joinClause.InnerSequence, VisitorParameters); + var baseMemberCheker = new BaseMemberChecker(VisitorParameters.SessionFactory); + HqlTreeNode join; - // When associations are located inside the inner key selector we have to use a cross join instead of an inner - // join and add the condition in the where statement. - if (queryModel.BodyClauses.OfType().Any(o => o.ParentJoinClause == joinClause)) + // When associations or members from another table are located inside the inner key selector we have to use a cross join + // instead of an inner join and add the condition in the where statement. + if (queryModel.BodyClauses.OfType().Any(o => o.ParentJoinClause == joinClause) || + queryModel.BodyClauses.OfType().Any(baseMemberCheker.ContainsBaseMember)) { if (!innerJoin) { @@ -551,6 +556,54 @@ private void AddJoin(JoinClause joinClause, QueryModel queryModel, bool innerJoi _hqlTree.AddFromClause(join); } + private class BaseMemberChecker : NhExpressionVisitor + { + private readonly ISessionFactoryImplementor _sessionFactory; + private bool _result; + + public BaseMemberChecker(ISessionFactoryImplementor sessionFactory) + { + _sessionFactory = sessionFactory; + } + + public bool ContainsBaseMember(JoinClause joinClause) + { + // Visit the join inner key only for entities that have subclasses + if (joinClause.InnerSequence is ConstantExpression constantNode && + constantNode.Value is IEntityNameProvider entityNameProvider && + !_sessionFactory.GetEntityPersister(entityNameProvider.EntityName).EntityMetamodel.HasSubclasses) + { + return false; + } + + _result = false; + Visit(joinClause.InnerKeySelector); + + return _result; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (ExpressionsHelper.TryGetMappedType( + _sessionFactory, + node, + out _, + out var persister, + out _, + out var propertyPath) && + persister is IOuterJoinLoadable joinLoadable && + joinLoadable.EntityMetamodel.GetIdentifierPropertyType(propertyPath) == null && + joinLoadable.GetPropertyTableName(propertyPath) != joinLoadable.TableName + ) + { + _result = true; + return node; + } + + return base.VisitMember(node); + } + } + public override void VisitGroupJoinClause(GroupJoinClause groupJoinClause, QueryModel queryModel, int index) { throw new NotImplementedException(); From a129e75c3ac3ce423df110a682a6cae6819cfee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Sun, 1 Nov 2020 16:55:49 +0100 Subject: [PATCH 6/6] Release 5.3.4 (#2587) And document a missing breaking change. Fixes #2578 --- build-common/NHibernate.props | 2 +- releasenotes.txt | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index 62ec177ced1..67c6bbe5362 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -5,7 +5,7 @@ 5.3 4 - dev + $(NhVersion).$(VersionPatch) $(VersionSuffix).$(BuildNumber) diff --git a/releasenotes.txt b/releasenotes.txt index bfd2a67804c..b5766fbf4bb 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -1,4 +1,26 @@ -Build 5.3.3 +Build 5.3.4 +============================= + +Release notes - NHibernate - Version 5.3.4 + +6 issues were resolved in this release. + +** Bug + + * #2580 InvalidWithClauseException when join polymorphic entity + * #2559 Regression in caching linq query with ThenFetchMany statement. + * #2549 ApplyFilter does not work on join statements in LINQ + * #2537 Unable to cast "System.Linq.Expressions.UnaryExpression" to "System.Linq.Expressions.LambdaExpression". + +** Task + + * #2578 Add missing possible breaking changes for #2365 + * #2587 Release 5.3.4 + +As part of releasing 5.3.4, one missing 5.3.0 possible breaking change has been added, about +custom method generators for Linq. See 5.3.0 possible breaking changes. + +Build 5.3.3 ============================= Release notes - NHibernate - Version 5.3.3 @@ -135,6 +157,8 @@ Release notes - NHibernate - Version 5.3.0 already exists in the set will now return false. * Calling `ISet<>.Add` or `ICollection<>.Add` on an uninitialized set mapped as `lazy="true"` with a transient element that does not override `Equals` method will not initialize the collection. + * Linq custom generators deriving from `BaseHqlGeneratorForMethod` should override the + `TryGetCollectionParameter` method if they have to support parameter lists. ** Bug